Skip to content

Releases: eman/nwp500-python

v8.1.1

18 May 20:10

Choose a tag to compare

Release version 8.1.1

v8.1.0

17 May 00:07

Choose a tag to compare

Bug Fixes

  • Fix MQTT connection flapping after reconnect: When _active_reconnect()
    created a new MqttConnection, the old connection was never closed. The old
    SDK connection's built-in auto-reconnect would eventually succeed, creating two
    active connections sharing the same client ID. Because AWS IoT allows only one
    connection per client ID, the broker would kick one off, triggering
    on_connection_interrupted and starting yet another reconnection — an
    infinite connect/disconnect loop. Fixed by adding MqttConnection.close()
    (unconditional teardown regardless of _connected state) and calling it
    before creating the replacement connection in both _active_reconnect() and
    _deep_reconnect().

  • Thread-safety race in ensure_device_info_cached: The future.done()
    check and future.set_result() were performed in the AWS SDK callback thread
    without synchronisation, creating a race against the asyncio event loop thread.
    Moved both operations inside a call_soon_threadsafe callback so they execute
    atomically on the event loop thread.

  • ZeroDivisionError when deep_reconnect_threshold is 0: Config validation
    now clamps deep_reconnect_threshold to a minimum of 1, preventing a
    ZeroDivisionError in the exponential-backoff reconnection logic.

  • Reconnect counter never incremented: total_reconnect_attempts in
    diagnostics was not incremented on connection drops, so it always reported 0
    despite active reconnections. Counter is now incremented on each
    on_connection_interrupted event.

  • shortest_session_seconds not JSON-serialisable: The diagnostics
    to_dict() method used float('inf') as the initial value for
    shortest_session_seconds, which is not valid JSON. Changed to None
    so serialisation succeeds when no session has completed yet.

  • wait_for() future not bound to running loop: wait_for() created a
    bare asyncio.Future() rather than
    asyncio.get_running_loop().create_future(), which could bind the future to
    a different loop in multi-loop test setups.

  • Reservation temperature validation was US-only: build_reservation_entry
    validated set-point temperatures against hardcoded Fahrenheit bounds (95–150 °F)
    regardless of the active unit system. Validation now uses the current unit system
    context: 35–65 °C in metric mode, 95–150 °F in US mode. Celsius users previously
    received spurious ValueError rejections for valid temperatures.

  • Malformed reservation data silently dropped: build_reservation_entry now
    logs a warning when reservation hex data contains unexpected trailing bytes
    instead of silently dropping partial entries.

  • Unknown PeriodicRequestType silently ignored: The periodic-request handler
    now logs an error and breaks when it encounters an unknown request type instead of
    doing nothing.

  • Memory leak in device info cache: get_all_cached() only filtered expired
    entries from its return value but left them in the cache dictionary. Expired
    entries are now evicted during get_all_cached() to prevent unbounded growth.

v8.0.0

14 May 04:06

Choose a tag to compare

Release 8.0.0

v7.4.10

13 Apr 15:14

Choose a tag to compare

Changed

  • Loosened pydantic version requirement: Changed from pydantic>=2.12.5 to
    pydantic>=2.0.0 to resolve dependency conflicts with Home Assistant, which
    ships with pydantic==2.12.2.

v7.4.9

13 Apr 06:00
21f97b7

Choose a tag to compare

Added

  • Firmware Payload Capture Tool: New example script
    examples/advanced/firmware_payload_capture.py for capturing raw MQTT
    payloads to detect firmware-introduced protocol changes. Subscribes to all
    response and event topics via wildcards, requests the full scheduling
    data set (weekly reservations, TOU, device info), and saves everything to a
    timestamped JSON file suitable for jq/diff comparison across firmware
    versions.

Fixed

  • Timezone-naive datetime in token expiry checks: AuthTokens.is_expired,
    are_aws_credentials_expired, and time_until_expiry used
    datetime.now() (naive, local time). During DST transitions or timezone
    changes this could cause incorrect expiry detection, leading to premature
    re-authentication or use of an actually-expired token. Fixed by using
    datetime.now(UTC) throughout, switching the issued_at field default
    to datetime.now(UTC), and adding a field validator to normalize any
    timezone-naive issued_at values loaded from old stored token files to UTC
    (previously this would raise a TypeError at comparison time). The
    validator was further extended to also handle ISO 8601 strings without
    timezone info (e.g. "2026-02-17T14:47:01.686943"), which is the actual
    format written by to_dict() for tokens stored before this fix.
  • Vacation mode sent wrong MQTT command: set_vacation_days() used
    CommandCode.GOOUT_DAY (33554466), which the device silently accepted
    but did not activate vacation mode — the operating mode remained unchanged.
    HAR capture of the official Navien app confirms the correct command is
    DHW_MODE (33554437) with param=[5, days]
    (DhwOperationSetting.VACATION). The valid range has also been corrected
    from 1–365 to 1–30 to match the device's actual constraint.
  • Duplicate AWS IoT subscribe calls on reconnect: 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.
  • Anti-Legionella set-period State Preservation: nwp-cli anti-legionella set-period was calling enable_anti_legionella() in both the enabled and
    disabled branches, silently re-enabling the feature when it was off. The
    command now informs the user that the period can only be updated while the
    feature is enabled and directs them to anti-legionella enable.
  • Subscription State Lost After Failed Resubscription: resubscribe_all()
    cleared _subscriptions and _message_handlers before the re-subscribe
    loop. Topics that failed to resubscribe were permanently dropped from internal
    state and could not be retried on the next reconnection. Failed topics are now
    restored so they are retried automatically.
  • Unit System Detection Returns None on Timeout: _detect_unit_system()
    declared return type UnitSystemType but returned None on
    TimeoutError, violating the type contract. Now returns
    "us_customary" consistent with the warning message.
  • Once-Listener Becomes Permanent With Duplicate Callbacks: 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.
  • Auth Session Leaked on Client Construction Failure: In
    create_navien_clients(), if NavienAPIClient or
    NavienMqttClient construction raised after a successful
    auth_client.__aenter__(), the auth session and its underlying
    aiohttp session would leak. Client construction is now wrapped in a
    try/except that calls auth_client.__aexit__() on failure.
    Additionally, both except BaseException blocks have been replaced with
    except Exception (passing real exception info to __aexit__) plus a
    separate except asyncio.CancelledError block that uses
    asyncio.shield() to ensure cleanup completes even when the task is
    being cancelled.
  • Hypothesis Tests Broke All Test Collection: test_mqtt_hypothesis.py
    imported hypothesis at module level; when it was not installed, pytest
    failed to collect every test in the suite. hypothesis is now mandated
    as a [testing] extra dependency, restoring correct collection behaviour.

Changed

  • Dependency updates: Bumped minimum versions to track current releases:
    aiohttp >= 3.13.5, pydantic >= 2.12.5, click >= 8.3.0,
    rich >= 14.3.0.
  • Dependency: awsiotsdk >= 1.28.2: Bumped minimum awsiotsdk version
    from >=1.27.0 to >=1.28.2 to track the current patch release.
    awscrt 0.31.3 is pulled in transitively.

v7.4.8

17 Feb 23:09

Choose a tag to compare

Added

  • Reservation CRUD Helpers: New public functions fetch_reservations(),
    add_reservation(), delete_reservation(), and update_reservation()
    in nwp500.reservations (and exported from nwp500). These abstract the
    read-modify-write pattern for single-entry schedule management so library
    users no longer need to fetch the full schedule, splice it manually, and send
    it back. The CLI now delegates to these library functions.

v7.4.7

17 Feb 21:16

Choose a tag to compare

Added

  • OpenEI Client Module: New OpenEIClient async client (nwp500.openei) for browsing utility rate plans from the OpenEI API by zip code. Supports listing utilities, filtering rate plans, and fetching plan details. API key read from OPENEI_API_KEY environment variable.
  • Convert TOU API: NavienAPIClient.convert_tou() sends raw OpenEI rate data to the Navien backend for server-side conversion into device-ready TOU schedules with season/week bitfields and scaled pricing.
  • Update TOU API: NavienAPIClient.update_tou() applies a converted TOU rate plan to a device, matching the mobile app's PUT /device/tou endpoint.
  • ConvertedTOUPlan Model: New Pydantic model for parsed convert_tou() results (utility, name, schedule).
  • CLI tou rates: Browse utilities and rate plans for a zip code (nwp500 tou rates 94903).
  • CLI tou plan: View converted rate plan details with decoded pricing (nwp500 tou plan 94903 "EV Rate A").
  • CLI tou apply: Apply a rate plan to the water heater with optional --enable flag to activate TOU via MQTT.
  • CLI Reservations Table Output: nwp-cli reservations get now displays reservations as a formatted table by default with global status indicator (ENABLED/DISABLED). Use --json flag for JSON output.
  • CLI anti-legionella set-period: New subcommand to change the Anti-Legionella cycle period (1-30 days) without toggling the feature. Use nwp-cli anti-legionella set-period 7 to update cycle period.

