Conversation
- Use pytest.importorskip() for hypothesis in test_mqtt_hypothesis.py so that a missing hypothesis install skips the tests rather than breaking all test collection - Bump awsiotsdk minimum from >=1.27.0 to >=1.28.2 (latest patch release); awscrt 0.31.3 is pulled in transitively Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- handlers.py: anti-legionella set-period else branch called enable_anti_legionella() in both branches; else now correctly calls disable_anti_legionella() to preserve the disabled state - __main__.py: _detect_unit_system() returned None on TimeoutError violating its UnitSystemType return type; now returns 'us_customary' matching the warning message - events.py: once-listener detection in emit() used _once_callbacks set lookup which deduplicates by (event, callback); if the same callback was registered twice with once=True the second listener would become permanent after first emit; now uses listener.once directly - subscriptions.py: resubscribe_all() cleared internal state before re-subscribing; failed topics were permanently lost from memory; now restores failed entries so they are retried on next reconnection - factory.py: if NavienAPIClient/NavienMqttClient constructors raised after successful auth.__aenter__(), the auth session leaked; now wrapped in try/except with proper cleanup - tests/test_mqtt_hypothesis.py: add noqa: E402 to imports following pytest.importorskip() to satisfy ruff line-order check
There was a problem hiding this comment.
Pull request overview
Routine maintenance updates across the dependency surface, test collection behavior, and several runtime correctness fixes in the CLI, MQTT subscription manager, event emitter, and client factory.
Changes:
- Bump
awsiotsdkminimum version and ensure Hypothesis-based tests skip cleanly when Hypothesis isn’t installed. - Fix multiple logic issues (anti-legionella CLI state handling, subscription resubscribe state restoration, unit-system timeout return value, once-listener handling).
- Prevent auth-session leakage when client construction fails after auth context entry.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
tests/test_mqtt_hypothesis.py |
Avoids pytest collection failure by skipping the file when Hypothesis is missing. |
src/nwp500/mqtt/subscriptions.py |
Restores failed topics/handlers into internal state so resubscribe can be retried on next reconnect. |
src/nwp500/factory.py |
Ensures auth context is exited if API/MQTT client construction raises after successful __aenter__(). |
src/nwp500/events.py |
Fixes once-listener semantics by using the listener’s once flag instead of a deduplicating set lookup. |
src/nwp500/cli/handlers.py |
Adjusts anti-legionella set-period logic to avoid always enabling the feature (but see review comment). |
src/nwp500/cli/__main__.py |
Returns a concrete UnitSystemType on timeout instead of None. |
setup.cfg |
Updates minimum awsiotsdk requirement to >=1.28.2. |
You can also share your feedback on Copilot code review. Take the survey.
- Revert pytest.importorskip() for hypothesis: hypothesis is already mandated via setup.cfg [testing] extras and installed by tox; the skip pattern was masking a misconfigured environment rather than solving the root cause - Anti-Legionella set-period: when the feature is disabled, disable_anti_legionella() takes no period_days argument so the period was silently not updated while printing a success message. Now informs the user that the period can only be set while the feature is enabled and directs them to 'anti-legionella enable'
…e capture tool; bump deps - auth.py: use datetime.now(UTC) in is_expired, are_aws_credentials_expired, and time_until_expiry; switch issued_at default to UTC-aware - subscriptions.py: resubscribe_all() now makes one network call per topic instead of one per handler, avoiding duplicate AWS IoT subscribe requests on every reconnect - examples/advanced/firmware_payload_capture.py: new tool to capture raw MQTT scheduling payloads for firmware change detection - setup.cfg: bump aiohttp>=3.13.5, pydantic>=2.12.5, click>=8.3.0, rich>=14.3.0 - Update tests to use UTC-aware datetimes throughout - Update CHANGELOG.rst
…f sensitive information' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…f sensitive information' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…f sensitive information' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Maintenance update that bumps key dependency minimums, hardens test infrastructure, and fixes several correctness issues across auth, MQTT reconnection/subscriptions, CLI behavior, and event listener semantics.
Changes:
- Updated minimum dependency versions (aiohttp/awsiotsdk/pydantic/click/rich) and documented changes in the changelog.
- Fixed multiple logic bugs: timezone-aware token expiry checks, MQTT resubscription handler duplication/state loss, once-listener semantics, and CLI unit-system fallback.
- Added a new advanced example for capturing firmware-related MQTT payloads.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
setup.cfg |
Bumps minimum dependency versions and CLI extras. |
CHANGELOG.rst |
Adds Unreleased notes documenting dependency bumps and bug fixes. |
src/nwp500/auth.py |
Moves token timing logic to UTC-aware datetimes for expiry calculations. |
src/nwp500/mqtt/subscriptions.py |
Improves resubscribe_all() to avoid duplicate network subscribes and to retain failed topics for retry. |
src/nwp500/factory.py |
Adds cleanup guard to avoid leaking the auth session if client construction fails. |
src/nwp500/events.py |
Fixes once-listener removal logic when the same callback is registered multiple times. |
src/nwp500/cli/__main__.py |
Ensures _detect_unit_system() returns a valid UnitSystemType on timeout. |
src/nwp500/cli/handlers.py |
Adjusts anti-legionella set-period behavior to avoid silently enabling when currently disabled. |
tests/test_auth.py |
Updates tests to use UTC-aware datetimes and adjusts expected serialization. |
tests/test_mqtt_client_init.py |
Updates tests to use UTC-aware datetimes for token expiry scenarios. |
examples/advanced/firmware_payload_capture.py |
New script to subscribe broadly and persist captured MQTT payloads for firmware diffing. |
…output The CodeQL autofix attempts removed 'mac = device.device_info.mac_address' while keeping its usage, causing F821 undefined name lint errors. Proper fix: - Restore mac variable (required to build the evt/# wildcard subscription topic) - Use existing redact() and redact_topic() utilities from nwp500.mqtt.utils for all console output and saved JSON (replaces hand-rolled regex approach) - Saved JSON now has macAddress, sessionID, and clientID fields redacted, resolving the CodeQL clear-text logging finding
…tory - auth.py: add field_validator to normalize timezone-naive issued_at values from old stored token files to UTC, preventing TypeError when comparing against datetime.now(UTC) at expiry check time - factory.py: replace BaseException with Exception + asyncio.CancelledError (shielded cleanup), pass real (exc_type, exc, traceback) to __aexit__ so context manager semantics are properly preserved - CHANGELOG: update auth fix entry and factory fix entry to reflect the actual implementation details
Summary
Routine maintenance pass covering dependency updates, a broken test collection issue, and logic bugs found during a code review.
Dependency & Test Infrastructure
awsiotsdk minimum version bumped to 1.28.2
The previously declared minimum (>=1.27.0) was behind the current patch release. Bumped to >=1.28.2, which also pulls in awscrt 0.31.3 transitively.
Hypothesis added as a [testing] extra dependency
tests/test_mqtt_hypothesis.py imported hypothesis at module level. When hypothesis was not installed, pytest failed to collect all tests — not just that file. Added hypothesis to the [testing] extras in setup.cfg so it is always present when running tests, restoring correct collection behavior.
Bug Fixes
Anti-Legionella set-period always enabled the feature (cli/handlers.py)
Both if/else branches of the enabled-state check called enable_anti_legionella(). A device with anti-legionella disabled would be silently re-enabled when the user ran nwp-cli anti-legionella set-period. The else branch now informs the user that the period can only be changed while the feature is enabled and directs them to run anti-legionella enable first.
Subscriptions permanently lost after failed resubscription (mqtt/subscriptions.py)
resubscribe_all() cleared _subscriptions and _message_handlers before the re-subscribe loop. Any topic that failed to resubscribe was dropped from internal state entirely and could never be retried on the next reconnection. Failed topics are now restored to internal state so they are retried on the next reconnect.
_detect_unit_system() returned None on timeout (cli/main.py)
The function declared return type UnitSystemType but returned None on TimeoutError. Now returns "us_customary" to match the warning message and the type contract.
Once-listener could become permanent with duplicate callbacks (events.py)
emit() identified once-listeners via a set of (event, callback) tuples. If the same callback was registered twice with once=True, the set deduplicated the tuple — after the first emit, the second listener lost its once-status and became permanent. Fixed by checking listener.once directly on the EventListener object instead.
Auth session leaked if client construction raised (factory.py)
In create_navien_clients(), the NavienAPIClient and NavienMqttClient constructors were called after auth_client.aenter() but outside its cleanup guard. If either constructor raised, the auth session and its underlying aiohttp session would leak. Client construction is now wrapped in try/except blocks that call auth_client.aexit() on failure. Both except BaseException blocks have been replaced with except Exception (with real exception info passed to aexit) plus a separate asyncio.CancelledError handler that uses asyncio.shield() to ensure cleanup completes even when the task is being cancelled.
Timezone-naive datetime caused TypeError when loading old stored tokens (auth.py)
Old stored token JSON files wrote datetime strings without timezone offset. After switching all comparisons to datetime.now(UTC), loading a naive issued_at would raise TypeError at the first expiry check. A field validator now normalizes any timezone-naive issued_at to UTC on load, restoring backward compatibility.
Duplicate AWS IoT subscribe calls on reconnect (mqtt/subscriptions.py)
resubscribe_all() called connection.subscribe() (a network round-trip to AWS IoT) once per handler per topic. If a topic had N handlers, N identical subscribe requests were sent on every reconnect. Fixed by making one network call per unique topic and registering remaining handlers directly into _message_handlers.
Test Results