CLI formatting stacks merged; Rich is the sole human renderer (#100): a new
src/nwp500/cli/presentation.pyowns all data-shaping for CLI output (field selection, labels, units, ordering, value formatting and energy aggregation) as presentation-neutral structures. Because the CLI hard-requiresrich(there is no plain-text fallback), the redundant plain-text human renderer and the Rich-vs-plain fallback machinery were removed:rich_output.pyno longer contains_should_use_rich,_rich_available, theNWP500_NO_RICHtoggle, or any_print_*_plainmethods, and now renders the neutral structures (including the energyTOTAL SUMMARY) exclusively with Rich, consuming the presentation dataclasses directly instead of dict adapters.output_formatters.pykeeps 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 ofNavienMqttClientinto two focused mixins,DeviceControlCommandsMixin(mqtt/_control_commands.py) andDeviceSubscriptionsMixin(mqtt/_device_subscriptions.py).NavienMqttClientnow inherits both, so its public API is unchanged, whilemqtt/client.pyshrinks 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.pyandmqtt_events.pyare not two competing event mechanisms.EventEmitter(events.py) is the sole delivery mechanism, whilemqtt_events.pyprovides the event-name registry (MqttClientEvents) and the typed dataclass payloads it carries. Internalemitcall sites inmqtt/state_tracker.py,mqtt/client.pyandmqtt/subscriptions.pynow reference theMqttClientEventsconstants 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
awscrtSDK types on its own public/semi-public API surface. A library-ownednwp500.mqtt.QoSIntEnum(also exported asnwp500.QoS) now replacesawscrt.mqtt.QoSon thepublishandsubscribemethods ofNavienMqttClient,MqttConnectionandMqttSubscriptionManager, onMqttCommandQueue.enqueueand on theQueuedCommanddataclass.awscrt.mqtt.Connectionhandles are typed behind theMqttConnectionHandlealias, and translation to/fromawscrthappens only at the connection-layer boundary (nwp500/mqtt/types.py).NavienMqttClient.publishnow wraps otherwise-uncaughtAwsCrtErrorinMqttPublishErrorsoawscrtexceptions 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)
- MQTT ack futures now consumed on abandonment (#97):
_await_ack()inmqtt/connection.pyand the inline subscribe/unsubscribe acknowledgement waits inmqtt/subscriptions.pyshield 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.AwsCrtErrorfrom 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.
- Unit-system preference is a deliberate process-wide global (#103): clarified and
locked in that the preference in
nwp500.unit_systemis an intentional process-wide module-level global rather than acontextvars.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, andTOUPeriod) 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.
- Tests for process-wide unit-system semantics (#103): new
tests/test_unit_system_process_wide.pyverifies that setting the global preference affects already-constructed model instances at access time and that the preference is visible across async tasks and threads.
BREAKING CHANGES: Public API surface trimmed and dead code removed. These changes require a major version bump.
- Removed
from __future__ import annotationsproject-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, andPERF, with documented ignores for intentional patterns (grouped__all__lists, unicode degree signs, publictimeoutparameters). 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, aClassVarannotation on the capability map, and danglingasyncio.create_taskreferences in tests. - PeriodicRequestType is now a StrEnum (was a plain
Enumwith string values), matching the enum style used elsewhere. - Event payload dataclasses use
slots=True: the frozen event dataclasses inmqtt_events.pyandevents.EventListenerare created per state change; slots reduce their memory footprint. - match statement for the periodic request-type dispatch.
- New unit tests for previously untested modules:
topic_builder.py(full topic schema),field_factory.py(metadata defaults, overrides, merge semantics), andmodels/_converters.py(unit-preference conversions and round-trips).
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, andDeviceOperationError. 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.commandsmodule (its metadata had drifted from the real click commands), the unusedconverters.str_enum_validator, the unused module-leveltemperature.half_celsius_to_fahrenheit/deci_celsius_to_fahrenheitwrappers, the no-opNavienMqttClient._on_message_receivedplaceholder, and unused width calculations in the CLI formatters.
- Temperature classes deduplicated:
HalfCelsius,DeciCelsius,RawCelsius, andDeciCelsiusDeltawere four near-identical copies differing only in a scale constant. All conversions are now implemented once on theTemperaturebase 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.pynow usesconverters.device_bool_from_python()instead of inline2 if enabled else 1literals. - Version resolution decoupled:
auth.pyresolves the package version from distribution metadata instead offrom . import __version__, removing an order-dependent near-circular import.
- Fix malformed weekly reservation and recirculation schedule
payloads:
update_weekly_reservation()andconfigure_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-convertedtemperaturealongside the raw half-Celsiusparam— into device commands. Both now send the flat, raw protocol shape used byupdate_reservations(). A newNavienBaseModel.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_tempreplayed beforepower_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 thanMqttConnectionConfig.max_queued_command_age(default 300 s,Noneto 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 usesmath.fmodsemantics. - 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 atdecimal_point=2encoded to 12 instead of 13) — now usesDecimalhalf-up rounding.build_tou_period()also treatedboolprices as pre-encoded integers (Truesent as price 1). - Fix protocol documentation errors:
decode_reservation_hex()documented the enable flag inverted (1=enabled instead of 2=enabled); thebuild_reservation_entry()example showedweek: 158for Mon/Wed/Fri instead of the correct 84; temperature doctest examples showedintraw values wherefloatis 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 aContextVarset 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 monitorlogged 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 afinallyblock. - Fix wait_for() docstring examples: examples showed
args, _ = await emitter.wait_for(...), butwait_forreturns just the args tuple — following the documented pattern raisedValueErroror silently mis-assigned. - Serialize concurrent token refresh:
ensure_valid_token()andrefresh_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 anasyncio.Lockwith 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 ownedClientSession(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 withasync 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
TokenRefreshErroreven though credentials for a fullsign_in()were available. - Stop pinning the auth session in the API client:
NavienAPIClientcapturedauth_client.sessionat 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 andOpenEIClientcreatedClientSession``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
AwsCrtErrorandRuntimeError, soTokenRefreshError,AuthenticationError, andMqttCredentialsErrorraised 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; onlyInvalidCredentialsErroris fatal and stops the loop with areconnection_failedevent. - 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_resumedcallback, 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
AwsCrtErrorandRuntimeError;MqttNotConnectedError/MqttPublishErrorraised 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_threadsafewere 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.CancelledErrorandbreak-ed, so cancelled tasks ended "successfully" (and the reconnection loop could emitreconnection_failedduring a manual disconnect). Cancellation now propagates correctly. - Fix AttributeError in configure_reservation_water_program: The
NavienMqttClientproxy referencedself._control, which is never assigned (the attribute is_device_controller), so every call raisedAttributeError. Now delegates correctly; a regression test guards all proxies against references to the undefined attribute. - Fix broken CLI mode choices:
nwp-cli mode vacationalways failed because vacation mode (5) requires a day count that was never supplied, andmode standbysent the invalid writable mode value0. Both choices were removed from themodecommand; use the dedicatedvacation DAYSandpower offcommands instead. - Fix CLI exit codes: click ignores command return values in standalone
mode, so the CLI always exited
0even when a command failed. Failures now propagate throughctx.exit()and produce a non-zero exit code for scripts and automation. - Fix cached tokens being reused for a different account: passing
--emailfor 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 raisesAPIErrorwith the API's error message. - Fix broken example import:
examples/advanced/mqtt_diagnostics.pyimportedMqttConnectionConfigfrom the nonexistentnwp500.mqtt_utilsmodule and used the deprecateddatetime.utcnow().
- 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.
- Restrict token cache file permissions:
~/.nwp500_tokens.json(refresh token and AWS credentials) was written world-readable. It is now created with mode0600, and existing files are tightened on save.
- Fix MQTT reconnection storm caused by non-thread-safe Task.cancel(): The
on_connection_resumedcallback is invoked from an AWS IoT SDK background thread. It was callingasyncio.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_backofftask would then complete its sleep, call_reconnect_func, and tear down an otherwise healthy connection — restarting the entire disconnect → reconnect →AWS_ERROR_MQTT_UNEXPECTED_HANGUPcycle. Fixed by replacing the directtask.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_taskto avoid wiping a newer task created during the await, and clears stale references to already-done tasks.
- Fix MQTT connection flapping after reconnect: When
_active_reconnect()created a newMqttConnection, 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, triggeringon_connection_interruptedand starting yet another reconnection — an infinite connect/disconnect loop. Fixed by addingMqttConnection.close()(unconditional teardown regardless of_connectedstate) 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 andfuture.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 acall_soon_threadsafecallback so they execute atomically on the event loop thread. - ZeroDivisionError when ``deep_reconnect_threshold`` is 0: Config validation
now clamps
deep_reconnect_thresholdto a minimum of 1, preventing aZeroDivisionErrorin the exponential-backoff reconnection logic. - Reconnect counter never incremented:
total_reconnect_attemptsin diagnostics was not incremented on connection drops, so it always reported 0 despite active reconnections. Counter is now incremented on eachon_connection_interruptedevent. - ``shortest_session_seconds`` not JSON-serialisable: The diagnostics
to_dict()method usedfloat('inf')as the initial value forshortest_session_seconds, which is not valid JSON. Changed toNoneso serialisation succeeds when no session has completed yet. - ``wait_for()`` future not bound to running loop:
wait_for()created a bareasyncio.Future()rather thanasyncio.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_entryvalidated 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 spuriousValueErrorrejections for valid temperatures. - Malformed reservation data silently dropped:
build_reservation_entrynow 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 duringget_all_cached()to prevent unbounded growth.
- Fix MQTT reconnection after unexpected AWS hangup: The
on_connection_resumedcallback was missing theconnectionparameter required by the AWS IoT SDK callback signature. The SDK callson_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), soself._connectedwas never restored toTrueafter anAWS_ERROR_MQTT_UNEXPECTED_HANGUP. As a result, theconnection_resumedevent was never emitted, the reconnection loop ran indefinitely, and device sensors became permanently unavailable until a manual restart. (#85)
- Multi-device support enhancements: Improved support for accounts with multiple
Navilink devices by injecting device identity into models and events.
- Added
mac_addressfield toDeviceStatusandDeviceFeaturemodels. - Added
device_macattribute to all device-specific MQTT events (temperature changes, mode changes, power updates, errors, etc.). - Updated
DeviceStateTrackerandMqttSubscriptionManagerto propagate device identity correctly.
- Added
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.
Typed event payloads: All
.on()event handler callbacks now receive a single typed event dataclass instance. The dataclasses are exported fromnwp500.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:
NavienMqttClientno longer exposes a.controlattribute. 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)
The following steps are recommended for a smooth migration, particularly for complex integrations like Home Assistant:
- Update Event Listeners: Locate all
mqtt.on()ormqtt.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_temperatureinstead ofnew_val). - Refactor Control Calls: Remove
.controlfrom all device command invocations. Instead ofawait mqtt.control.set_power(...), useawait mqtt.set_power(...). - Handle Unit Conversions: If your integration previously performed its own
conversions or relied on the library's eager conversion, note that
DeviceStatusfields likedhw_temperatureare now properties. They return values based on the global unit system context (us_customaryby default).- Home Assistant Tip: To ensure your state tracking is immune to unit system
toggles within the library, use the new
*_rawfields (e.g.,status.dhw_temperature_raw) for comparison logic, and use the properties only for display or when a converted value is explicitly needed.
- Home Assistant Tip: To ensure your state tracking is immune to unit system
toggles within the library, use the new
- Remove ``from_dict()`` Calls: The
from_dict()method has been removed from all models. Usemodel_validate()instead.- Note:
AuthenticationResponse.model_validate()now automatically handles the"data": { ... }wrapper found in raw API responses.
- Note:
- Subpackage Imports: While top-level imports from
nwp500.modelsare preserved, if you were importing from the internalnwp500.modelsmodule file directly, you must update your imports to point to the new structured files (e.g.,nwp500.models.status).
- New control methods: Nine previously unimplemented
CommandCodevalues 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, andset_water_program_reservation. - Typed subscription methods:
subscribe_reservation,subscribe_weekly_reservation, andsubscribe_recirculationreturn typed responses directly without requiring raw MQTT event handlers. - New protocol models:
WeeklyReservationSchedule,WeeklyReservationEntry,RecirculationSchedule,RecirculationScheduleEntry, andOtaCommitPayload. - ``DeviceStateTracker``: State change detection extracted into a dedicated class
in
nwp500.mqtt.state_trackerwith per-device tracking keyed by MAC address. - ``MQTT_PROTOCOL_VERSION`` constant: Protocol version is now a named constant in
nwp500.configrather than a hardcoded integer in the command payload builder. - ``response_ack_topic()``: New method on
MqttTopicBuilderfor control command acknowledgement topics.
- Unit conversion redesign: Temperature, flow rate, and volume fields in
DeviceStatusandDeviceFeaturenow store raw device values as*_raw: intfields 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.modelsis now a package (nwp500/models/) with modules for status, schedule, TOU, and MQTT models. Public imports fromnwp500.modelsare unchanged. - Topic building centralised: All MQTT topic construction now goes through
MqttTopicBuilder. Hardcoded topic strings removed fromcontrol.py,reservations.py, andcli/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_statuschanged from a singleDeviceStatus | Noneto adict[str, DeviceStatus]keyed by MAC address, preventing spurious state-change events when multiple devices are connected. - Unit system stored as instance variable:
NavienAPIClient,NavienMqttClient, andNavienAuthClientno longer callset_unit_system()as a constructor side-effect. - ``NavienBaseModel`` consolidated: Duplicate definitions in
auth.pyandmodels.pymerged into a single definition innwp500._base.
- Loosened pydantic version requirement: Changed from
pydantic>=2.12.5topydantic>=2.0.0to resolve dependency conflicts with Home Assistant, which ships withpydantic==2.12.2.
- Firmware Payload Capture Tool: New example script
examples/advanced/firmware_payload_capture.pyfor 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 forjq/diffcomparison across firmware versions.
- Timezone-naive datetime in token expiry checks:
AuthTokens.is_expired,are_aws_credentials_expired, andtime_until_expiryuseddatetime.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 usingdatetime.now(UTC)throughout, switching theissued_atfield default todatetime.now(UTC), and adding a field validator to normalize any timezone-naiveissued_atvalues loaded from old stored token files to UTC (previously this would raise aTypeErrorat 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 byto_dict()for tokens stored before this fix. - Vacation mode sent wrong MQTT command:
set_vacation_days()usedCommandCode.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 isDHW_MODE(33554437) withparam=[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()calledconnection.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-periodwas callingenable_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 toanti-legionella enable. - Subscription State Lost After Failed Resubscription:
resubscribe_all()cleared_subscriptionsand_message_handlersbefore 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 typeUnitSystemTypebut returnedNoneonTimeoutError, 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 asetof(event, callback)tuples. If the same callback was registered twice withonce=True, the set deduplicated the tuple — after the first emit the second listener lost its once-status and became permanent. Fixed by checkinglistener.oncedirectly on theEventListenerobject. - Auth Session Leaked on Client Construction Failure: In
create_navien_clients(), ifNavienAPIClientorNavienMqttClientconstruction raised after a successfulauth_client.__aenter__(), the auth session and its underlyingaiohttpsession would leak. Client construction is now wrapped in atry/exceptthat callsauth_client.__aexit__()on failure. Additionally, bothexcept BaseExceptionblocks have been replaced withexcept Exception(passing real exception info to__aexit__) plus a separateexcept asyncio.CancelledErrorblock that usesasyncio.shield()to ensure cleanup completes even when the task is being cancelled. - Hypothesis Tests Broke All Test Collection:
test_mqtt_hypothesis.pyimportedhypothesisat module level; when it was not installed, pytest failed to collect every test in the suite.hypothesisis now mandated as a[testing]extra dependency, restoring correct collection behaviour.
- 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
awsiotsdkversion from>=1.27.0to>=1.28.2to track the current patch release.awscrt0.31.3 is pulled in transitively.
- Reservation CRUD Helpers: New public functions
fetch_reservations(),add_reservation(),delete_reservation(), andupdate_reservation()innwp500.reservations(and exported fromnwp500). 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.
- OpenEI Client Module: New
OpenEIClientasync 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 fromOPENEI_API_KEYenvironment 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'sPUT /device/touendpoint. - 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
--enableflag to activate TOU via MQTT. - CLI Reservations Table Output:
nwp-cli reservations getnow displays reservations as a formatted table by default with global status indicator (ENABLED/DISABLED). Use--jsonflag 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 7to update cycle period.
- ``examples/advanced/tou_openei.py``: Rewritten to use the new
OpenEIClientandconvert_tou()/update_tou()library methods instead of inline OpenEI API calls and client-side conversion.
- 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 setsubscription 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.
- Converter Consistency:
div_10()andmul_10()now correctly apply division/multiplication to all input types afterfloat()conversion, not justint/floattypes - 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:
MqttCommandQueuenow raises onQueueFullinstead of silently swallowing the error - Flow Rate Metadata: Removed hardcoded
"GPM"unit fromrecirc_dhw_flow_ratefield; unit is now dynamic based on unit system - Temperature Rounding:
RawCelsiusFahrenheit conversion now uses a catch-all default for standard rounding instead of matching onlySTANDARDenum value - Unit System Default:
is_metric_preferred()now returnsFalse(Fahrenheit) instead ofNonewhen no unit system override or context is set
- Sensitive Data Logging: Redacted MQTT topics in subscription manager logging to prevent leaking device IDs (resolves CodeQL alerts)
- Auth Session Property: Added
NavienAuthClient.sessionproperty to access the activeaiohttpsession without usinggetattr - 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
- 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
- Temperature Delta Conversions: Fixed incorrect Fahrenheit conversion for differential temperature settings (heat pump and heater element on/off thresholds)
- Created new
DeciCelsiusDeltaclass for temperature deltas that apply scale factor (9/5) but NOT the +32 offset - Heat pump and heater element differential settings now use
DeciCelsiusDeltainstead ofDeciCelsius 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
- Created new
- CLI Output: Added display of heat pump and heater element differential temperature settings in device status output
- Internal API: Added
div_10_celsius_delta_to_preferredconverter for temperature delta values in device models
Temperature Setpoint Limits: Replaced hardcoded temperature limits with device-provided values
set_dhw_temperature()now validates against device-specificdhw_temperature_minanddhw_temperature_maxinstead of hardcoded 95-150°F boundsbuild_reservation_entry()changed parameter name fromtemperature_ftotemperature(unit-agnostic)- Added optional
temperature_minandtemperature_maxparameters tobuild_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, )
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()andbuild_reservation_entry()
- 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
- Critical: Temperature Unit Bug in Set Operations: Fixed incorrect temperature conversion when setting DHW temperature and reservations
set_dhw_temperature()was callingfahrenheit_to_half_celsius()with unit-agnostic temperature parameterbuild_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
- Unit System Consistency: Removed deprecated
"imperial"unit system identifier in favor of"us_customary"for Home Assistant compatibility- Changed all references from
UnitSystem.IMPERIALtoUnitSystem.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
- Changed all references from
- Unit System Consolidation: Consolidated duplicate
UnitSystemTypetype alias definitions into single canonical definition inunit_system.py- Removed redundant type definitions from multiple modules
- Improved code maintainability and consistency
- All unit system operations now reference centralized type definition
- 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
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 fieldAll conversions use Pydantic
WrapValidatorfor transparent, automatic conversionComprehensive documentation in
docs/guides/unit_conversion.rstExample 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_volumeand related storage fields
- Temperature:
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_systemparameter toNavienAuthClient,NavienMqttClient, andNavienAPIClientinitializationSet once at initialization; applies to all subsequent data conversions
Accepts:
"metric"(Celsius/LPM/Liters),"imperial"(Fahrenheit/GPM/Gallons), orNone(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-systemflag for per-command overrideExample:
nwp-cli status --unit-system metricDefaults 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
- MQTT Unit System Override Bug: Fixed unit_system CLI flag not applying to device status values
- Issue: Running
nwp-cli --unit-system metric statusdisplayed 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
NavienMqttClientandMqttSubscriptionManager. 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-systemoverride is specified - Testing: All 393 tests pass including new unit system context override tests
- Issue: Running
- Type Annotation Quotes: Removed unnecessary quoted type annotations (UP037 violations)
- With
from __future__ import annotations, explicit string quotes are redundant - Updated
from_dict()methods inUserInfo,AuthTokens,AuthenticationResponse,DeviceStatus,DeviceFeature, andEnergyUsageResponse - Improves code clarity and passes modern linting standards
- With
Daily Energy Breakdown by Month: New
--monthoption 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
- 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
ModuleNotFoundErrorwhen 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
- Installation Documentation: Updated installation instructions to clarify optional CLI and Rich dependencies
- TOU Status Always Showing False: Fixed
touStatusfield always reportingFalseregardless of actual device state- Root cause: Version 7.2.1 incorrectly changed
touStatusto use device-specific 1/2 encoding, but the device uses standard 0/1 encoding - Solution: Use Python's built-in
bool()fortouStatusfield (handles 0=False, 1=True naturally) - Updated documentation in
docs/protocol/quick_reference.rstto notetouStatusexception - Added tests verifying built-in
bool()handles 0/1 encoding correctly - Device encoding: 0=OFF/disabled, 1=ON/enabled (standard Python truthiness)
- Root cause: Version 7.2.1 incorrectly changed
CLI Command: New
device-infocommand 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 --rawConnectionStatus Enum: New
ConnectionStatusenum for device cloud connection stateConnectionStatus.DISCONNECTED= 1 - Device offline/not connectedConnectionStatus.CONNECTED= 2 - Device online and reachable- Used in
DeviceInfo.connectedfield with automatic validation
InstallType Enum: New
InstallTypeenum for device installation classificationInstallType.RESIDENTIAL= "R" - Residential useInstallType.COMMERCIAL= "C" - Commercial use- Used in
DeviceInfo.install_typefield with automatic validation - Includes
INSTALL_TYPE_TEXTmapping for display purposes
String Enum Validator: New
str_enum_validator()converter for string-based enums
- DeviceInfo Model:
-
connectedfield now usesConnectionStatusenum instead of plain int -install_typefield now usesInstallTypeenum instead of plain string - TOU Status Conversion: Simplified TOU status to use standard
device_bool_to_pythonconverter (consistent with other OnOffFlag fields) - Removed special-casetou_status_to_python()converter -TouStatusannotated type now usesdevice_bool_to_pythonvalidator - Device encoding: 1=OFF/disabled, 2=ON/enabled (consistent with all other boolean fields) - CLI Documentation: Clarified distinction between
info(DeviceFeature via MQTT) anddevice-info(DeviceInfo via REST API) commands - Type Annotations: Fixed CLI rich_output console type annotation to declare at class level
constants.py Module: Removed empty
constants.pymodule.CommandCodeenum was already moved toenums.pyin 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
BREAKING CHANGES: Class names renamed for consistency with MQTT-specific functionality
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:
DeviceCapabilityChecker→MqttDeviceCapabilityCheckerDeviceInfoCache→MqttDeviceInfoCache
All functionality remains identical - only the class names changed.
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_codefield with automatic validation - Exported from main package for convenience
- Includes
VOLUME_CODE_TEXTmapping for display purposes
Temperature Conversion Classes: Type-safe temperature handling with clear precision
HalfCelsiusclass: 0.5°C precision (value / 2.0)DeciCelsiusclass: 0.1°C precision (value / 10.0)- Base
TemperatureABC withto_celsius()andto_fahrenheit()methods from_fahrenheit()class methods for reverse conversions- Validator functions for Pydantic integration
- Centralized in new
temperature.pymodule - 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 conversiontou_status_to_python(): Time of Use status conversiontou_override_to_python(): TOU override status conversiondiv_10(): Divide by 10.0 utilityenum_validator(): Generic enum factory- Comprehensive documentation explaining device protocol quirks
- New
converters.pymodule replacing scattered validators
MQTT Event System: Structured event handling for MQTT operations
MqttClientEventsclass with type-safe event definitions- Feature monitoring and capability detection events
- Enhanced device capability monitoring in MQTT control module
- New
mqtt_events.pymodule 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.tomlwith strict mode forsrc/nwp500 - Integrated into tox lint environment and CI workflows
- Runs automatically with
make ci-lintorpython3 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
- New helper functions:
Comprehensive Protocol Documentation: Complete protocol reference documentation
- New
docs/protocol/quick_reference.rstwith 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
- New
MQTT Module Reorganization: Consolidated 9 separate modules into cohesive
mqttpackage# 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__.pywith clean public API exports - Backward compatibility maintained via main package exports
- All 209 tests pass with zero type checking errors
- Created
CLI Framework Migration: Migrated from argparse to Click framework
- Implemented
async_commanddecorator 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
- Implemented
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.mdwith 'Getting Started' guide and categorized index - Added 01-04 beginner series for smooth onboarding:
beginner/01_authentication.py- Basic authentication patternsbeginner/02_list_devices.py- Retrieving device informationbeginner/03_get_status.py- Getting device statusbeginner/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
- Created structured hierarchy in
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
- Complete rewrite of
Model Refactoring: Updated to use new converter modules
- Replaced 53 lines of scattered validators with imports
- Updated
fahrenheit_to_half_celsius()to useHalfCelsiusclass - 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.pyfor optional dependencies - Fixed type narrowing issues in
encoding.py - Updated
MqttConnectioncallback signature to useAwsCrtError - Added public properties and setters where needed for type checking
- Fixed datetime imports to use
- Superheat Temperature Units: Target and Current SuperHeat now correctly display in °F instead of °C
- Both fields use
DeciCelsiusToFconversion, now properly reflected in CLI output - Fields were displaying inconsistent units compared to all other temperature readings
- Both fields use
- Missing CLI Output Units: Multiple fields now display with proper units from model metadata
current_dhw_flow_rate: Now shows GPM unittotal_energy_capacity: Now shows Wh unitavailable_energy_capacity: Now shows Wh unitdr_override_status: Now shows hours unitvacation_day_setting: Now shows days unitvacation_day_elapsed: Now shows days unitanti_legionella_period: Fixed to show days unit (was incorrectly h)wifi_rssi: Now shows dBm unit
- Invalid MQTT Topic Filter: Fixed
reservations getcommand subscription topic- Changed invalid topic pattern
cmd/52/navilink-+/#to validcmd/52/+/# - AWS IoT Core MQTT does not support wildcards within topic segments
- Affected:
handle_get_reservations_request()in commands.py
- Changed invalid topic pattern
- DeviceFeature Documentation: Clarified field descriptions and fixed documentation errors
- Fixed
country_codedocumentation (actual value is 3, not 1 as previously noted) - Clarified
model_type_code,control_type_code,recirc_model_type_codefield purposes - Updated
volume_codeto use newVolumeCodeenum with validation
- Fixed
- 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
- 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_capabilitydecorator: 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.rstwith 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
- CLI Output: Numeric values in status output now rounded to one decimal place for better readability
MqttDeviceControllernow 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
.controlnamespace - Standardized periodic request patterns - Public API methodensure_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
- Type annotation consistency: Optional parameters now properly annotated as
type | Noneinstead oftype - 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
- Minor bug fixes and improvements
BREAKING CHANGES: - Minimum Python version raised to 3.13 - Enumerations refactored for type safety and consistency
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.UTCconstant
If you need Python 3.12 support, use version 6.1.x of this library.
CommandCode moved: Import from
nwp500.enumsinstead ofnwp500.constants# OLD (removed) from nwp500.constants import CommandCode # NEW from nwp500.enums import CommandCode # OR from nwp500 import CommandCode # Still works
- Python 3.12+ Optimizations: Leverage latest Python features
- PEP 695: New type parameter syntax (
def func[T](...)instead ofTypeVar) - Use
datetime.UTCconstant instead ofdatetime.timezone.utc - Native union syntax (
X | Yinstead ofUnion[X, Y]) - Cleaner generic type annotations throughout codebase
- PEP 695: New type parameter syntax (
- 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:
ErrorCodewith 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
- Status value enums:
- Command Code Constants: Migrated from
constants.pytoCommandCodeenum inenums.pyANTI_LEGIONELLA_ENABLE→CommandCode.ANTI_LEGIONELLA_ONANTI_LEGIONELLA_DISABLE→CommandCode.ANTI_LEGIONELLA_OFFTOU_ENABLE→CommandCode.TOU_ONTOU_DISABLE→CommandCode.TOU_OFFTOU_SETTINGS→CommandCode.TOU_RESERVATION- All command constants now use consistent naming in
CommandCodeenum
- Model Enumerations: Updated type annotations for clarity and type safety
TemperatureUnit→TemperatureType(matches device protocol field names)- All capability flags (e.g.,
power_use,dhw_use) now useCapabilityFlagtype MqttRequest.device_typenow acceptsUnion[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 valuesdocs/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 namesdocs/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: UsesCommandCodeenumexamples/device_feature_callback.py: Uses capability enumsexamples/event_emitter_demo.py: Uses status enumsexamples/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
- Temperature Conversion Test: Corrected
test_device_status_div10to useHalfCelsiusToFconversion (100 → 122°F, not 50.0) - Documentation: Fixed references to non-existent
OperationModeenum - replaced with correctDhwOperationSettingandCurrentOperationModeenums
- MQTT Diagnostics Module: New
MqttDiagnosticsCollectorfor 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.pydemonstrating usage patterns- Comprehensive documentation in
docs/MQTT_DIAGNOSTICS.rst - Exported from main package:
from nwp500 import MqttDiagnosticsCollector
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.
``build_reservation_entry()``: Now accepts
temperature_f(Fahrenheit) instead of rawparamvalue. 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)
- ``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.
``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
- 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).
- Maintenance Release: Version bump for PyPI release
- Documentation: Added TOU (Time-of-Use) enable/disable command payload formats to protocol documentation
- Field Descriptions: Added comprehensive Field descriptions to
DeviceStatusandDeviceFeaturemodels with full documentation details including units, ranges, and usage context
- Example Code: Fixed
device_status_callback.pyexample to use snake_case attribute names consistently - Field Descriptions: Clarified distinctions between similar fields:
dhw_temperature_settingvsdhw_target_temperature_settingdescriptionsfreeze_protection_tempdescriptions differ between DeviceStatus and DeviceFeatureeco_usedescriptions differ between DeviceStatus (current state) and DeviceFeature (capability)
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 sensortank_lower_temperature- Water tank lower sensordischarge_temperature- Compressor discharge temperature (refrigerant)suction_temperature- Compressor suction temperature (refrigerant)evaporator_temperature- Evaporator coil temperature (refrigerant)ambient_temperature- Ambient air temperature at heat pumptarget_super_heat- Target superheat setpointcurrent_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 toDeciCelsiusToF(÷10). The correct formula is(raw_value / 10.0) * 9/5 + 32.
- Documentation: Updated
data_conversions.rstanddevice_status.rstto reflect correctDeciCelsiusToFconversion for refrigerant circuit and tank temperature sensors
- 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
Add20validator withHalfCelsiusToFfor fields transmitted as half-degrees Celsius - Replaced
DeciCelsiusToFwithPentaCelsiusToFfor fields scaled by factor of 5 - Affects multiple temperature sensor readings for improved accuracy
- Replaced
- CLI Output Formatting: Fixed formatting issues in command-line interface output
- Documentation: Updated temperature conversion documentation to use precise 9/5 fraction instead of 1.8 approximation for clarity
- Test Coverage: Added
tests/test_models.pyto verify temperature conversion correctness
BREAKING CHANGES: Migration from custom dataclass-based models to Pydantic BaseModel implementations with automatic field validation and alias handling.
- Removed legacy dataclass implementations for models (DeviceInfo, Location, Device, FirmwareInfo, DeviceStatus, DeviceFeature, EnergyUsage*). All models now inherit from
NavienBaseModel(Pydantic). - Removed manual
from_dictconstructors relying on camelCase key mapping logic. - Removed field metadata conversion system (
meta()+apply_field_conversions()) in favor of PydanticBeforeValidatorpipeline.
- 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
DeviceBoolAnnotated 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
BeforeValidatorfunctions (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.
- Introduced
NavienBaseModelconfiguring alias generation, population by name, and ignoring unknown fields for forward compatibility. - Added structured Annotated types:
DeviceBool,Add20,Div10,DeciCelsiusToFfor declarative conversion definitions. - Added consistent default enum values directly in field declarations (e.g.
operation_mode=STANDBY).
- Replace any imports of dataclass models with Pydantic versions (paths unchanged). No code change required if you only accessed attributes.
- Remove calls to
Model.from_dict(data): Either useModel.model_validate(data)or continue callingfrom_dictwhere still provided (thin wrapper for backward compatibility on some classes). Preferred:DeviceStatus.model_validate(raw_payload). - 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 - Remove manual conversion code. Raw numeric values are converted automatically; stop adding +20 or dividing by 10 in user code.
- Stop performing boolean normalization (
value == 2) manually; attributes already return proper bools. - For enum handling, remove try/except wrappers; rely on defaulted fields (e.g.
operation_modedefaults toSTANDBY). - If you previously mutated raw payload keys to snake_case, eliminate that transformation step.
- If you logged intermediate converted dictionaries, you can access
model.model_dump()for a fully converted representation. - Replace any custom validation logic with Pydantic validators or continue using existing patterns; most prior validation code is now unnecessary.
- Energy usage: Access percentages via properties unchanged; object types now Pydantic models.
# 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- 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.
- DNS resolution in containerized environments using ThreadedResolver
- Updated AWS IoT library version
- Device status field conversions
- Refactored ThreadedResolver session creation into helper method
- Minor bug fixes and improvements
BREAKING CHANGES: Removed constructor callbacks and backward compatibility re-exports
Constructor Callbacks: Removed
on_connection_interruptedandon_connection_resumedconstructor parameters fromNavienMqttClient# 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_clientandauthmodules# 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.
- 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.pyto use event emitter pattern - Updated
examples/reconnection_demo.pyto use event emitter pattern - Updated
examples/device_status_callback.pyto import exceptions from correct module - Updated
examples/device_status_callback_debug.pyto import exceptions from correct module - Updated
examples/device_feature_callback.pyto import exceptions from correct module - Updated
examples/test_api_client.pyto import exceptions from correct module - Removed misleading "legacy state" comments from connection tracking code
- 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
- Maintenance Release: Removed deprecated backward compatibility code
- Removed
CMD_*backward compatibility aliases fromconstants.py - Removed
cli.pybackward compatibility wrapper module - Updated setup.cfg entry point to use
nwp500.cli.__main__:rundirectly - Updated all examples to use
CommandCodeenum instead ofCMD_*aliases - Updated examples to use standalone functions (
build_tou_period,encode_price,decode_price,encode_week_bitfield,decode_week_bitfield) instead ofNavienAPIClient.*static methods - Updated documentation to reference standalone functions
- Fixed deprecated method name (
set_dhw_operation_setting→set_dhw_mode) - Removed broken relative links from
README.rst - Added Read the Docs and GitHub links to
README.rstheader
- Removed
BREAKING CHANGES: This release introduces a comprehensive enterprise exception architecture. See migration guide below for details on updating your code.
- Enterprise Exception Architecture: Complete exception hierarchy for better error handling
- Created
exceptions.pymodule with comprehensive exception hierarchy - Added
Nwp500Erroras 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, andretriableattributes - 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
- Created
- 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")withMqttNotConnectedError - Replaced
ValueErrorin validation code withRangeValidationErrorandParameterValidationError - Replaced
ValueErrorfor credentials withMqttCredentialsError - Replaced
RuntimeErrorfor connection issues withMqttConnectionError - Enhanced
AwsCrtErrorwrapping in MQTT code with proper exception chaining - Moved authentication exceptions from
auth.pytoexceptions.py - Moved
APIErrorfromapi_client.pytoexceptions.py - CLI now handles specific exception types with better error messages and user guidance
- All exception wrapping now uses exception chaining (
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:
- Import new exception types:
from nwp500 import MqttNotConnectedError, MqttError, ValidationError - Replace
except RuntimeErrorwithexcept MqttNotConnectedErrorfor connection checks - Replace
except ValueErrorwithexcept ValidationErrorfor parameter validation - Use
except Nwp500Errorto catch all library errors - 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
- Token Restoration Support: Enable session persistence across application restarts
- Added
stored_tokensparameter toNavienAuthClient.__init__()for restoring saved tokens - Added
AuthTokens.to_dict()method for serializing tokens (includesissued_attimestamp) - 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.pydemonstrating save/restore workflow - Updated authentication documentation with token restoration guide
- Added
- Benefits: Reduces API load, improves startup time, prevents rate limiting for frequently restarting applications (e.g., Home Assistant)
- Patch Release: No code changes, updated version format to full semantic versioning
- 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_attemptsfrom 10 to -1 (unlimited retries) - Added
deep_reconnect_thresholdconfiguration parameter (default: 10) - Added
has_stored_credentialsproperty toNavienAuthClient - Added
re_authenticate()method toNavienAuthClientfor credential-based re-authentication - Added
resubscribe_all()method toMqttSubscriptionManagerfor 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
- Exception Handling: Replaced 25 catch-all exception handlers with specific exception types
mqtt_client.py: UsesAwsCrtError,AuthenticationError,TokenRefreshError,RuntimeError,ValueError,TypeError,AttributeErrormqtt_reconnection.py: UsesAwsCrtError,RuntimeError,ValueError,TypeErrormqtt_connection.py: UsesAwsCrtError,RuntimeError,ValueErrormqtt_subscriptions.py: UsesAwsCrtError,RuntimeError,TypeError,AttributeError,KeyError,ValueErrormqtt_periodic.py: UsesAwsCrtError,RuntimeErrorevents.py: RetainsExceptionfor 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 accessingAwsCrtError.nameattribute - 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
- MQTT Reconnection: Eliminated duplicate "Connection interrupted" log messages
- Removed duplicate logging from
mqtt_client.py(kept inmqtt_reconnection.py)
- Removed duplicate logging from
- MQTT Reconnection: Fixed MQTT reconnection failures due to expired AWS credentials
- Added AWS credential expiration tracking (
_aws_expires_atfield inAuthTokens) - Added
are_aws_credentials_expiredproperty 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
- Added AWS credential expiration tracking (
- MQTT Reconnection: Improved MQTT reconnection reliability with active reconnection
- Breaking Internal Change:
MqttReconnectionHandlernow requiresreconnect_funcparameter (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_failedevent 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 -
NavienMqttClientcontinues to work as before - Compatible with existing auto-recovery examples (
auto_recovery_example.py,simple_auto_recovery.py)
- Breaking Internal Change:
- 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
- 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
MqttConnectionfor proper type checking - Resolves mypy type checking errors and ensures AWS SDK compatibility
- Fixed E501 line length linting issue in connection interruption handler
- Updated callback to match latest AWS IoT SDK signature:
Breaking Changes
- REMOVED:
OperationModeenum has been removed- This enum was deprecated in v2.0.0 and has now been fully removed
- Use
DhwOperationSettingfor user-configured mode preferences (values 1-6) - Use
CurrentOperationModefor 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
DhwOperationSettingandCurrentOperationMode
- Removed
Breaking Changes (Planned for v3.0.0)
- DEPRECATION:
OperationModeenum is deprecated and will be removed in v3.0.0- Use
DhwOperationSettingfor user-configured mode preferences (values 1-6) - Use
CurrentOperationModefor real-time operational states (values 0, 32, 64, 96) - See
MIGRATION.mdfor detailed migration guide
- Use
- Enhanced Type Safety: Split
OperationModeinto semantically distinct enumsDhwOperationSetting: 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 guidanceMIGRATION.mdwith 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.rstupdated with new enum types- Code examples use new enums with proper imports
- Clear distinction between configuration vs real-time status
- DeviceStatus Model: Updated to use specific enum types
operationModefield now usesCurrentOperationModetypedhwOperationSettingfield now usesDhwOperationSettingtype- Maintains backward compatibility through value preservation
- Example Scripts: Updated to demonstrate new enum usage
event_emitter_demo.pyupdated to useCurrentOperationMode- Fixed incorrect enum references (HEAT_PUMP_ONLY → HEAT_PUMP_MODE)
- All examples remain functional with new type system
- 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
- Release version 1.2.2
- 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.pyandscripts/format.pymirrortox -e lintandtox -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
EventEmitterbase 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
NavienAuthClientnow requiresuser_idandpasswordin 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_countproperty for monitoringclear_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
MqttConnectionConfigwith 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 intoDeviceStatusobjectssubscribe_device_feature()- Automatic parsing of feature messages intoDeviceFeatureobjectssubscribe_energy_usage()- Automatic parsing of energy usage responses intoEnergyUsageResponseobjects- 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)EnergyUsageResponsedataclass with daily breakdownEnergyUsageTotalwith percentage calculationsMonthlyEnergyDatawith per-day access methodsEnergyUsageDatafor 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
DeviceStatusdataclass with 125 sensor and operational fieldsDeviceFeaturedataclass with 46 capability and configuration fieldsEnergyUsageResponsedataclass for historical energy dataEnergyUsageTotalwith aggregated statistics and percentagesMonthlyEnergyDatawith daily breakdown per monthEnergyUsageDatafor individual day/month metricsOperationModeenum including STANDBY state (value 0)TemperatureUnitenum (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 callbacksdevice_feature_callback.py- Device capabilities and firmware infocombined_callbacks.py- Both status and feature callbacks togethermqtt_client_example.py- Complete MQTT usage demonstrationenergy_usage_example.py- Historical energy usage monitoring and analysisreconnection_demo.py- MQTT automatic reconnection demonstrationauth_constructor_example.py- Simplified authentication pattern
- 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 ofDict[str, Any] - Removed
typing.Dict,typing.List,typing.Dequeimports - 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:
NavienAuthClientconstructor signature- Now requires
user_idandpasswordas first parameters - Old:
NavienAuthClient()thenawait 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
- Now requires
- 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)
- Critical Bug: Thread-safe reconnection task creation from MQTT callbacks
- Fixed
RuntimeError: no running event loopwhen 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
- Fixed
- 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
- Fixed
- Bug: Incorrect method parameter passing in temperature control
- Fixed
set_dhw_temperature_display()callingset_dhw_temperature()with wrong parameters - Was passing individual parameters (
device_id,device_type,additional_value) - Now correctly passes
Deviceobject as expected by method signature - Simplified implementation to just calculate offset and delegate to base method
- Updated docstrings to match actual method signatures
- Fixed
- 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
- Replaced all occurrences of real MAC address (
- 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
- Fixed
- Bug: Missing
OperationMode.STANDBYenum value- Added
STANDBY = 0toOperationModeenum - Device reports mode 0 when tank is fully charged and no heating is needed
- Added graceful fallback for unknown enum values
- Prevents
ValueErrorwhen parsing device status
- Added
- 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
- Added try/except blocks for all enum conversions in
- Documentation: Updated MQTT_MESSAGES.rst with correct command codes and temperature offset
- 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
- Initial Documentation