Skip to content

V1.18.0#689

Merged
jasonacox merged 13 commits into
masterfrom
v1.17.7
Apr 13, 2026
Merged

V1.18.0#689
jasonacox merged 13 commits into
masterfrom
v1.17.7

Conversation

@jasonacox
Copy link
Copy Markdown
Owner

@jasonacox jasonacox commented Mar 5, 2026

v1.18.0 - UX Improvements and Devices.json Fixes

  • Scanner: Improved messaging for devices with no IP address — now clearly indicates the device may be battery-powered or sleeping and that local control is not supported, instead of the generic "Error: No IP found".
  • Wizard: When the Tuya Cloud API returns a "permission deny" error (or error code 1010), the wizard now prints a targeted hint suggesting the user check their IoT Core service subscription at https://iot.tuya.com.
  • README: Added troubleshooting notes clarifying battery-powered device limitations and warning against aggressive polling intervals that can cause devices to drop or reset their connection.
  • CLI (on, off, set, get): Improved handling of device local keys that contain special shell characters ($, #, =, :, !) - re: #688:
    • If --key is omitted and the key is not found in devices.json, the CLI now prompts interactively for the key. Input at a terminal prompt bypasses shell interpretation entirely, so no quoting or escaping is needed.
    • Added key length validation — Tuya local keys are always exactly 16 characters. If the resolved key is the wrong length (the most common symptom of a shell-escaping problem), a clear error is printed with platform-specific quoting tips for Linux/Mac and Windows CMD.

- Added troubleshooting notes in README for battery-powered device limitations and polling warnings.
- Enhanced scanner output to clarify when no IP is found, indicating potential battery-powered or offline status.
- Updated wizard to provide hints for "permission deny" errors, suggesting users check their Tuya IoT subscription.
- Bump version to 1.17.7.
Copilot AI review requested due to automatic review settings March 5, 2026 05:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

v1.17.7 focuses on UX improvements across the setup wizard, network scanner output, CLI ergonomics, and documentation to better guide users through common failure modes (battery/offline devices, cloud permission errors, and shell-escaping pitfalls for local keys).

Changes:

  • Wizard: add a targeted hint for “permission deny” / code 1010 Tuya Cloud errors.
  • Scanner/README/RELEASE: improve messaging + troubleshooting guidance around battery-powered/offline devices and polling frequency.
  • CLI: prompt for missing --key and validate local key length with clearer error tips.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tinytuya/wizard.py Adds a “permission deny / 1010” hint when cloud calls fail.
tinytuya/scanner.py Updates “no IP” status line to be more descriptive.
tinytuya/core/core.py Bumps version tuple to 1.17.7.
tinytuya/__main__.py Improves CLI --key handling (prompt + length validation + usage tips).
RELEASE.md Adds release notes for v1.17.7.
README.md Adds troubleshooting notes on battery devices and polling rates.
Comments suppressed due to low confidence (3)

tinytuya/scanner.py:1845

  • This new message says "Battery-powered or offline", but the PR description/release notes say the scanner should also clearly indicate that local control is not supported when there is no IP. Consider updating the message here to include that local control isn't possible without an IP (and possibly that the device may be sleeping).
        print("    %s[%-25.25s] %sNo IP found - Battery-powered or offline%s" %
              (term.subbold, name, term.alertdim, term.normal))

tinytuya/wizard.py:196

  • The 1010 detection uses a broad substring match ('1010' in str(err)), which can produce false positives if other codes/messages contain that sequence. Since Cloud errors appear to be formatted like "Code 1010" (see Cloud.py), consider matching more specifically (e.g., checking for "code 1010" case-insensitively) or extracting the numeric code when available.
            if 'permission' in str(err).lower() or '1010' in str(err):
                print(bold + 'Hint: ' + dim + 'This may indicate your Tuya IoT subscription has expired.')

tinytuya/wizard.py:216

  • This same hint logic is duplicated in multiple error branches. Consider factoring it into a small helper (or a local function) so future changes to the hint text/conditions only need to be made once.
            if 'permission' in str(err).lower() or '1010' in str(err):
                print(bold + 'Hint: ' + dim + 'This may indicate your Tuya IoT subscription has expired.')
                print('      Visit https://iot.tuya.com to check and renew your IoT Core service.')

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tinytuya/__main__.py Outdated
Comment thread tinytuya/__main__.py Outdated
Comment thread tinytuya/__main__.py Outdated
- Strip leading/trailing whitespace from keys from all sources (--key,
  devices.json, prompt) before length validation, preventing false
  failures from copy-paste artifacts
- Guard interactive key prompt with sys.stdin.isatty(); non-interactive
  contexts (cron, CI, piped input) now exit with a clear error instead
  of blocking indefinitely
- Switch key prompt from input() to getpass.getpass() so the key is not
  echoed to the terminal or captured in screen recordings/logs; prompt
  text notes that input is hidden
@uzlonewolf
Copy link
Copy Markdown
Collaborator

At some point I would like to add something like the "monitor" example to make it easier for people with devices that only send updates asynchronously to figure that out (i.e. #673). Call it monitor? I'll see if I can find time to add it later today.

@jasonacox
Copy link
Copy Markdown
Owner Author

I love that idea!

# using devices.json
tinytuya monitor --name "Kitchen Light"

# direct
tinytuya monitor --id $DEVICE_ID --key $KEY --ip $IP --version $VER

@uzlonewolf
Copy link
Copy Markdown
Collaborator

I felt __main__ was starting to grow a bit too big and so I moved all the CLI functions into a new file, cli.py.

I also reworked the argpase config a bit and moved the CLI commands up into the main for loop. This allows it to pick up both -debug options as well as the scanner -maxtime/-force/etc options.

I kinda want to add keyboard-interactive DP set commands now but that's going to have to wait for next time 😆

@uzlonewolf
Copy link
Copy Markdown
Collaborator

uzlonewolf commented Mar 8, 2026

Oh yeah, either --id or --name are now required and are mutually exclusive, I reworked a few of the messages, and omitting --version now kicks off a scan instead of defaulting to v3.3.

@uzlonewolf
Copy link
Copy Markdown
Collaborator

The fact that all the old commands use -arg but all the new ones use --arg is going to irk me to no end now 😆

@jasonacox
Copy link
Copy Markdown
Owner Author

The fact that all the old commands use -arg but all the new ones use --arg is going to irk me to no end now 😆

I thought the same. I'm glad you added the option for both formats... honors the old API but accomodates the more modern --.

@jasonacox
Copy link
Copy Markdown
Owner Author

I thought about merging this separate from documentation, but since the doc changes are simple, it is probably good to combine under same version. Let me know if you agree.

@uzlonewolf
Copy link
Copy Markdown
Collaborator

Sounds good to me.

@jasonacox jasonacox changed the title V1.17.7 V1.18.0 Apr 12, 2026
@jasonacox
Copy link
Copy Markdown
Owner Author

jasonacox commented Apr 12, 2026

This seems ready to merge. Instead of overloading this PR with the Docs, I'll separate it since that seems like a larger effort.

I'm going to run test and another code review.

Server container to test:

jasonacox/tinytuya:1.18.0p16-beta

Requesting code review from CoPilot and @jasonacox-sam ...

@jasonacox-sam
Copy link
Copy Markdown
Collaborator

[Watt — Agent Code Review]

Overall: This is a clean, well-structured PR. The refactoring of CLI logic into cli.py, the new _build_device() helper, key validation, interactive prompt, and monitor command are all solid improvements. A few observations:


✅ What looks good

  • _build_device() extraction — DRY improvement. Both _run_device_command() and _monitor_device() share device resolution logic. Clean separation.
  • Key length validation — 16-char check with platform-specific shell escaping tips is a great UX fix for Key not accepted on commandline #688.
  • Interactive getpass prompt — Correctly gates on sys.stdin.isatty() so CI/piped contexts get a clear error instead of hanging. Good.
  • --id/--name mutually exclusive — Cleaner than the old optional-both approach. Explicit is better.
  • Scanner messaging — "No IP found - Battery-powered or offline" is much more helpful than "Error: No IP found".
  • Wizard permission deny hint — Targeted and actionable.
  • can_end_early in scanner — Skipping snapshot saving during targeted lookups is a sensible optimization.

⚠️ Minor observations

1. _monitor_device()KeyboardInterrupt handling is inconsistent

KeyboardInterrupt is caught inside the receive() branch but not during d.status() or d.heartbeat() calls:

        elif time.time() >= heartbeat_time:
            data = d.heartbeat(nowait=False)  # no KeyboardInterrupt catch
        else:
            try:
                data = d.receive()
            except KeyboardInterrupt:  # only here
                print(" > Keyboard Interrupt, Exiting! < ")
                break

These are quick calls so it's unlikely in practice, but if you Ctrl-C during a status poll, it'll propagate as an unhandled exception. Consider wrapping the entire loop body in a try/except, or at least the status/heartbeat branches.

2. _monitor_device()status_time is never initialized for periodic polling

    status_time =  None

Since status_time starts as None, the periodic status refresh (if status_time and time.time() >= status_time) never fires — only the heartbeat and receive() branches run. After the first heartbeat fires, status_time gets set... but only inside the status_time branch which never executes. So periodic status polling never actually happens.

This looks like it might be intentional (rely on async updates only), but the commented-out UPDATEDPS code suggests this was meant to be active. If periodic status polling is desired, initialize status_time = time.time() + STATUS_TIMER or set it after the initial status call.

3. -device-file vs --device-file inconsistency

uzlonewolf already flagged this. The PR adds --device-file as an alias for -device-file, which is good. Just noting it as a known inconsistency that's being tracked.

4. Monitor command doesn't handle device disconnection gracefully

If the persistent connection drops mid-monitor, d.receive() will throw a socket error. There's no reconnect logic. For a first version this is fine — Ctrl-C and reconnect is reasonable — but worth noting for a future iteration.


Summary

The core logic is correct, the refactoring is clean, and the UX improvements are well-targeted. Items 1 and 2 above are minor but worth a quick look before merge. Items 3 and 4 are known/future-work.

Verdict: LGTM with minor nits. 🚢


Code review by Watt (TinyTuya agent). @jasonacox please verify before merging.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tinytuya/cli.py
Comment thread tinytuya/cli.py Outdated
Comment thread tinytuya/__main__.py Outdated
Comment thread tinytuya/__main__.py Outdated
@jasonacox
Copy link
Copy Markdown
Owner Author

jasonacox commented Apr 12, 2026

@jasonacox-sam (Watt) — Agent Code Review

⚠️ Observation 1: KeyboardInterrupt only caught in receive() branch

KeyboardInterrupt is caught inside the receive() branch but not during d.status() or d.heartbeat() calls.

Fixed. Restructured the entire while loop in _monitor_device() to use a single outer try/except KeyboardInterrupt, replacing the nested per-branch try/except. Ctrl-C now exits cleanly regardless of which call is in progress.

⚠️ Observation 2: status_time is never initialized for periodic polling

Since status_time starts as None, the periodic status refresh never fires — only the heartbeat and receive() branches run.

Fixed. Changed status_time = None to status_time = time.time() + STATUS_TIMER (initialized right after the initial d.status() call). Periodic polling now fires every 30 seconds as intended.

⚠️ Observation 3: -device-file vs --device-file inconsistency

The PR adds --device-file as an alias for -device-file, which is good. Just noting it as a known inconsistency.

Partially addressed. The device argument-group description in __main__.py was referencing -device-file in isolation. Updated the text to read --device-file/-device-file to reflect both accepted forms. The underlying single-vs-double-dash inconsistency across all options is a pre-existing issue tracked separately.

⚠️ Observation 4: Monitor command doesn't handle device disconnection gracefully

If the persistent connection drops mid-monitor, d.receive() will throw a socket error. There's no reconnect logic.

Acknowledged — deferred. For this first version, the behavior (exception surfaces + user Ctrl-C + rerun) is acceptable. Reconnect logic will be a separate follow-up, as it requires a broader retry/backoff design.

@jasonacox
Copy link
Copy Markdown
Owner Author

Container Testing....

jasonacox/tinytuya:1.18.0p16-beta1

@jasonacox jasonacox merged commit 079fe3d into master Apr 13, 2026
32 checks passed
@jasonacox jasonacox deleted the v1.17.7 branch April 13, 2026 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants