Skip to content

Latest commit

 

History

History
2619 lines (2069 loc) · 125 KB

File metadata and controls

2619 lines (2069 loc) · 125 KB

Changelog

Unreleased

Version 9.2.0 (2026-07-06)

Changed

  • CLI formatting stacks merged; Rich is the sole human renderer (#100): a new src/nwp500/cli/presentation.py owns all data-shaping for CLI output (field selection, labels, units, ordering, value formatting and energy aggregation) as presentation-neutral structures. Because the CLI hard-requires rich (there is no plain-text fallback), the redundant plain-text human renderer and the Rich-vs-plain fallback machinery were removed: rich_output.py no longer contains _should_use_rich, _rich_available, the NWP500_NO_RICH toggle, or any _print_*_plain methods, and now renders the neutral structures (including the energy TOTAL SUMMARY) exclusively with Rich, consuming the presentation dataclasses directly instead of dict adapters. output_formatters.py keeps only JSON/CSV rendering plus thin human-output dispatch. Net CLI code drops from ~2088 to ~1838 lines with a single data-shaping layer and a single human renderer. Non-energy output is byte-for-byte unchanged (verified by golden capture and the existing CLI tests); energy output now renders a summary table plus a breakdown table entirely in Rich rather than printing plain text and a Rich table.

  • mqtt/client.py slimmed toward a thin façade (#99): the ~40 device control command proxies and the typed subscribe_*/unsubscribe_* device-subscription proxies were moved out of NavienMqttClient into two focused mixins, DeviceControlCommandsMixin (mqtt/_control_commands.py) and DeviceSubscriptionsMixin (mqtt/_device_subscriptions.py). NavienMqttClient now inherits both, so its public API is unchanged, while mqtt/client.py shrinks from ~1572 to ~1196 lines and reads more clearly as connection orchestration plus a public façade. No behavior change.

  • Event system relationship clarified and de-duplicated (#102): events.py and mqtt_events.py are not two competing event mechanisms. EventEmitter (events.py) is the sole delivery mechanism, while mqtt_events.py provides the event-name registry (MqttClientEvents) and the typed dataclass payloads it carries. Internal emit call sites in mqtt/state_tracker.py, mqtt/client.py and mqtt/subscriptions.py now reference the MqttClientEvents constants instead of duplicating the raw event-name strings, so each event name is defined in exactly one place. Module docstrings and the event-system reference docs were updated to document the relationship, and typed-payload delivery is now covered by tests.

  • awscrt types wrapped out of public MQTT signatures (#101): the library no longer exposes awscrt SDK types on its own public/semi-public API surface. A library-owned nwp500.mqtt.QoS IntEnum (also exported as nwp500.QoS) now replaces awscrt.mqtt.QoS on the publish and subscribe methods of NavienMqttClient, MqttConnection and MqttSubscriptionManager, on MqttCommandQueue.enqueue and on the QueuedCommand dataclass. awscrt.mqtt.Connection handles are typed behind the MqttConnectionHandle alias, and translation to/from awscrt happens only at the connection-layer boundary (nwp500/mqtt/types.py). NavienMqttClient.publish now wraps otherwise-uncaught AwsCrtError in MqttPublishError so awscrt exceptions no longer leak out of the public boundary.

    # OLD
    from awscrt import mqtt
    await client.publish(topic, payload, qos=mqtt.QoS.AT_LEAST_ONCE)
    
    # NEW
    from nwp500 import QoS
    await client.publish(topic, payload, qos=QoS.AT_LEAST_ONCE)

Fixed

  • MQTT ack futures now consumed on abandonment (#97): _await_ack() in mqtt/connection.py and the inline subscribe/unsubscribe acknowledgement waits in mqtt/subscriptions.py shield the AWS CRT future so a timeout or cancellation doesn't propagate into the SDK future. Previously, if the shielded future later completed with an exception (e.g. AwsCrtError from clean-session cancellation during reconnect) after the awaiting task had already given up, nobody retrieved that exception, and asyncio logged a "Future exception was never retrieved" warning at garbage-collection time. A done callback is now attached whenever a wait is abandoned so the eventual result/exception is always retrieved and logged at debug level instead of leaking as an unhandled asyncio warning.

Documentation

  • Unit-system preference is a deliberate process-wide global (#103): clarified and locked in that the preference in nwp500.unit_system is an intentional process-wide module-level global rather than a contextvars.ContextVar. A context-local value silently reverted to auto-detect for real-time data because MQTT message handling runs in tasks scheduled from AWS CRT callback threads whose context never inherits from the application task. Added prominent docstring notes to the affected models (DeviceStatus, DeviceFeature, ReservationEntry, WeeklyReservationEntry, and TOUPeriod) explaining that unit-aware computed fields read the preference at access time and share it across all async tasks and threads. This is a non-breaking documentation and test change; no API changes.

Added

  • Tests for process-wide unit-system semantics (#103): new tests/test_unit_system_process_wide.py verifies that setting the global preference affects already-constructed model instances at access time and that the preference is visible across async tasks and threads.

Version 9.0.0 (2026-07-05)

BREAKING CHANGES: Public API surface trimmed and dead code removed. These changes require a major version bump.

Modernization (Python 3.14)

  • Removed from __future__ import annotations project-wide (57 files): redundant on a Python >=3.14-only package where lazy annotation evaluation (PEP 649) is the default.
  • Adopted additional ruff rule sets: RUF (Ruff-specific), DTZ (naive datetime), PTH (pathlib), ASYNC, and PERF, with documented ignores for intentional patterns (grouped __all__ lists, unicode degree signs, public timeout parameters). Fixes applied for all resulting findings, including a timezone-naive timestamp in CSV exports (now timezone-aware local time), Path.open() usage, itertools.pairwise() in the CLI range collapser, a ClassVar annotation on the capability map, and dangling asyncio.create_task references in tests.
  • PeriodicRequestType is now a StrEnum (was a plain Enum with string values), matching the enum style used elsewhere.
  • Event payload dataclasses use slots=True: the frozen event dataclasses in mqtt_events.py and events.EventListener are created per state change; slots reduce their memory footprint.
  • match statement for the periodic request-type dispatch.

Testing

  • New unit tests for previously untested modules: topic_builder.py (full topic schema), field_factory.py (metadata defaults, overrides, merge semantics), and models/_converters.py (unit-preference conversions and round-trips).

Removed

  • Deprecated ``.control`` property: removed NavienMqttClient.control (deprecated shim slated for v9.0.0; the project policy is to remove rather than deprecate). Use the delegated methods on the client directly:

    # OLD (removed)
    await client.control.set_power(device, True)
    
    # NEW
    await client.set_power(device, True)
  • Unused exception classes: removed TokenExpiredError, MqttSubscriptionError, DeviceNotFoundError, DeviceOfflineError, and DeviceOperationError. None of them was ever raised by the library, so no working error handling can break; catch the parent classes (AuthenticationError, MqttError, DeviceError) instead.

  • Internal plumbing removed from the top-level namespace: the package no longer re-exports requires_capability, MqttDeviceInfoCache, MqttDeviceCapabilityChecker, log_performance, or the bit-encoding helpers (encode_week_bitfield, decode_week_bitfield, encode_season_bitfield, decode_season_bitfield, encode_price, decode_price, build_reservation_entry, build_tou_period). Import them from their owning modules:

    # OLD (removed)
    from nwp500 import build_reservation_entry, encode_price
    
    # NEW
    from nwp500.encoding import build_reservation_entry, encode_price
  • Dead code: removed the never-imported nwp500.cli.commands module (its metadata had drifted from the real click commands), the unused converters.str_enum_validator, the unused module-level temperature.half_celsius_to_fahrenheit / deci_celsius_to_fahrenheit wrappers, the no-op NavienMqttClient._on_message_received placeholder, and unused width calculations in the CLI formatters.

Changed

  • Temperature classes deduplicated: HalfCelsius, DeciCelsius, RawCelsius, and DeciCelsiusDelta were four near-identical copies differing only in a scale constant. All conversions are now implemented once on the Temperature base class with a per-class _scale; only special rounding (RawCelsius) and delta semantics (DeciCelsiusDelta) are overridden. Behavior is unchanged (~200 lines removed).
  • field_factory deduplicated: the four field factories shared a copy-pasted metadata-merge block, now extracted into a single private helper. Behavior is unchanged.
  • Device boolean encoding centralized: mqtt/control.py now uses converters.device_bool_from_python() instead of inline 2 if enabled else 1 literals.
  • Version resolution decoupled: auth.py resolves the package version from distribution metadata instead of from . import __version__, removing an order-dependent near-circular import.

Bug Fixes

  • Fix malformed weekly reservation and recirculation schedule payloads: update_weekly_reservation() and configure_recirculation_schedule() dumped the schedule models as-is, double-nesting the request (request.reservation.reservation / request.schedule.schedule) and leaking pydantic computed display fields — including a unit-converted temperature alongside the raw half-Celsius param — into device commands. Both now send the flat, raw protocol shape used by update_reservations(). A new NavienBaseModel.to_protocol_dict() dumps only declared protocol fields.
  • Preserve command order when a queued flush fails: a command that failed mid-flush was re-queued at the tail, behind commands queued after it, inverting order-sensitive sequences (e.g. set_temp replayed before power_on). The queue is now a deque and failed commands are re-inserted at the front.
  • Expire stale queued commands: queued commands stored a timestamp that was never checked, so a multi-hour outage replayed hours-old control commands (e.g. set_power) to the appliance on reconnect. Commands older than MqttConnectionConfig.max_queued_command_age (default 300 s, None to disable) are now discarded at send time.
  • Fix Fahrenheit conversion for sub-zero temperatures (ASYMMETRIC formula): the rounding used Python's floored %, which is always non-negative, while the firmware/app uses a truncated remainder. Raw -11 (-5.5 °C) decoded to 22 °F instead of the app's 23 °F. Now uses math.fmod semantics.
  • Fix freeze protection default limits: the defaults (43/65) were Fahrenheit display values stored in raw half-Celsius fields, decoding to 70.7 °F / 90.5 °F when the device omitted them. Corrected to raw 12/20 (43 °F / 50 °F), matching the documented fixed limits.
  • Emit error_detected when the error code changes: a transition between two non-zero error codes (e.g. E799 → E407) emitted no event, so consumers kept displaying the stale error.
  • Fix TOU price encoding: encode_price() used banker's rounding, under-encoding exact half values at even boundaries (0.125 at decimal_point=2 encoded to 12 instead of 13) — now uses Decimal half-up rounding. build_tou_period() also treated bool prices as pre-encoded integers (True sent as price 1).
  • Fix protocol documentation errors: decode_reservation_hex() documented the enable flag inverted (1=enabled instead of 2=enabled); the build_reservation_entry() example showed week: 158 for Mon/Wed/Fri instead of the correct 84; temperature doctest examples showed int raw values where float is returned.
  • Run MQTT message dispatch on the event loop: JSON parsing, pydantic model validation, and user callbacks all executed directly on the AWS CRT network thread. A slow or blocking callback (e.g. the CLI monitor's CSV writes) stalled all MQTT message processing, user callbacks ran on an undocumented SDK thread where asyncio operations are unsafe, and the handler registry could be mutated on the event loop while the CRT thread iterated it (RuntimeError: dictionary changed size during iteration, dropping the message — most likely during the reconnect/resubscribe window). The awscrt callback now only marshals the raw payload onto the event loop; parsing and dispatch run there, iterating snapshots of the handler registries.
  • Stop one raising handler from aborting message delivery: handler dispatch caught only (TypeError, AttributeError, KeyError); a user callback raising anything else (e.g. ValueError) escaped into the awscrt callback machinery and skipped the remaining handlers for that message. Individual handler failures are now logged and isolated.
  • Make the unit system preference process-wide: set_unit_system() stored the preference in a ContextVar set in the caller's task. Tasks scheduled from AWS CRT callback threads never inherit that context, so the preference silently reverted to auto-detect for all MQTT-delivered data — nwp-cli --unit-system metric monitor logged temperatures in the device's native unit while labeling them °C. The preference is now a process-wide setting visible from every task and thread.
  • Fix once-listeners firing more than once: EventEmitter.emit() removed one-time listeners only after invoking them, so a callback that raised stayed registered forever, and two overlapping emits could both fire the same once-listener. Once-listeners are now removed before invocation.
  • Fix wait_for() leaking its listener on cancellation: EventEmitter.wait_for() removed its listener only on timeout; a cancelled waiter left the listener registered until the event next fired, setting a result on a dead future. Cleanup now happens in a finally block.
  • Fix wait_for() docstring examples: examples showed args, _ = await emitter.wait_for(...), but wait_for returns just the args tuple — following the documented pattern raised ValueError or silently mis-assigned.
  • Serialize concurrent token refresh: ensure_valid_token() and refresh_token() had no lock, so concurrent callers (API 401 retry, MQTT reconnect, periodic requests) at token expiry fired parallel refresh requests; with token rotation the losers were left holding invalidated tokens. Refreshes are now serialized behind an asyncio.Lock with a post-acquire re-check: callers that lose the race receive the already-refreshed tokens, callers passing a stale (pre-rotation) refresh token get the fresh tokens instead of a guaranteed failure, and an explicit refresh with the current token still forces a refresh (deep-reconnect behavior preserved).
  • Preserve refresh_token/id_token across refreshes: the refresh response merge preserved AWS credential fields but not refresh_token/id_token. A refresh response omitting them wiped the stored refresh token, so the next refresh posted an empty string and failed unconditionally.
  • Fix aiohttp session leak when authentication fails in __aenter__: Python never calls __aexit__ when __aenter__ raises, so bad credentials or a network error leaked the owned ClientSession (one per retry attempt). The session is now closed before the exception propagates.
  • Make NavienAuthClient.__aenter__ idempotent: create_navien_clients() pre-enters the context and its docstring instructs users to enter again with async with auth:; the second entry created a fresh session and orphaned the first — which the API client was still pinned to. Re-entering now reuses the existing session.
  • Fall back to full sign-in when stored-token refresh fails: restoring expired stored tokens raised TokenRefreshError even though credentials for a full sign_in() were available.
  • Stop pinning the auth session in the API client: NavienAPIClient captured auth_client.session at construction and kept using it after the auth client recreated its session (RuntimeError: Session is closed). The session is now resolved per request; an explicitly provided session still takes precedence.
  • Add HTTP timeouts to unguarded sessions: the standalone refresh_access_token() helper and OpenEIClient created ClientSession``s without a ``ClientTimeout; requests could hang indefinitely. Both now use a 30-second total timeout.
  • Fix reconnection loop dying on authentication errors: the backoff loop caught only AwsCrtError and RuntimeError, so TokenRefreshError, AuthenticationError, and MqttCredentialsError raised during quick/deep reconnection escaped and silently killed the reconnect task. A routine outage coinciding with token expiry left the client permanently offline despite unlimited retries. All library errors (Nwp500Error) and operation timeouts are now treated as failed attempts and retried; only InvalidCredentialsError is fatal and stops the loop with a reconnection_failed event.
  • Fix disconnect() being a no-op while the connection is interrupted: calling disconnect() during an interruption returned early without disabling automatic reconnection or stopping periodic tasks, so the backoff loop would resurrect the connection after the application shut the client down. disconnect() now always disables reconnection and stops periodic tasks, and tears down the SDK connection even when not connected.
  • Fix queued commands being lost after active/deep reconnection: the command queue was only flushed from the SDK's on_connection_resumed callback, which never fires for the new connection built by active/deep reconnection. Commands queued while offline were silently dropped. Both reconnect paths now flush the queue after subscriptions are restored.
  • Fix periodic request tasks dying on MQTT errors: the periodic loop caught only AwsCrtError and RuntimeError; MqttNotConnectedError/MqttPublishError raised by a publish racing a disconnection permanently killed the polling task while it still appeared active. The loop now survives all library errors.
  • Fix silent failures in thread-scheduled coroutines: futures returned by run_coroutine_threadsafe were discarded, so exceptions from scheduled work (e.g. a failed resubscribe after a clean-session resume, leaving the client connected but deaf) vanished. A done callback now logs them.
  • Fix CancelledError being swallowed in reconnection and periodic loops: both loops caught asyncio.CancelledError and break-ed, so cancelled tasks ended "successfully" (and the reconnection loop could emit reconnection_failed during a manual disconnect). Cancellation now propagates correctly.
  • Fix AttributeError in configure_reservation_water_program: The NavienMqttClient proxy referenced self._control, which is never assigned (the attribute is _device_controller), so every call raised AttributeError. Now delegates correctly; a regression test guards all proxies against references to the undefined attribute.
  • Fix broken CLI mode choices: nwp-cli mode vacation always failed because vacation mode (5) requires a day count that was never supplied, and mode standby sent the invalid writable mode value 0. Both choices were removed from the mode command; use the dedicated vacation DAYS and power off commands instead.
  • Fix CLI exit codes: click ignores command return values in standalone mode, so the CLI always exited 0 even when a command failed. Failures now propagate through ctx.exit() and produce a non-zero exit code for scripts and automation.
  • Fix cached tokens being reused for a different account: passing --email for account B while tokens for account A were cached silently ran commands against account A's session. Cached tokens are now discarded when the provided email does not match the cached one.
  • Surface OpenEI application errors: OpenEI reports errors such as an invalid API key in the body of an HTTP 200 response; these were masked as "no rate plans found". fetch_rates() now raises APIError with the API's error message.
  • Fix broken example import: examples/advanced/mqtt_diagnostics.py imported MqttConnectionConfig from the nonexistent nwp500.mqtt_utils module and used the deprecated datetime.utcnow().

Improvements

  • MQTT operation acknowledgement timeouts: connect, publish, subscribe, unsubscribe, and disconnect acknowledgements are now awaited with a timeout (MqttConnectionConfig.operation_timeout, default 30 seconds). Previously a half-open TCP connection could hang callers until the 20-minute keep-alive expired.
  • Reconnection backoff jitter: reconnect delays are now randomized (±50%, capped at max_reconnect_delay) so fleets of clients disconnected simultaneously (e.g. the AWS IoT 24-hour disconnect) no longer reconnect in synchronized waves.

Security

  • Restrict token cache file permissions: ~/.nwp500_tokens.json (refresh token and AWS credentials) was written world-readable. It is now created with mode 0600, and existing files are tightened on save.

Version 8.1.3 (2026-06-15)

Bug Fixes

  • Fix MQTT reconnection storm caused by non-thread-safe Task.cancel(): The on_connection_resumed callback is invoked from an AWS IoT SDK background thread. It was calling asyncio.Task.cancel() directly on that thread, which is not thread-safe. When the event loop was busy at the moment of cancellation (e.g. the sleeping task's timer callback had already been enqueued), the cancellation was silently dropped. The stale _reconnect_with_backoff task would then complete its sleep, call _reconnect_func, and tear down an otherwise healthy connection — restarting the entire disconnect → reconnect → AWS_ERROR_MQTT_UNEXPECTED_HANGUP cycle. Fixed by replacing the direct task.cancel() call with a _cancel_pending_reconnect() coroutine scheduled via _schedule_coroutine, so the cancellation runs on the event loop where asyncio operations are safe. The method also uses an identity check before clearing _reconnect_task to avoid wiping a newer task created during the await, and clears stale references to already-done tasks.

Version 8.1.0 (2026-05-16)

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.

Version 8.0.0 (2026-05-13)

Bug Fixes

  • Fix MQTT reconnection after unexpected AWS hangup: The on_connection_resumed callback was missing the connection parameter required by the AWS IoT SDK callback signature. The SDK calls on_connection_resumed(connection, return_code, session_present, **kwargs), but the handler only accepted (return_code, session_present). The mismatched signature caused the callback to fail silently (exception swallowed by the AWS CRT C layer), so self._connected was never restored to True after an AWS_ERROR_MQTT_UNEXPECTED_HANGUP. As a result, the connection_resumed event was never emitted, the reconnection loop ran indefinitely, and device sensors became permanently unavailable until a manual restart. (#85)

Features

  • Multi-device support enhancements: Improved support for accounts with multiple Navilink devices by injecting device identity into models and events.
    • Added mac_address field to DeviceStatus and DeviceFeature models.
    • Added device_mac attribute to all device-specific MQTT events (temperature changes, mode changes, power updates, errors, etc.).
    • Updated DeviceStateTracker and MqttSubscriptionManager to propagate device identity correctly.

BREAKING CHANGES: .on() event handler callbacks now receive a single typed event dataclass instead of positional arguments. MqttDeviceController is no longer accessible as .control on NavienMqttClient; all control methods are now available directly on the client.

Breaking Changes

  • Typed event payloads: All .on() event handler callbacks now receive a single typed event dataclass instance. The dataclasses are exported from nwp500.mqtt_events.

    # OLD (removed)
    mqtt.on("temperature_changed", lambda old, new: print(old, new))
    mqtt.on("connection_resumed", lambda rc, sp: print(rc, sp))
    
    # NEW
    mqtt.on("temperature_changed", lambda e: print(e.old_temperature, e.new_temperature))
    mqtt.on("connection_resumed", lambda e: print(e.return_code, e.session_present))
  • ``MqttDeviceController`` no longer public: NavienMqttClient no longer exposes a .control attribute. All control methods are now available directly on the client.

    # OLD (removed)
    await mqtt.control.set_temperature(device, 50)
    
    # NEW
    await mqtt.set_temperature(device, 50)

Migration Guide (from 7.x.x)

The following steps are recommended for a smooth migration, particularly for complex integrations like Home Assistant:

  1. Update Event Listeners: Locate all mqtt.on() or mqtt.once() calls. Update the callback signatures to accept a single argument (the event object) and update the body to access fields via the object (e.g., event.new_temperature instead of new_val).
  2. Refactor Control Calls: Remove .control from all device command invocations. Instead of await mqtt.control.set_power(...), use await mqtt.set_power(...).
  3. Handle Unit Conversions: If your integration previously performed its own conversions or relied on the library's eager conversion, note that DeviceStatus fields like dhw_temperature are now properties. They return values based on the global unit system context (us_customary by default).
    • Home Assistant Tip: To ensure your state tracking is immune to unit system toggles within the library, use the new *_raw fields (e.g., status.dhw_temperature_raw) for comparison logic, and use the properties only for display or when a converted value is explicitly needed.
  4. Remove ``from_dict()`` Calls: The from_dict() method has been removed from all models. Use model_validate() instead.
    • Note: AuthenticationResponse.model_validate() now automatically handles the "data": { ... } wrapper found in raw API responses.
  5. Subpackage Imports: While top-level imports from nwp500.models are preserved, if you were importing from the internal nwp500.models module file directly, you must update your imports to point to the new structured files (e.g., nwp500.models.status).

Added

  • New control methods: Nine previously unimplemented CommandCode values now have full implementations: check_firmware, commit_firmware, reconnect_wifi, reset_wifi, set_freeze_protection_temperature, run_smart_diagnostic, enable_intelligent_reservation, disable_intelligent_reservation, and set_water_program_reservation.
  • Typed subscription methods: subscribe_reservation, subscribe_weekly_reservation, and subscribe_recirculation return typed responses directly without requiring raw MQTT event handlers.
  • New protocol models: WeeklyReservationSchedule, WeeklyReservationEntry, RecirculationSchedule, RecirculationScheduleEntry, and OtaCommitPayload.
  • ``DeviceStateTracker``: State change detection extracted into a dedicated class in nwp500.mqtt.state_tracker with per-device tracking keyed by MAC address.
  • ``MQTT_PROTOCOL_VERSION`` constant: Protocol version is now a named constant in nwp500.config rather than a hardcoded integer in the command payload builder.
  • ``response_ack_topic()``: New method on MqttTopicBuilder for control command acknowledgement topics.

Changed

  • Unit conversion redesign: Temperature, flow rate, and volume fields in DeviceStatus and DeviceFeature now store raw device values as *_raw: int fields and expose converted values via lazy computed properties. Conversion happens at access time rather than during Pydantic deserialization, preserving the original device value in all cases.
  • Models split into subpackage: nwp500.models is now a package (nwp500/models/) with modules for status, schedule, TOU, and MQTT models. Public imports from nwp500.models are unchanged.
  • Topic building centralised: All MQTT topic construction now goes through MqttTopicBuilder. Hardcoded topic strings removed from control.py, reservations.py, and cli/handlers.py.
  • ``set_vacation_days`` delegates to ``set_dhw_mode``: The method now calls set_dhw_mode(device, DhwOperationSetting.VACATION, vacation_days=days) directly.
  • Per-device state tracking: _previous_status changed from a single DeviceStatus | None to a dict[str, DeviceStatus] keyed by MAC address, preventing spurious state-change events when multiple devices are connected.
  • Unit system stored as instance variable: NavienAPIClient, NavienMqttClient, and NavienAuthClient no longer call set_unit_system() as a constructor side-effect.
  • ``NavienBaseModel`` consolidated: Duplicate definitions in auth.py and models.py merged into a single definition in nwp500._base.

Version 7.4.10 (2026-04-13)

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.

Version 7.4.9 (2026-04-12)

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.

Version 7.4.8 (2026-02-17)

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.

Version 7.4.7 (2026-02-17)

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.

Version 7.4.6 (2026-02-13)

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

Version 7.4.5 (2026-02-04)

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

Version 7.3.4 (2026-01-27)

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

Version 7.3.3 (2026-01-27)

Breaking Changes

  • Temperature Setpoint Limits: Replaced hardcoded temperature limits with device-provided values

    • set_dhw_temperature() now validates against device-specific dhw_temperature_min and dhw_temperature_max instead of hardcoded 95-150°F bounds
    • build_reservation_entry() changed parameter name from temperature_f to temperature (unit-agnostic)
    • Added optional temperature_min and temperature_max parameters to build_reservation_entry() for device-specific limit overrides
    • Temperature parameters now accept values in the user's preferred unit (Celsius or Fahrenheit) based on global unit system context
    • Fixes Home Assistant and other integrations that prefer Celsius unit display

    Migration guide:

    # OLD (hardcoded 95-150°F)
    await mqtt.set_dhw_temperature(device, temperature_f=140.0)
    entry = build_reservation_entry(
        enabled=True,
        days=["Monday"],
        hour=6,
        minute=0,
        mode_id=3,
        temperature_f=140.0,
    )
    
    # NEW (device-provided limits, unit-aware)
    # Temperature value automatically uses user's preferred unit
    await mqtt.set_dhw_temperature(device, 140.0)
    
    # Device features provide min/max in user's preferred unit
    features = await device_info_cache.get(device.device_info.mac_address)
    entry = build_reservation_entry(
        enabled=True,
        days=["Monday"],
        hour=6,
        minute=0,
        mode_id=3,
        temperature=140.0,
        temperature_min=features.dhw_temperature_min,
        temperature_max=features.dhw_temperature_max,
    )

Added

  • Reservation Temperature Conversion: New reservation_param_to_preferred() utility function for unit-aware reservation display

    • Converts device reservation parameters (half-degree Celsius) to user's preferred unit
    • Respects global unit system context (metric/us_customary)
    • Enables proper thermostat/reservation scheduling display in Home Assistant and other integrations
    • Example usage:
    from nwp500 import reservation_param_to_preferred
    
    # Display reservation temperature in user's preferred unit
    param = 120  # Device raw value in half-Celsius
    temp = reservation_param_to_preferred(param)
    # Returns: 60.0 (Celsius) or 140.0 (Fahrenheit) based on unit context
  • Unit-Aware Temperature Conversion: New preferred_to_half_celsius() utility function

    • Converts temperature from user's preferred unit to half-degree Celsius for device commands
    • Respects global unit system context (metric/us_customary)
    • Replaces misleading fahrenheit_to_half_celsius() in unit-agnostic code paths
    • Used internally by set_dhw_temperature() and build_reservation_entry()

Changed

  • Unit System Agnostic Display: All logging and user-facing messages now respect global unit system context
    • Temperature change logs dynamically show °C or °F based on user preference
    • CLI monitoring and temperature setting messages use correct unit suffix
    • Event listener documentation updated with unit-aware examples
    • Reservation schedule examples now use reservation_param_to_preferred() for proper unit handling

Fixed

  • Critical: Temperature Unit Bug in Set Operations: Fixed incorrect temperature conversion when setting DHW temperature and reservations
    • set_dhw_temperature() was calling fahrenheit_to_half_celsius() with unit-agnostic temperature parameter
    • build_reservation_entry() had the same issue
    • Impact: If user preferred Celsius, temperature would be interpreted as Fahrenheit, causing wrong setpoints
    • Fix: Use new preferred_to_half_celsius() that respects unit system context
    • This was a critical data correctness bug that would cause incorrect device behavior for Celsius users

Version 7.3.2 (2026-01-25)

Changed

  • Unit System Consistency: Removed deprecated "imperial" unit system identifier in favor of "us_customary" for Home Assistant compatibility
    • Changed all references from UnitSystem.IMPERIAL to UnitSystem.US_CUSTOMARY
    • Updated set_unit_system() and related functions to use "us_customary" exclusively
    • "metric" remains unchanged for Metric conversions
    • Affects API client, MQTT client, and CLI unit system handling
    • Improves consistency with Home Assistant naming conventions

Fixed

  • Unit System Consolidation: Consolidated duplicate UnitSystemType type alias definitions into single canonical definition in unit_system.py
    • Removed redundant type definitions from multiple modules
    • Improved code maintainability and consistency
    • All unit system operations now reference centralized type definition

Version 7.3.1 (2026-01-25)

Fixed

  • MQTT Client Initialization: Removed overly strict token validity check from NavienMqttClient.__init__()
    • The strict token validity check prevented creating MQTT clients with restored tokens that may have expired between application restarts
    • However, NavienMqttClient.connect() already handles token refresh automatically, making the check redundant
    • This change allows integrations to create MQTT clients with expired tokens and let connect() handle validation
    • Simplifies application code by removing duplicate token refresh calls
    • Enables proper handling of restored authentication sessions
    • Fixes MQTT connection failures when using stored tokens across application restarts

Version 7.3.0 (2026-01-19)

Added

  • Dynamic Unit Conversion: All temperature, flow, and volume measurements now dynamically convert based on user's region preference (Metric/Imperial)

    • Temperature fields convert between Celsius and Fahrenheit using standard formulas

    • Flow rate fields convert between LPM (Liters Per Minute) and GPM (Gallons Per Minute)

    • Volume fields convert between Liters and Gallons

    • Available devices and regions determine conversion support (validated at runtime)

    • get_field_unit() method retrieves the correct unit suffix for any field

    • All conversions use Pydantic WrapValidator for transparent, automatic conversion

    • Comprehensive documentation in docs/guides/unit_conversion.rst

    • Example usage:

      # Get converted value with unit
      temp_f = device_status.flow_rate_target  # Returns Fahrenheit if region prefers imperial
      unit = device_status.get_field_unit("flow_rate_target")  # Returns "°F" or "°C"
      
      # All conversions are transparent - values automatically convert to preferred units
      flow_gpm = device_status.flow_rate_current  # GPM if imperial, LPM if metric
      volume_gal = device_status.tank_volume  # Gallons if imperial, Liters if metric
    • Supported conversion fields:

      • Temperature: flow_rate_target, flow_rate_current, in_water_temp, out_water_temp, set_temp, in_temp, out_temp, etc.
      • Flow Rate: flow_rate_target, flow_rate_current
      • Volume: tank_volume and related storage fields
    • Integration patterns for Home Assistant, CLI, and custom integrations documented

  • Unit System Override: Allow applications and CLI users to override the device's temperature preference and explicitly specify Metric or Imperial units

    • Library-level: Add optional unit_system parameter to NavienAuthClient, NavienMqttClient, and NavienAPIClient initialization

    • Set once at initialization; applies to all subsequent data conversions

    • Accepts: "metric" (Celsius/LPM/Liters), "imperial" (Fahrenheit/GPM/Gallons), or None (auto-detect from device)

    • Decouples unit preference from device configuration - users can override what the device is set to

    • Uses context variables for thread-safe and async-safe unit system management

    • Example usage:

      # Library initialization
      from nwp500 import NavienAuthClient, set_unit_system
      auth = NavienAuthClient(email, password, unit_system="metric")
      
      # Or set after initialization
      set_unit_system("imperial")
      device_status = await mqtt.request_device_status(device)
      # Values now in F, GPM, gallons regardless of device setting
    • CLI-level: Add --unit-system flag for per-command override

    • Example: nwp-cli status --unit-system metric

    • Defaults to device's setting if not specified

    • New exported functions: - set_unit_system(unit_system) - Set the preferred unit system - get_unit_system() - Get the current unit system preference - reset_unit_system() - Reset to auto-detect mode

Fixed

  • MQTT Unit System Override Bug: Fixed unit_system CLI flag not applying to device status values
    • Issue: Running nwp-cli --unit-system metric status displayed values in device's native format (imperial) with metric unit strings (e.g., "104.9 °C" instead of "40.5 °C")
    • Root Cause: MQTT callbacks from AWS CRT execute on different threads where context variables are not set. Since Python's context variables are task-local, the unit_system preference was not visible to validators
    • Solution: Store unit_system in NavienMqttClient and MqttSubscriptionManager. Before parsing MQTT messages, explicitly set the context variable in message handlers to ensure validators use the correct unit system regardless of thread context
    • Result: Values and units now correctly convert when --unit-system override is specified
    • Testing: All 393 tests pass including new unit system context override tests
  • Type Annotation Quotes: Removed unnecessary quoted type annotations (UP037 violations)
    • With from __future__ import annotations, explicit string quotes are redundant
    • Updated from_dict() methods in UserInfo, AuthTokens, AuthenticationResponse, DeviceStatus, DeviceFeature, and EnergyUsageResponse
    • Improves code clarity and passes modern linting standards

Version 7.2.3 (2026-01-15)

Added

  • Daily Energy Breakdown by Month: New --month option for energy command to show daily energy data for a specific month

    # Daily breakdown for a single month
    nwp-cli energy --year 2025 --month 12
    
    # Monthly summary for multiple months (existing)
    nwp-cli energy --year 2025 --months 10,11,12
    • Displays daily energy consumption, efficiency, and heat source breakdown
    • Rich formatted output with progress bars and color-coded efficiency percentages
    • Plain text fallback for non-Rich environments
    • Smart routing: single month shows daily data, multiple months show summary

Fixed

  • Documentation: Fixed all warnings and broken cross-references in documentation
    • Fixed docstring formatting in field_factory.py module
    • Fixed broken cross-reference links in enumerations.rst, mqtt_diagnostics.rst, cli.rst, and models.rst
    • Fixed invalid JSON syntax in code examples (removed invalid [...] and ... tokens)
    • Suppressed duplicate object description warnings from re-exported classes
  • CLI Documentation: Updated documentation for all 19 CLI commands
    • Added missing device-info command documentation
    • Added --raw flag documentation for status, info, and device-info commands
    • Added --month option documentation to energy command
    • Clarified mutually exclusive options (--months vs --month)
  • RST Title Hierarchy: Fixed title level inconsistencies in device_control.rst
  • Read the Docs Configuration: Updated Python version requirement to 3.13 in Read the Docs config
  • CI Test Failures: Fixed ModuleNotFoundError when running tests without CLI dependencies installed
    • Wrapped CLI module imports in try-except blocks in test modules
    • Tests are skipped gracefully when optional dependencies (click, rich) are not installed
    • Allows pytest to run without CLI extra, while supporting full test suite with tox
    • Network errors in authentication are now marked as retriable for better resilience

Changed

  • Installation Documentation: Updated installation instructions to clarify optional CLI and Rich dependencies

Version 7.2.2 (2025-12-25)

Fixed

  • TOU Status Always Showing False: Fixed touStatus field always reporting False regardless of actual device state
    • Root cause: Version 7.2.1 incorrectly changed touStatus to use device-specific 1/2 encoding, but the device uses standard 0/1 encoding
    • Solution: Use Python's built-in bool() for touStatus field (handles 0=False, 1=True naturally)
    • Updated documentation in docs/protocol/quick_reference.rst to note touStatus exception
    • Added tests verifying built-in bool() handles 0/1 encoding correctly
    • Device encoding: 0=OFF/disabled, 1=ON/enabled (standard Python truthiness)

Version 7.2.1 (2025-12-25)

Added

  • CLI Command: New device-info command to retrieve basic device information from REST API

    # Get basic device info (DeviceInfo model)
    python3 -m nwp500.cli device-info
    python3 -m nwp500.cli device-info --raw
  • ConnectionStatus Enum: New ConnectionStatus enum for device cloud connection state

    • ConnectionStatus.DISCONNECTED = 1 - Device offline/not connected
    • ConnectionStatus.CONNECTED = 2 - Device online and reachable
    • Used in DeviceInfo.connected field with automatic validation
  • InstallType Enum: New InstallType enum for device installation classification

    • InstallType.RESIDENTIAL = "R" - Residential use
    • InstallType.COMMERCIAL = "C" - Commercial use
    • Used in DeviceInfo.install_type field with automatic validation
    • Includes INSTALL_TYPE_TEXT mapping for display purposes
  • String Enum Validator: New str_enum_validator() converter for string-based enums

Changed

  • DeviceInfo Model: - connected field now uses ConnectionStatus enum instead of plain int - install_type field now uses InstallType enum instead of plain string
  • TOU Status Conversion: Simplified TOU status to use standard device_bool_to_python converter (consistent with other OnOffFlag fields) - Removed special-case tou_status_to_python() converter - TouStatus annotated type now uses device_bool_to_python validator - Device encoding: 1=OFF/disabled, 2=ON/enabled (consistent with all other boolean fields)
  • CLI Documentation: Clarified distinction between info (DeviceFeature via MQTT) and device-info (DeviceInfo via REST API) commands
  • Type Annotations: Fixed CLI rich_output console type annotation to declare at class level

Removed

  • constants.py Module: Removed empty constants.py module. CommandCode enum was already moved to enums.py in version 4.2.0.

    # OLD (removed)
    from nwp500.constants import CommandCode
    
    # NEW (use this)
    from nwp500.enums import CommandCode
  • Firmware Tracking: Removed unused firmware tracking constants and documentation (KNOWN_FIRMWARE_FIELD_CHANGES, LATEST_KNOWN_FIRMWARE, docs/protocol/firmware_tracking.rst)

  • TOU Status Converter: Removed redundant tou_status_to_python() converter function and associated tests

Version 7.2.0 (2025-12-23)

BREAKING CHANGES: Class names renamed for consistency with MQTT-specific functionality

Removed

  • Renamed Classes: Updated class names to clarify MQTT-specific implementations

    # OLD (removed)
    from nwp500 import DeviceCapabilityChecker, DeviceInfoCache
    
    # NEW
    from nwp500 import MqttDeviceCapabilityChecker, MqttDeviceInfoCache

    Rationale: The original names were too generic. These classes are specifically designed for MQTT client functionality (auto-fetching device info, caching, capability checking). The new names make it clear they're MQTT-specific implementations, leaving room for future REST API versions if needed.

    Migration: Simple find-and-replace:

    • DeviceCapabilityCheckerMqttDeviceCapabilityChecker
    • DeviceInfoCacheMqttDeviceInfoCache

    All functionality remains identical - only the class names changed.

Added

  • Factory Function: New create_navien_clients() factory for streamlined client initialization

    # Create both API and MQTT clients in one call
    from nwp500 import create_navien_clients
    
    async with create_navien_clients(email, password) as (api_client, mqtt_client):
        devices = await api_client.get_devices()
        await mqtt_client.connect()
        # Both clients ready to use
    • Automatic auth client management (created internally, shared by both clients)
    • Simplified initialization for common use case (API + MQTT)
    • Proper async context manager support
    • Reduces boilerplate in application code
    • Comprehensive documentation in docs/guides/authentication.rst
    • Example: examples/intermediate/advanced_auth_patterns.py
  • VolumeCode Enum: Tank capacity identification with gallon values

    from nwp500 import VolumeCode
    
    # Enum values: VOLUME_50GAL = 65, VOLUME_65GAL = 66, VOLUME_80GAL = 67
    # Human-readable text available in VOLUME_CODE_TEXT dict
    • Maps device codes to actual tank capacities (50, 65, 80 gallons)
    • Used in DeviceFeature.volume_code field with automatic validation
    • Exported from main package for convenience
    • Includes VOLUME_CODE_TEXT mapping for display purposes
  • Temperature Conversion Classes: Type-safe temperature handling with clear precision

    • HalfCelsius class: 0.5°C precision (value / 2.0)
    • DeciCelsius class: 0.1°C precision (value / 10.0)
    • Base Temperature ABC with to_celsius() and to_fahrenheit() methods
    • from_fahrenheit() class methods for reverse conversions
    • Validator functions for Pydantic integration
    • Centralized in new temperature.py module
    • Better type safety and clearer intent than raw number conversions
  • Protocol Converters Module: Centralized device protocol conversion logic

    • device_bool_to_python(): Convert device boolean (1=False, 2=True)
    • device_bool_from_python(): Reverse conversion
    • tou_status_to_python(): Time of Use status conversion
    • tou_override_to_python(): TOU override status conversion
    • div_10(): Divide by 10.0 utility
    • enum_validator(): Generic enum factory
    • Comprehensive documentation explaining device protocol quirks
    • New converters.py module replacing scattered validators
  • MQTT Event System: Structured event handling for MQTT operations

    • MqttClientEvents class with type-safe event definitions
    • Feature monitoring and capability detection events
    • Enhanced device capability monitoring in MQTT control module
    • New mqtt_events.py module for event infrastructure
    • Improved separation of concerns for event-driven architectures
  • Pyright Type Checking: Static type analysis integrated into CI/CD

    • Added pyright>=1.1.0 to dev dependencies
    • Configured in pyproject.toml with strict mode for src/nwp500
    • Integrated into tox lint environment and CI workflows
    • Runs automatically with make ci-lint or python3 scripts/lint.py
    • All source code now passes strict type checking (0 errors)
    • Improved type annotations across codebase
  • Dynamic Unit Extraction in CLI: CLI output now dynamically extracts units from DeviceStatus model metadata

    • New helper functions: _get_unit_suffix() and _add_numeric_item()
    • Eliminates hardcoded units in output formatter
    • Single source of truth: model metadata drives CLI display
  • Comprehensive Protocol Documentation: Complete protocol reference documentation

    • New docs/protocol/quick_reference.rst with command codes, field formats, and conversions
    • Converted protocol documentation to RST format for Sphinx integration
    • Added protocol reference links in source code comments
    • Improved cross-referencing between code and documentation

Changed

  • MQTT Module Reorganization: Consolidated 9 separate modules into cohesive mqtt package

    # OLD imports (still work via compatibility layer)
    from nwp500.mqtt_client import NavienMqttClient
    from nwp500.mqtt_diagnostics import MqttDiagnosticsCollector
    from nwp500.mqtt_utils import MqttConnectionConfig
    
    # NEW imports (preferred)
    from nwp500.mqtt import NavienMqttClient, MqttDiagnosticsCollector, MqttConnectionConfig
    # OR import from main package (recommended)
    from nwp500 import NavienMqttClient, MqttDiagnosticsCollector, MqttConnectionConfig
    • Created src/nwp500/mqtt/ package with organized submodules
    • Better package organization and structure
    • Clearer public vs internal APIs
    • New mqtt/__init__.py with clean public API exports
    • Backward compatibility maintained via main package exports
    • All 209 tests pass with zero type checking errors
  • CLI Framework Migration: Migrated from argparse to Click framework

    • Implemented async_command decorator for automatic loop and connection management
    • Added support for command groups (reservations, tou)
    • Improved argument and option parsing with built-in validation
    • Enhanced help text and version reporting
    • Centralized command registry in src/nwp500/cli/commands.py
    • Reorganized CLI handlers into src/nwp500/cli/handlers.py
    • Better separation of concerns between CLI framework and business logic
    • Industry-standard CLI framework with better maintainability
    • Added click>=8.0.0 dependency
  • Examples Reorganization: Restructured examples into beginner/intermediate/advanced/testing categories

    • Created structured hierarchy in examples/ directory
    • Renamed and moved 35+ example scripts for better discoverability
    • Updated examples/README.md with 'Getting Started' guide and categorized index
    • Added 01-04 beginner series for smooth onboarding:
      • beginner/01_authentication.py - Basic authentication patterns
      • beginner/02_list_devices.py - Retrieving device information
      • beginner/03_get_status.py - Getting device status
      • beginner/04_set_temperature.py - Basic device control
    • Intermediate examples: event-driven control, error handling, MQTT monitoring
    • Advanced examples: demand response, recirculation, TOU schedules, diagnostics
    • Testing examples: connection testing, periodic updates, minimal examples
    • All examples updated with correct imports for new package structure
  • Authentication Documentation: Major improvements to authentication guide

    • Complete rewrite of docs/guides/authentication.rst
    • Added factory function patterns and examples
    • Improved context manager documentation
    • Added best practices and common patterns
    • More comprehensive code examples
  • Model Refactoring: Updated to use new converter modules

    • Replaced 53 lines of scattered validators with imports
    • Updated fahrenheit_to_half_celsius() to use HalfCelsius class
    • Cleaner model definitions with centralized conversion logic
    • No breaking changes to public API
  • CLI Output Formatter Refactoring: Restructured print_device_status() to use dynamic unit extraction

    • Reduced code duplication by ~400 lines
    • Improved maintainability: field additions automatically get correct units
    • No breaking changes to CLI output format or behavior
  • Type Annotations: Improved type safety across entire codebase

    • Fixed datetime imports to use datetime.UTC (Python 3.13)
    • Fixed type annotations in rich_output.py for optional dependencies
    • Fixed type narrowing issues in encoding.py
    • Updated MqttConnection callback signature to use AwsCrtError
    • Added public properties and setters where needed for type checking

Fixed

  • Superheat Temperature Units: Target and Current SuperHeat now correctly display in °F instead of °C
    • Both fields use DeciCelsiusToF conversion, now properly reflected in CLI output
    • Fields were displaying inconsistent units compared to all other temperature readings
  • Missing CLI Output Units: Multiple fields now display with proper units from model metadata
    • current_dhw_flow_rate: Now shows GPM unit
    • total_energy_capacity: Now shows Wh unit
    • available_energy_capacity: Now shows Wh unit
    • dr_override_status: Now shows hours unit
    • vacation_day_setting: Now shows days unit
    • vacation_day_elapsed: Now shows days unit
    • anti_legionella_period: Fixed to show days unit (was incorrectly h)
    • wifi_rssi: Now shows dBm unit
  • Invalid MQTT Topic Filter: Fixed reservations get command subscription topic
    • Changed invalid topic pattern cmd/52/navilink-+/# to valid cmd/52/+/#
    • AWS IoT Core MQTT does not support wildcards within topic segments
    • Affected: handle_get_reservations_request() in commands.py
  • DeviceFeature Documentation: Clarified field descriptions and fixed documentation errors
    • Fixed country_code documentation (actual value is 3, not 1 as previously noted)
    • Clarified model_type_code, control_type_code, recirc_model_type_code field purposes
    • Updated volume_code to use new VolumeCode enum with validation
  • Type Checking Errors: Resolved all pyright type checking errors in source code
    • Fixed datetime imports and type annotations
    • Added missing public properties and setters
    • Removed unused imports and variables
    • All source code now passes strict type checking

Version 7.1.0 (2025-12-22)

Added

  • Device Capability System: New device capability detection and validation framework: - DeviceCapabilityChecker: Validates device feature support based on device models - DeviceInfoCache: Efficient caching of device information with configurable update intervals - @requires_capability decorator: Automatic capability validation for MQTT commands - DeviceCapabilityError: New exception for unsupported device features
  • Advanced Control Commands: New MQTT commands for advanced device features: - enable_demand_response() / disable_demand_response(): Demand response participation control - reset_air_filter(): Air filter maintenance tracking reset - set_vacation_days(): Configure vacation mode duration - configure_reservation_water_program(): Water program reservation management - set_recirculation_mode() / configure_recirculation_schedule() / trigger_recirculation_hot_button(): Recirculation pump control and scheduling
  • CLI Documentation Updates: Comprehensive documentation updates for subcommand-based CLI - Complete rewrite of docs/python_api/cli.rst with full command reference - Updated README.rst with new subcommand syntax and examples - Added 8+ practical usage examples (cron jobs, automation, monitoring) - Added troubleshooting guide and best practices section
  • Model Field Factory Pattern: New field factory to reduce boilerplate in model definitions - Automatic field conversion and validation - Cleaner model architecture

Changed

  • CLI Output: Numeric values in status output now rounded to one decimal place for better readability
  • MqttDeviceController now integrates device capability checking with auto-caching of device info
  • Exception type hints improved with proper None handling in optional parameters
  • MQTT Control Refactoring: Centralized device control via .control namespace - Standardized periodic request patterns - Public API method ensure_device_info_cached() for better cache management
  • Logging Security: Enhanced sensitive data redaction - MAC addresses consistently redacted across all logging output - Token logging removed from docstrings and examples - Intermediate variables used for redacted data

Fixed

  • Type annotation consistency: Optional parameters now properly annotated as type | None instead of type
  • Type System Fixes: Resolved multiple type annotation issues for CI compatibility
  • Mixing Valve Field: Corrected alias field name and removed unused TOU status validator
  • Vacation Days Validation: Enforced maximum value validation for vacation mode days
  • CI Linting: Fixed line length violations and import sorting issues
  • Security Scanning: Resolved intermediate variable issues in redacted MAC address handling
  • Parser Regressions: Fixed data parsing issues introduced in MQTT refactoring

Version 7.0.1 (2025-12-18)

Fixed

  • Minor bug fixes and improvements

Version 7.0.0 (2025-12-17)

BREAKING CHANGES: - Minimum Python version raised to 3.13 - Enumerations refactored for type safety and consistency

Removed

  • Python 3.9-3.12 Support: Minimum Python version is now 3.13

    Home Assistant has deprecated Python 3.12 support, making Python 3.13 the de facto minimum for this ecosystem.

    Python 3.13 features and improvements:

    • Experimental free-threaded mode (PEP 703): Optional GIL removal for true parallelism
    • JIT compiler (PEP 744): Just-in-time compilation for performance improvements
    • Better error messages: Enhanced suggestions for NameError, AttributeError, and import errors
    • Type system enhancements: TypeVars with defaults (PEP 696), @deprecated decorator (PEP 702), ReadOnly TypedDict (PEP 705)
    • Performance: ~5-10% faster overall, optimized dictionary/set operations, better function calls
    • PEP 695: New type parameter syntax for generics
    • PEP 701: f-string improvements
    • Built-in datetime.UTC constant

    If you need Python 3.12 support, use version 6.1.x of this library.

  • CommandCode moved: Import from nwp500.enums instead of nwp500.constants

    # OLD (removed)
    from nwp500.constants import CommandCode
    
    # NEW
    from nwp500.enums import CommandCode
    # OR
    from nwp500 import CommandCode  # Still works

Added

  • Python 3.12+ Optimizations: Leverage latest Python features
    • PEP 695: New type parameter syntax (def func[T](...) instead of TypeVar)
    • Use datetime.UTC constant instead of datetime.timezone.utc
    • Native union syntax (X | Y instead of Union[X, Y])
    • Cleaner generic type annotations throughout codebase
  • Enumerations Module (``src/nwp500/enums.py``): Comprehensive type-safe enums for device control and status
    • Status value enums: OnOffFlag, Operation, DhwOperationSetting, CurrentOperationMode, HeatSource, DREvent, WaterLevel, FilterChange, RecirculationMode
    • Time of Use enums: TouWeekType, TouRateType
    • Device capability enums: CapabilityFlag, TemperatureType, DeviceType
    • Device control command enum: CommandCode (all MQTT command codes)
    • Error code enum: ErrorCode with complete error code mappings
    • Human-readable text mappings for all enums (e.g., DHW_OPERATION_SETTING_TEXT, ERROR_CODE_TEXT)
    • Exported from main package: from nwp500 import OnOffFlag, ErrorCode, CommandCode
    • Comprehensive documentation in docs/enumerations.rst
    • Example usage in examples/error_code_demo.py

Changed

  • Command Code Constants: Migrated from constants.py to CommandCode enum in enums.py
    • ANTI_LEGIONELLA_ENABLECommandCode.ANTI_LEGIONELLA_ON
    • ANTI_LEGIONELLA_DISABLECommandCode.ANTI_LEGIONELLA_OFF
    • TOU_ENABLECommandCode.TOU_ON
    • TOU_DISABLECommandCode.TOU_OFF
    • TOU_SETTINGSCommandCode.TOU_RESERVATION
    • All command constants now use consistent naming in CommandCode enum
  • Model Enumerations: Updated type annotations for clarity and type safety
    • TemperatureUnitTemperatureType (matches device protocol field names)
    • All capability flags (e.g., power_use, dhw_use) now use CapabilityFlag type
    • MqttRequest.device_type now accepts Union[DeviceType, int] for flexibility
  • Model Serialization: Enums automatically serialize to human-readable names
    • model_dump() converts enums to names (e.g., DhwOperationSetting.HEAT_PUMP → "HEAT_PUMP")
    • CLI and other consumers benefit from automatic enum name serialization
    • Text mappings available for custom formatting (e.g., DHW_OPERATION_TEXT[enum] → "Heat Pump Only")
  • Documentation: Comprehensive updates across protocol and API documentation
    • docs/guides/time_of_use.rst: Clarified TOU override status behavior (1=OFF/override active, 2=ON/normal operation)
    • docs/protocol/data_conversions.rst: Updated TOU field descriptions with correct enum values
    • docs/protocol/device_features.rst: Added capability flag pattern explanation (2=supported, 1=not supported)
    • docs/protocol/mqtt_protocol.rst: Updated command code references to use new enum names
    • docs/python_api/models.rst: Updated model field type annotations
  • Examples: Updated to use new enums for type-safe device control
    • examples/anti_legionella_example.py: Uses CommandCode enum
    • examples/device_feature_callback.py: Uses capability enums
    • examples/event_emitter_demo.py: Uses status enums
    • examples/mqtt_diagnostics_example.py: Uses command enums
  • CLI Code Cleanup: Refactored JSON formatting to use shared utility function
    • Extracted repeated json.dumps() calls to format_json_output() helper
    • Cleaner code with consistent formatting across all commands

Fixed

  • Temperature Conversion Test: Corrected test_device_status_div10 to use HalfCelsiusToF conversion (100 → 122°F, not 50.0)
  • Documentation: Fixed references to non-existent OperationMode enum - replaced with correct DhwOperationSetting and CurrentOperationMode enums

Version 6.1.1 (2025-12-08)

Added

  • MQTT Diagnostics Module: New MqttDiagnosticsCollector for capturing MQTT message traffic for debugging
    • Captures all MQTT publish/subscribe activity with timestamps and payloads
    • Configurable message filtering by topic pattern
    • Message deduplication to reduce storage
    • Automatic cleanup of old diagnostics (configurable retention)
    • Export diagnostics to JSON for analysis and debugging
    • Home Assistant integration support for custom components
    • examples/mqtt_diagnostics_example.py demonstrating usage patterns
    • Comprehensive documentation in docs/MQTT_DIAGNOSTICS.rst
    • Exported from main package: from nwp500 import MqttDiagnosticsCollector

Version 6.1.0 (2025-12-03)

BREAKING CHANGES: Temperature API simplified with Fahrenheit input

This release fixes incorrect temperature conversions and provides a cleaner API where users pass temperatures in Fahrenheit directly, with automatic conversion to the device's internal format.

Changed

  • ``build_reservation_entry()``: Now accepts temperature_f (Fahrenheit) instead of raw param value. The conversion to half-degrees Celsius is handled automatically.

    # OLD (removed)
    build_reservation_entry(..., param=120)
    
    # NEW
    build_reservation_entry(..., temperature_f=140.0)
  • ``set_dhw_temperature()``: Now accepts temperature_f: float (Fahrenheit) instead of raw integer. Valid range: 95-150°F.

    # OLD (removed)
    await mqtt.set_dhw_temperature(device, 120)
    
    # NEW
    await mqtt.set_dhw_temperature(device, 140.0)

Removed

  • ``set_dhw_temperature_display()``: Removed. This method used an incorrect conversion formula (subtracting 20 instead of proper half-degrees Celsius encoding). Use set_dhw_temperature() with Fahrenheit directly.

Added

  • ``fahrenheit_to_half_celsius()``: New utility function for converting Fahrenheit to the device's half-degrees Celsius format. Exported from the main package for advanced use cases.

    from nwp500 import fahrenheit_to_half_celsius
    
    param = fahrenheit_to_half_celsius(140.0)  # Returns 120

Fixed

  • Temperature Encoding Bug: Fixed set_dhw_temperature() which was using an incorrect "subtract 20" conversion instead of proper half-degrees Celsius encoding. This caused temperatures to be set incorrectly for values other than 140°F (where both formulas happened to give the same result).

Version 6.0.8 (2025-12-02)

Changed

  • Maintenance Release: Version bump for PyPI release

Version 6.0.7 (2025-11-30)

Added

  • Documentation: Added TOU (Time-of-Use) enable/disable command payload formats to protocol documentation

Version 6.0.6 (2025-11-24)

Added

  • Field Descriptions: Added comprehensive Field descriptions to DeviceStatus and DeviceFeature models with full documentation details including units, ranges, and usage context

Fixed

  • Example Code: Fixed device_status_callback.py example to use snake_case attribute names consistently
  • Field Descriptions: Clarified distinctions between similar fields:
    • dhw_temperature_setting vs dhw_target_temperature_setting descriptions
    • freeze_protection_temp descriptions differ between DeviceStatus and DeviceFeature
    • eco_use descriptions differ between DeviceStatus (current state) and DeviceFeature (capability)

Version 6.0.5 (2025-11-21)

Fixed

  • CRITICAL Temperature Conversion Bug: Corrected temperature conversion formula for 8 sensor fields that were displaying values ~100°F higher than expected. The v6.0.4 change incorrectly used division by 5 (pentacelsius) instead of division by 10 (decicelsius) for these fields:

    • tank_upper_temperature - Water tank upper sensor
    • tank_lower_temperature - Water tank lower sensor
    • discharge_temperature - Compressor discharge temperature (refrigerant)
    • suction_temperature - Compressor suction temperature (refrigerant)
    • evaporator_temperature - Evaporator coil temperature (refrigerant)
    • ambient_temperature - Ambient air temperature at heat pump
    • target_super_heat - Target superheat setpoint
    • current_super_heat - Measured superheat value

    Impact: These fields now correctly display temperatures in expected ranges:

    • Tank temperatures: ~120°F (close to DHW temperature, not ~220°F)
    • Discharge temperature: 120-180°F (not 220-280°F)
    • Suction, evaporator, ambient: Now showing physically realistic values

    Technical details: Changed from PentaCelsiusToF (÷5) back to DeciCelsiusToF (÷10). The correct formula is (raw_value / 10.0) * 9/5 + 32.

Changed

  • Documentation: Updated data_conversions.rst and device_status.rst to reflect correct DeciCelsiusToF conversion for refrigerant circuit and tank temperature sensors

Version 6.0.4 (2025-11-21)

Fixed

  • Temperature Conversion Accuracy: Corrected temperature conversion logic based on analysis of the decompiled mobile application. Previous conversions used approximations; new logic uses exact formulas from the app:
    • Replaced Add20 validator with HalfCelsiusToF for fields transmitted as half-degrees Celsius
    • Replaced DeciCelsiusToF with PentaCelsiusToF for fields scaled by factor of 5
    • Affects multiple temperature sensor readings for improved accuracy
  • CLI Output Formatting: Fixed formatting issues in command-line interface output

Changed

  • Documentation: Updated temperature conversion documentation to use precise 9/5 fraction instead of 1.8 approximation for clarity

Added

  • Test Coverage: Added tests/test_models.py to verify temperature conversion correctness

Version 6.0.3 (2025-11-20)

BREAKING CHANGES: Migration from custom dataclass-based models to Pydantic BaseModel implementations with automatic field validation and alias handling.

Removed

  • Removed legacy dataclass implementations for models (DeviceInfo, Location, Device, FirmwareInfo, DeviceStatus, DeviceFeature, EnergyUsage*). All models now inherit from NavienBaseModel (Pydantic).
  • Removed manual from_dict constructors relying on camelCase key mapping logic.
  • Removed field metadata conversion system (meta() + apply_field_conversions()) in favor of Pydantic BeforeValidator pipeline.

Changed

  • Models now use snake_case attribute names consistently; camelCase keys from API/MQTT are mapped automatically via Pydantic alias_generator=to_camel.
  • Boolean device fields now validated via DeviceBool Annotated type (device value 2 -> True, 0/1 -> False) replacing manual conversion code.
  • Temperature offset (+20), scale division (/10) and decicelsius-to-Fahrenheit conversions implemented with lightweight BeforeValidator functions (Add20, Div10, DeciCelsiusToF) instead of post-processing.
  • Enum parsing now handled directly by Pydantic; unknown values default safely via explicit Field defaults instead of try/except conversion loops.
  • Field names updated (examples & docs) to snake_case: e.g. operationMode -> operation_mode, dhwTemperatureSetting -> dhw_temperature_setting.
  • API typo handled using Field alias (heLowerOnTDiffempSetting -> he_lower_on_diff_temp_setting) rather than custom dictionary mutation.
  • DeviceStatus conversion now performed on parse instead of separate transformation step, improving performance and reducing memory copies.
  • Improved validation error messages from Pydantic on malformed payloads.
  • Simplified energy usage model accessors; removed manual percentage methods duplication.

Added

  • Introduced NavienBaseModel configuring alias generation, population by name, and ignoring unknown fields for forward compatibility.
  • Added structured Annotated types: DeviceBool, Add20, Div10, DeciCelsiusToF for declarative conversion definitions.
  • Added consistent default enum values directly in field declarations (e.g. operation_mode=STANDBY).

Migration Guide (v6.0.2 -> v6.0.3)

  1. Replace any imports of dataclass models with Pydantic versions (paths unchanged). No code change required if you only accessed attributes.
  2. Remove calls to Model.from_dict(data): Either use Model.model_validate(data) or continue calling from_dict where still provided (thin wrapper for backward compatibility on some classes). Preferred: DeviceStatus.model_validate(raw_payload).
  3. Update attribute access to snake_case. Common mappings: - deviceInfo.macAddress -> device.device_info.mac_address - deviceStatus.operationMode -> status.operation_mode - deviceStatus.dhwTemperatureSetting -> status.dhw_temperature_setting - deviceStatus.currentInletTemperature -> status.current_inlet_temperature
  4. Remove manual conversion code. Raw numeric values are converted automatically; stop adding +20 or dividing by 10 in user code.
  5. Stop performing boolean normalization (value == 2) manually; attributes already return proper bools.
  6. For enum handling, remove try/except wrappers; rely on defaulted fields (e.g. operation_mode defaults to STANDBY).
  7. If you previously mutated raw payload keys to snake_case, eliminate that transformation step.
  8. If you logged intermediate converted dictionaries, you can access model.model_dump() for a fully converted representation.
  9. Replace any custom validation logic with Pydantic validators or continue using existing patterns; most prior validation code is now unnecessary.
  10. Energy usage: Access percentages via properties unchanged; object types now Pydantic models.

Quick Example

# OLD (v6.0.2)
raw = mqtt_payload["deviceStatus"]
converted = apply_field_conversions(DeviceStatus, raw)
status = DeviceStatus(**converted)
print(converted["dhwTemperatureSetting"] + 20)  # manual offset

# NEW (v6.0.3)
status = DeviceStatus.model_validate(mqtt_payload["deviceStatus"])
print(status.dhw_temperature_setting)  # already includes +20 offset

# OLD boolean and enum handling
is_heating = converted["currentHeatUse"] == 2
mode = OperationMode(converted["operationMode"]) if converted["operationMode"] in (0,32,64,96) else OperationMode.STANDBY

# NEW simplified
is_heating = status.current_heat_use
mode = status.operation_mode

Benefits

  • Declarative conversions reduce 400+ lines of imperative transformation logic.
  • Improved performance (single parse vs copy + transform).
  • Automatic camelCase key mapping; less brittle than manual dict key copying.
  • Rich validation errors for debugging malformed device messages.
  • Cleaner, shorter model definitions with clearer intent.
  • Easier extension: add new fields with conversion by combining Annotated + validator.

Version 6.0.2 (2025-11-15)

Fixed

  • DNS resolution in containerized environments using ThreadedResolver
  • Updated AWS IoT library version
  • Device status field conversions

Changed

  • Refactored ThreadedResolver session creation into helper method

Version 6.0.1 (2025-11-06)

Fixed

  • Minor bug fixes and improvements

Version 6.0.0 (2025-11-02)

BREAKING CHANGES: Removed constructor callbacks and backward compatibility re-exports

Removed

  • Constructor Callbacks: Removed on_connection_interrupted and on_connection_resumed constructor parameters from NavienMqttClient

    # OLD (removed in v6.0.0)
    mqtt_client = NavienMqttClient(
        auth_client,
        on_connection_interrupted=on_interrupted,
        on_connection_resumed=on_resumed,
    )
    
    # NEW (use event emitter pattern)
    mqtt_client = NavienMqttClient(auth_client)
    mqtt_client.on("connection_interrupted", on_interrupted)
    mqtt_client.on("connection_resumed", on_resumed)
  • Backward Compatibility Re-exports: Removed exception re-exports from api_client and auth modules

    # OLD (removed in v6.0.0)
    from nwp500.api_client import APIError
    from nwp500.auth import AuthenticationError, TokenRefreshError
    
    # NEW (import from exceptions module)
    from nwp500.exceptions import APIError, AuthenticationError, TokenRefreshError
    
    # OR (import from package root - recommended)
    from nwp500 import APIError, AuthenticationError, TokenRefreshError
  • Rationale: Library is young with no external clients. Removing backward compatibility allows for cleaner architecture and prevents accumulation of legacy patterns.

Changed

  • Migration Benefits:
    • Multiple listeners per event (not just one callback)
    • Consistent API with other events (temperature_changed, mode_changed, etc.)
    • Dynamic listener management (add/remove listeners at runtime)
    • Async handler support
    • Priority-based execution
    • Cleaner imports (exceptions from one module)
  • Updated examples/command_queue_demo.py to use event emitter pattern
  • Updated examples/reconnection_demo.py to use event emitter pattern
  • Updated examples/device_status_callback.py to import exceptions from correct module
  • Updated examples/device_status_callback_debug.py to import exceptions from correct module
  • Updated examples/device_feature_callback.py to import exceptions from correct module
  • Updated examples/test_api_client.py to import exceptions from correct module
  • Removed misleading "legacy state" comments from connection tracking code

Version 5.0.2 (2025-10-31)

Fixed

  • MQTT Future Cancellation: Fixed InvalidStateError exceptions during disconnect
    • Added asyncio.shield() to protect concurrent.futures.Future objects from cancellation
    • Applied consistent cancellation handling across all MQTT operations (connect, disconnect, subscribe, unsubscribe, publish)
    • AWS CRT callbacks can now complete independently without raising InvalidStateError
    • Added debug logging when operations are cancelled for better diagnostics
    • Ensures clean shutdown without spurious exception messages

Version 5.0.1 (2025-10-27)

Changed

  • Maintenance Release: Removed deprecated backward compatibility code
    • Removed CMD_* backward compatibility aliases from constants.py
    • Removed cli.py backward compatibility wrapper module
    • Updated setup.cfg entry point to use nwp500.cli.__main__:run directly
    • Updated all examples to use CommandCode enum instead of CMD_* aliases
    • Updated examples to use standalone functions (build_tou_period, encode_price, decode_price, encode_week_bitfield, decode_week_bitfield) instead of NavienAPIClient.* static methods
    • Updated documentation to reference standalone functions
    • Fixed deprecated method name (set_dhw_operation_settingset_dhw_mode)
    • Removed broken relative links from README.rst
    • Added Read the Docs and GitHub links to README.rst header

Version 5.0.0 (2025-10-27)

BREAKING CHANGES: This release introduces a comprehensive enterprise exception architecture. See migration guide below for details on updating your code.

Added

  • Enterprise Exception Architecture: Complete exception hierarchy for better error handling
    • Created exceptions.py module with comprehensive exception hierarchy
    • Added Nwp500Error as base exception for all library errors
    • Added MQTT-specific exceptions: MqttError, MqttConnectionError, MqttNotConnectedError, MqttPublishError, MqttSubscriptionError, MqttCredentialsError
    • Added validation exceptions: ValidationError, ParameterValidationError, RangeValidationError
    • Added device exceptions: DeviceError, DeviceNotFoundError, DeviceOfflineError, DeviceOperationError
    • All exceptions now include error_code, details, and retriable attributes
    • Added to_dict() method to all exceptions for structured logging
    • Added comprehensive test suite in tests/test_exceptions.py
    • Added comprehensive exception handling example (examples/exception_handling_example.py)
    • Updated key examples to demonstrate new exception handling patterns

Changed

  • Exception Handling Improvements:
    • All exception wrapping now uses exception chaining (raise ... from e) to preserve stack traces
    • Replaced 19+ instances of RuntimeError("Not connected to MQTT broker") with MqttNotConnectedError
    • Replaced ValueError in validation code with RangeValidationError and ParameterValidationError
    • Replaced ValueError for credentials with MqttCredentialsError
    • Replaced RuntimeError for connection issues with MqttConnectionError
    • Enhanced AwsCrtError wrapping in MQTT code with proper exception chaining
    • Moved authentication exceptions from auth.py to exceptions.py
    • Moved APIError from api_client.py to exceptions.py
    • CLI now handles specific exception types with better error messages and user guidance

Migration Guide (v4.x to v5.0)

Breaking Changes Summary:

The library now uses specific exception types instead of generic RuntimeError and ValueError. This improves error handling but requires updates to exception handling code.

1. MQTT Connection Errors

# OLD CODE (v4.x) - will break
try:
    await mqtt_client.request_device_status(device)
except RuntimeError as e:
    if "Not connected" in str(e):
        await mqtt_client.connect()

# NEW CODE (v5.0+)
from nwp500 import MqttNotConnectedError, MqttError

try:
    await mqtt_client.request_device_status(device)
except MqttNotConnectedError:
    # Handle not connected - attempt reconnection
    await mqtt_client.connect()
    await mqtt_client.request_device_status(device)
except MqttError as e:
    # Handle other MQTT errors
    logger.error(f"MQTT error: {e}")

2. Validation Errors

# OLD CODE (v4.x) - will break
try:
    set_vacation_mode(device, days=35)
except ValueError as e:
    print(f"Invalid input: {e}")

# NEW CODE (v5.0+)
from nwp500 import RangeValidationError, ValidationError

try:
    set_vacation_mode(device, days=35)
except RangeValidationError as e:
    # Access structured error information
    print(f"Invalid {e.field}: must be {e.min_value}-{e.max_value}")
    print(f"You provided: {e.value}")
except ValidationError as e:
    # Handle other validation errors
    print(f"Validation error: {e}")

3. AWS Credentials Errors

# OLD CODE (v4.x) - will break
try:
    mqtt_client = NavienMqttClient(auth_client)
except ValueError as e:
    if "credentials" in str(e).lower():
        # handle missing credentials

# NEW CODE (v5.0+)
from nwp500 import MqttCredentialsError

try:
    mqtt_client = NavienMqttClient(auth_client)
except MqttCredentialsError as e:
    # Handle missing or invalid AWS credentials
    logger.error(f"Credentials error: {e}")
    await re_authenticate()

4. Catching All Library Errors

# NEW CODE (v5.0+) - catch all library exceptions
from nwp500 import Nwp500Error

try:
    # Any library operation
    await mqtt_client.request_device_status(device)
except Nwp500Error as e:
    # All nwp500 exceptions inherit from Nwp500Error
    logger.error(f"Library error: {e.to_dict()}")

    # Check if retriable
    if e.retriable:
        await retry_operation()

5. Enhanced Error Information

All exceptions now include structured information:

from nwp500 import MqttPublishError

try:
    await mqtt_client.publish(topic, payload)
except MqttPublishError as e:
    # Access structured error information
    error_info = e.to_dict()
    # {
    #     'error_type': 'MqttPublishError',
    #     'message': 'Publish failed',
    #     'error_code': 'AWS_ERROR_...',
    #     'details': {},
    #     'retriable': True
    # }

    # Log for monitoring/alerting
    logger.error("Publish failed", extra=error_info)

    # Implement retry logic
    if e.retriable:
        await asyncio.sleep(1)
        await mqtt_client.publish(topic, payload)

Quick Migration Strategy:

  1. Import new exception types: from nwp500 import MqttNotConnectedError, MqttError, ValidationError
  2. Replace except RuntimeError with except MqttNotConnectedError for connection checks
  3. Replace except ValueError with except ValidationError for parameter validation
  4. Use except Nwp500Error to catch all library errors
  5. Test error handling paths thoroughly

Benefits of New Architecture:

  • Specific exception types for specific errors (no more string matching)
  • Preserved stack traces with exception chaining (from e)
  • Structured error information via to_dict()
  • Retriable flag for implementing retry logic
  • Better integration with monitoring/logging systems
  • Type-safe error handling
  • Clearer API documentation

Version 4.8.0 (2025-10-27)

Added

  • Token Restoration Support: Enable session persistence across application restarts
    • Added stored_tokens parameter to NavienAuthClient.__init__() for restoring saved tokens
    • Added AuthTokens.to_dict() method for serializing tokens (includes issued_at timestamp)
    • Enhanced AuthTokens.from_dict() to support both API responses (camelCase) and stored data (snake_case)
    • Modified NavienAuthClient.__aenter__() to skip authentication when valid stored tokens are provided
    • Automatically refreshes expired JWT tokens or re-authenticates if AWS credentials expired
    • Added 7 new tests for token serialization, deserialization, and restoration flows
    • Added examples/token_restoration_example.py demonstrating save/restore workflow
    • Updated authentication documentation with token restoration guide
  • Benefits: Reduces API load, improves startup time, prevents rate limiting for frequently restarting applications (e.g., Home Assistant)

Version 4.7.1 (2025-10-27)

Changed

  • Patch Release: No code changes, updated version format to full semantic versioning

Version 4.7 (2025-10-27)

Added

  • MQTT Reconnection: Two-tier reconnection strategy with unlimited retries
    • Implemented quick reconnection (attempts 1-9) for fast recovery from transient network issues
    • Implemented deep reconnection (every 10th attempt) with full connection rebuild and credential refresh
    • Changed default max_reconnect_attempts from 10 to -1 (unlimited retries)
    • Added deep_reconnect_threshold configuration parameter (default: 10)
    • Added has_stored_credentials property to NavienAuthClient
    • Added re_authenticate() method to NavienAuthClient for credential-based re-authentication
    • Added resubscribe_all() method to MqttSubscriptionManager for subscription recovery
    • Deep reconnection now performs token refresh and falls back to full re-authentication if needed
    • Deep reconnection automatically re-establishes all subscriptions after rebuild
    • Connection now continues retrying indefinitely instead of giving up after 10 attempts

Improved

  • Exception Handling: Replaced 25 catch-all exception handlers with specific exception types
    • mqtt_client.py: Uses AwsCrtError, AuthenticationError, TokenRefreshError, RuntimeError, ValueError, TypeError, AttributeError
    • mqtt_reconnection.py: Uses AwsCrtError, RuntimeError, ValueError, TypeError
    • mqtt_connection.py: Uses AwsCrtError, RuntimeError, ValueError
    • mqtt_subscriptions.py: Uses AwsCrtError, RuntimeError, TypeError, AttributeError, KeyError, ValueError
    • mqtt_periodic.py: Uses AwsCrtError, RuntimeError
    • events.py: Retains Exception for user callbacks (documented as legitimate use case)
    • Added exception handling guidelines to .github/copilot-instructions.md
  • Code Quality: Multiple readability and safety improvements
    • Simplified nested conditions by extracting to local variables
    • Added hasattr() checks before accessing AwsCrtError.name attribute
    • Optimized resubscribe_all() to break after first failure per topic (reduces redundant error logs)
    • Fixed subscription failure tracking to use sets for unique topic counting
    • Improved code clarity with intermediate variables for complex boolean expressions

Fixed

  • MQTT Reconnection: Eliminated duplicate "Connection interrupted" log messages
    • Removed duplicate logging from mqtt_client.py (kept in mqtt_reconnection.py)

Version 3.1.4 (2025-10-26)

Fixed

  • MQTT Reconnection: Fixed MQTT reconnection failures due to expired AWS credentials
    • Added AWS credential expiration tracking (_aws_expires_at field in AuthTokens)
    • Added are_aws_credentials_expired property to check AWS credential validity
    • Modified ensure_valid_token() to prioritize AWS credential expiration check
    • Triggers full re-authentication (not just token refresh) when AWS credentials expire
    • Preserves AWS credential expiration timestamps during token refresh
    • Prevents reconnection failures when connection interrupts after AWS credentials expire but before JWT tokens expire
    • Resolves AWS_ERROR_HTTP_WEBSOCKET_UPGRADE_FAILURE errors during reconnection attempts
    • Improved test coverage for auth module from 31% to 60% with comprehensive test suite

Version 3.1.3 (2025-10-24)

Fixed

  • MQTT Reconnection: Improved MQTT reconnection reliability with active reconnection
    • Breaking Internal Change: MqttReconnectionHandler now requires reconnect_func parameter (not Optional)
    • Implemented active reconnection that always recreates MQTT connection on interruption
    • Removed unreliable passive fallback to AWS IoT SDK automatic reconnection
    • Added automatic connection state checking during reconnection attempts
    • Now emits reconnection_failed event when max reconnection attempts are exhausted
    • Improved error handling and logging during reconnection process
    • Better recovery from WebSocket connection interruptions (AWS_ERROR_MQTT_UNEXPECTED_HANGUP)
    • Resolves issues where connection would fail to recover after network interruptions
    • Note: Public API unchanged - NavienMqttClient continues to work as before
    • Compatible with existing auto-recovery examples (auto_recovery_example.py, simple_auto_recovery.py)

Version 3.1.2 (2025-01-23)

Fixed

  • Authentication: Fixed 401 authentication errors with automatic token refresh
    • Add automatic token refresh on 401 Unauthorized responses in API client
    • Preserve AWS credentials when refreshing tokens (required for MQTT)
    • Save refreshed tokens to cache after successful API calls
    • Add retry logic to prevent infinite retry loops
    • Validate refresh_token exists before attempting refresh
    • Use specific exception types (TokenRefreshError, AuthenticationError) in error handling
    • Prevents masking unexpected errors during token refresh
    • Resolves 'API request failed: 401' error when using cached tokens

Version 3.1.1 (2025-01-22)

Fixed

  • MQTT Client: Fixed connection interrupted callback signature for AWS SDK
    • Updated callback to match latest AWS IoT SDK signature: (connection, error, **kwargs)
    • Fixed type annotations in MqttConnection for proper type checking
    • Resolves mypy type checking errors and ensures AWS SDK compatibility
    • Fixed E501 line length linting issue in connection interruption handler

Version 3.0.0 (Unreleased)

Breaking Changes

  • REMOVED: OperationMode enum has been removed
    • This enum was deprecated in v2.0.0 and has now been fully removed
    • Use DhwOperationSetting for user-configured mode preferences (values 1-6)
    • Use CurrentOperationMode for real-time operational states (values 0, 32, 64, 96)
    • Migration was supported throughout the v2.x series
  • REMOVED: Migration helper functions and deprecation infrastructure
    • Removed migrate_operation_mode_usage() function
    • Removed enable_deprecation_warnings() function
    • Removed migration documentation files (MIGRATION.md, BREAKING_CHANGES_V3.md)
    • All functionality available through DhwOperationSetting and CurrentOperationMode

Version 2.0.0 (Unreleased)

Breaking Changes (Planned for v3.0.0)

  • DEPRECATION: OperationMode enum is deprecated and will be removed in v3.0.0
    • Use DhwOperationSetting for user-configured mode preferences (values 1-6)
    • Use CurrentOperationMode for real-time operational states (values 0, 32, 64, 96)
    • See MIGRATION.md for detailed migration guide

Added

  • Enhanced Type Safety: Split OperationMode into semantically distinct enums
    • DhwOperationSetting: User-configured mode preferences (HEAT_PUMP, ELECTRIC, ENERGY_SAVER, HIGH_DEMAND, VACATION, POWER_OFF)
    • CurrentOperationMode: Real-time operational states (STANDBY, HEAT_PUMP_MODE, HYBRID_EFFICIENCY_MODE, HYBRID_BOOST_MODE)
    • Prevents accidental comparison of user preferences with real-time states
    • Better IDE support with more specific enum types
  • Migration Support: Comprehensive tools for smooth migration
    • migrate_operation_mode_usage() helper function with programmatic guidance
    • MIGRATION.md with step-by-step migration instructions
    • Value mappings and common usage pattern examples
    • Backward compatibility preservation during transition
  • Documentation Updates: Updated all documentation to reflect new enum structure
    • DEVICE_STATUS_FIELDS.rst updated with new enum types
    • Code examples use new enums with proper imports
    • Clear distinction between configuration vs real-time status

Changed

  • DeviceStatus Model: Updated to use specific enum types
    • operationMode field now uses CurrentOperationMode type
    • dhwOperationSetting field now uses DhwOperationSetting type
    • Maintains backward compatibility through value preservation
  • Example Scripts: Updated to demonstrate new enum usage
    • event_emitter_demo.py updated to use CurrentOperationMode
    • Fixed incorrect enum references (HEAT_PUMP_ONLY → HEAT_PUMP_MODE)
    • All examples remain functional with new type system

Deprecated

  • OperationMode enum: Will be removed in v3.0.0
    • All functionality preserved for backward compatibility
    • Migration guide available in MIGRATION.md
    • Helper function migrate_operation_mode_usage() provides guidance
    • Original enum remains available during transition period

Version 1.2.2 (2025-10-17)

Fixed

  • Release version 1.2.2

Version 0.2 (Unreleased)

Added

  • Local/CI Linting Synchronization: Complete tooling to ensure consistent linting results
    • Multiple sync methods: tox (recommended), direct scripts, pre-commit hooks, Makefile commands
    • CI-identical scripts: scripts/lint.py and scripts/format.py mirror tox -e lint and tox -e format
    • Pre-commit hooks configuration for automatic checking
    • Comprehensive documentation: LINTING_SETUP.md, DEVELOPMENT.md, FIX_LINTING.md
    • Makefile commands: make ci-lint, make ci-format, make ci-check
    • Standardized ruff configuration across all environments
    • Eliminates "passes locally but fails in CI" issues
    • Cross-platform support (Linux, macOS, Windows, containers)
    • All MQTT operations (connect, disconnect, subscribe, unsubscribe, publish) use asyncio.wrap_future() to convert AWS SDK Futures to asyncio Futures
    • Eliminates "blocking I/O detected" warnings in Home Assistant and other async applications
    • Fully compatible with async event loops without blocking other operations
    • More efficient than executor-based approaches (no thread pool usage)
    • No API changes required - existing code works without modification
    • Maintains full performance and reliability of the underlying AWS IoT SDK
    • Safe for use in Home Assistant custom integrations and other async applications
    • Updated documentation with non-blocking implementation details
  • Event Emitter Pattern (Phase 1): Event-driven architecture for device state changes
    • EventEmitter base class with multiple listeners per event
    • Async and sync handler support
    • Priority-based execution order (higher priority executes first)
    • One-time listeners with once() method
    • Dynamic listener management with on(), off(), remove_all_listeners()
    • Event statistics tracking (listener_count(), event_count())
    • wait_for() pattern for waiting on specific events
    • Thread-safe event emission from MQTT callback threads
    • Automatic state change detection for device monitoring
    • 11 events emitted automatically: status_received, feature_received, temperature_changed, mode_changed, power_changed, heating_started, heating_stopped, error_detected, error_cleared, connection_interrupted, connection_resumed
    • NavienMqttClient now inherits from EventEmitter
    • Full backward compatibility with existing callback API
    • 19 unit tests with 93% code coverage
    • Example: event_emitter_demo.py
    • Documentation: EVENT_EMITTER.rst, EVENT_QUICK_REFERENCE.rst, EVENT_ARCHITECTURE.rst
  • Authentication: Simplified constructor-based authentication
    • NavienAuthClient now requires user_id and password in constructor
    • Automatic authentication when entering async context manager
    • No need to call sign_in() manually
    • Breaking change: credentials are now required parameters
    • Updated all 18 example files to use new pattern
    • Updated all documentation with new authentication examples
  • MQTT Command Queue: Automatic command queuing when disconnected
    • Commands sent while disconnected are automatically queued
    • Queue processed in FIFO order when connection is restored
    • Configurable queue size (default: 100 commands)
    • Automatic oldest-command-dropping when queue is full
    • Enabled by default for reliability
    • queued_commands_count property for monitoring
    • clear_command_queue() method for manual management
    • Integrates seamlessly with automatic reconnection
    • Example: command_queue_demo.py
    • Documentation: COMMAND_QUEUE.rst
  • MQTT Reconnection: Automatic reconnection with exponential backoff
    • Automatic reconnection on connection interruption
    • Configurable exponential backoff (default: 1s, 2s, 4s, 8s, ... up to 120s)
    • Configurable max attempts (default: 10)
    • Connection state properties: is_reconnecting, reconnect_attempts
    • User callbacks for connection interruption and resumption events
    • Manual disconnect detection to prevent unwanted reconnection
    • MqttConnectionConfig with reconnection settings
    • Example: reconnection_demo.py
    • Documentation: Added reconnection section to MQTT_CLIENT.rst
  • MQTT Client: Complete implementation of real-time device communication
    • WebSocket MQTT connection to AWS IoT Core
    • Device subscription and message handling
    • Status request methods (device info, device status)
    • Control commands for device management
    • Topic pattern matching with wildcard support
    • Connection lifecycle management (connect, disconnect, reconnect)
  • Device Control: Fully implemented and verified control commands
    • Power control (on/off) with correct command codes
    • DHW mode control (Heat Pump, Electric, Energy Saver, High Demand)
    • DHW temperature control with 20°F offset handling
    • App connection signaling
    • Helper method for display-value temperature control
  • Typed Callbacks: 100% coverage of all MQTT response types
    • subscribe_device_status() - Automatic parsing of status messages into DeviceStatus objects
    • subscribe_device_feature() - Automatic parsing of feature messages into DeviceFeature objects
    • subscribe_energy_usage() - Automatic parsing of energy usage responses into EnergyUsageResponse objects
    • Type-safe callbacks with IDE autocomplete support
    • Comprehensive error handling and logging
    • Example scripts demonstrating usage patterns
  • Energy Usage API (EMS): Historical energy consumption data
    • request_energy_usage() - Query daily energy usage for specified month(s)
    • EnergyUsageResponse dataclass with daily breakdown
    • EnergyUsageTotal with percentage calculations
    • MonthlyEnergyData with per-day access methods
    • EnergyUsageData for individual day/month metrics
    • Heat pump vs. electric element usage tracking
    • Operating time statistics (hours)
    • Energy consumption data (Watt-hours)
    • Efficiency percentage calculations
  • Data Models: Comprehensive type-safe models
    • DeviceStatus dataclass with 125 sensor and operational fields
    • DeviceFeature dataclass with 46 capability and configuration fields
    • EnergyUsageResponse dataclass for historical energy data
    • EnergyUsageTotal with aggregated statistics and percentages
    • MonthlyEnergyData with daily breakdown per month
    • EnergyUsageData for individual day/month metrics
    • OperationMode enum including STANDBY state (value 0)
    • TemperatureUnit enum (Celsius/Fahrenheit)
    • MQTT command structures
    • Authentication tokens and user info
  • API Client: High-level REST API client
    • Device listing and information retrieval
    • Firmware information queries
    • Time-of-Use (TOU) schedule management
    • Push notification token management
    • Async context manager support
    • Automatic session management
  • Authentication: AWS Cognito integration
    • Sign-in with email/password
    • Access token management
    • Token refresh functionality
    • AWS IoT credentials extraction for MQTT
    • Async context manager support
  • Documentation: Complete protocol and API documentation
    • MQTT message format specifications
    • Energy usage query API documentation (EMS data)
    • API client usage guide
    • MQTT client usage guide
    • Typed callbacks implementation guide
    • Control command reference with verified command codes
    • Example scripts for common use cases
    • Comprehensive troubleshooting guides
    • Complete energy data reference (ENERGY_DATA_SUMMARY.md)
  • Examples: Production-ready example scripts
    • device_status_callback.py - Real-time status monitoring with typed callbacks
    • device_feature_callback.py - Device capabilities and firmware info
    • combined_callbacks.py - Both status and feature callbacks together
    • mqtt_client_example.py - Complete MQTT usage demonstration
    • energy_usage_example.py - Historical energy usage monitoring and analysis
    • reconnection_demo.py - MQTT automatic reconnection demonstration
    • auth_constructor_example.py - Simplified authentication pattern

Changed

  • Breaking: Python version requirement updated to 3.9+
    • Minimum Python version is now 3.9 (was 3.8)
    • Migrated to native type hints (PEP 585): dict[str, Any] instead of Dict[str, Any]
    • Removed typing.Dict, typing.List, typing.Deque imports
    • Cleaner, more readable code with modern Python features
    • Added Python version classifiers (3.9-3.13) to setup.cfg
    • Updated ruff target-version to py39
  • Breaking: NavienAuthClient constructor signature
    • Now requires user_id and password as first parameters
    • Old: NavienAuthClient() then await client.sign_in(email, password)
    • New: NavienAuthClient(email, password) - authentication is automatic
    • Migration: Pass credentials to constructor instead of sign_in()
    • All 18 example files updated to new pattern
    • All documentation updated with new examples
  • Documentation: Major updates across all files
    • Fixed all RST formatting issues (title underlines, tables)
    • Updated authentication examples in 8 documentation files
    • Fixed broken documentation links (local file paths)
    • Removed "Optional Feature" and "not required for basic operation" phrases
    • Fixed table rendering in DEVICE_STATUS_FIELDS.rst
    • Fixed JSON syntax in code examples
    • Added comprehensive reconnection documentation
    • Added comprehensive command queue documentation
    • Cleaned up backward compatibility references (new library)

Fixed

  • Critical Bug: Thread-safe reconnection task creation from MQTT callbacks
    • Fixed RuntimeError: no running event loop when connection is interrupted
    • Fixed RuntimeWarning: coroutine '_reconnect_with_backoff' was never awaited
    • Connection interruption callbacks run in separate threads without event loops
    • Implemented _start_reconnect_task() helper method to properly create reconnection tasks
    • Uses existing _schedule_coroutine() method for thread-safe task scheduling
    • Prevents crashes during automatic reconnection after connection interruptions
    • Ensures reconnection tasks are properly awaited and executed
  • Critical Bug: Thread-safe event emission from MQTT callbacks
    • Fixed RuntimeError: no running event loop in thread 'Dummy-1'
    • MQTT callbacks run in separate threads created by AWS IoT SDK
    • Implemented _schedule_coroutine() method for thread-safe scheduling
    • Event loop reference captured during connect() for cross-thread access
    • Uses asyncio.run_coroutine_threadsafe() for safe event emission
    • Prevents crashes when emitting events from MQTT message handlers
    • All event emissions now work correctly from any thread
  • Bug: Incorrect method parameter passing in temperature control
    • Fixed set_dhw_temperature_display() calling set_dhw_temperature() with wrong parameters
    • Was passing individual parameters (device_id, device_type, additional_value)
    • Now correctly passes Device object as expected by method signature
    • Simplified implementation to just calculate offset and delegate to base method
    • Updated docstrings to match actual method signatures
  • Enhancement: Anonymized MAC addresses in documentation
    • Replaced all occurrences of real MAC address (04786332fca0) with placeholder (aabbccddeeff)
    • Updated API_CLIENT.rst, MQTT_CLIENT.rst, MQTT_MESSAGES.rst
    • Updated built HTML documentation files
    • Protects privacy in public documentation
  • Critical Bug: Device control command codes
    • Fixed incorrect command code usage causing unintended power-off
    • Power-off now uses command code 33554433
    • Power-on now uses command code 33554434
    • DHW mode control now uses command code 33554437
    • Discovered through network traffic analysis of official app
  • Critical Bug: MQTT topic pattern matching with wildcards
    • Fixed _topic_matches_pattern() to correctly handle # wildcard
    • Topics now match when message arrives on base topic (e.g., cmd/52/device/res)
    • Topics also match subtopics (e.g., cmd/52/device/res/extra)
    • Added length validation to prevent index out of bounds errors
    • Enables callbacks to receive messages correctly
  • Bug: Missing OperationMode.STANDBY enum value
    • Added STANDBY = 0 to OperationMode enum
    • Device reports mode 0 when tank is fully charged and no heating is needed
    • Added graceful fallback for unknown enum values
    • Prevents ValueError when parsing device status
  • Bug: Insufficient topic subscriptions
    • Examples now subscribe to broader topic patterns
    • Subscribe to cmd/{device_type}/{device_topic}/# to catch all command messages
    • Subscribe to evt/{device_type}/{device_topic}/# to catch all event messages
    • Ensures all device responses are received
  • Enhancement: Robust enum conversion with fallbacks
    • Added try/except blocks for all enum conversions in DeviceStatus.from_dict()
    • Added try/except blocks for all enum conversions in DeviceFeature.from_dict()
    • Unknown operation modes default to STANDBY
    • Unknown temperature types default to FAHRENHEIT
    • Prevents parsing failures from unexpected values
  • Documentation: Updated MQTT_MESSAGES.rst with correct command codes and temperature offset

Verified

  • Device Control: Real-world testing with Navien NWP500 device
    • Successfully changed DHW mode from Heat Pump to Energy Saver
    • Successfully changed DHW mode from Energy Saver to High Demand
    • Successfully changed DHW temperature (discovered 20°F offset between message and display)
    • Commands confirmed to reach and control physical device
    • Documented in DEVICE_CONTROL_VERIFIED.md

Version 0.1

  • Initial Documentation