Changed

  • examples/advanced/tou_openei.py: Rewritten to use the new OpenEIClient and convert_tou()/update_tou() library methods instead of inline OpenEI API calls and client-side conversion.

Fixed

  • Week Bitfield Encoding (CRITICAL): Fixed MGPP week bitfield encoding to match NaviLink APK protocol. Sunday is now correctly bit 7 (128), Monday bit 6 (64), ..., Saturday bit 1 (2); bit 0 is unused. Affects all reservation and TOU schedule operations. Verified against reference captures.
  • Enable/Disable Convention: Fixed reservation and TOU enable/disable flags to use standard device boolean convention (1=OFF, 2=ON) instead of inverted logic. This aligns with other device binary sensors and matches app behavior. Global reservation status now correctly shows DISABLED when reservationUse=1.
  • Reservation Set Command Timeout: Fixed reservations set subscription pattern that had extra wildcards preventing response matching. Command now receives confirmations correctly.
  • Intermittent Fetch Bug: Tightened MQTT topic filter for reservation fetch from /res/ to /res/rsv/ with content validation to prevent false matches on unrelated response messages.

v7.4.6

14 Feb 02:27

Choose a tag to compare

Fixed

  • Converter Consistency: div_10() and mul_10() now correctly apply division/multiplication to all input types after float() conversion, not just int/float types
  • Reservation Decoding: Fixed decode_reservation_hex() to validate chunk length before checking for empty entries, preventing potential out-of-bounds access
  • Factory Cleanup: create_navien_clients() now properly cleans up auth session if authentication fails during context manager entry
  • MQTT Reconnection: MQTT client now resubscribes to all topics after successful reconnection
  • Subscription Leak: Fixed resource leak where wait_for_device_feature() did not unsubscribe its callback after completion
  • Duplicate Handlers: Subscription manager now prevents duplicate callback registration for the same topic
  • Command Queue: MqttCommandQueue now raises on QueueFull instead of silently swallowing the error
  • Flow Rate Metadata: Removed hardcoded "GPM" unit from recirc_dhw_flow_rate field; unit is now dynamic based on unit system
  • Temperature Rounding: RawCelsius Fahrenheit conversion now uses a catch-all default for standard rounding instead of matching only STANDARD enum value
  • Unit System Default: is_metric_preferred() now returns False (Fahrenheit) instead of None when no unit system override or context is set

Security

  • Sensitive Data Logging: Redacted MQTT topics in subscription manager logging to prevent leaking device IDs (resolves CodeQL alerts)

Added

  • Auth Session Property: Added NavienAuthClient.session property to access the active aiohttp session without using getattr
  • Unsubscribe Feature: Added unsubscribe_device_feature() method to MQTT client and subscription manager for targeted callback removal
  • Hypothesis Fuzzing: Added property-based fuzzing tests for MQTT payload handling
  • Bandit Security Scanning: Added bandit configuration for security analysis in CI

v7.4.5

04 Feb 18:32

Choose a tag to compare

Fixed

  • Energy Capacity Unit Scaling: Corrected unit scaling for energy capacity fields that were off by a factor of 10
  • CLI Output: Fixed linting issue by replacing str and Enum with StrEnum for InstallType

v7.3.4

27 Jan 23:37
502e87c

Choose a tag to compare

Fixed

  • Temperature Delta Conversions: Fixed incorrect Fahrenheit conversion for differential temperature settings (heat pump and heater element on/off thresholds)

    • Created new DeciCelsiusDelta class for temperature deltas that apply scale factor (9/5) but NOT the +32 offset
    • Heat pump and heater element differential settings now use DeciCelsiusDelta instead of DeciCelsius
    • hp_upper_on_diff_temp_setting, hp_lower_on_diff_temp_setting, he_upper_on_diff_temp_setting, he_lower_on_diff_temp_setting, and related off settings now convert correctly to Fahrenheit
    • Example: A device value of 5 (representing 0.5°C delta) now correctly converts to 0.9°F delta instead of 32.9°F
  • CLI Output: Added display of heat pump and heater element differential temperature settings in device status output

Changed

  • Internal API: Added div_10_celsius_delta_to_preferred converter for temperature delta values in device models