Changelog¶
Unreleased¶
Version 9.3.2 (2026-09-01)¶
Fixed¶
A null ``errorCode`` no longer makes ``/device/list`` unparseable. The cloud returns
"error": {"errorCode": null}on some devices.DeviceErrorSummarytyped the fieldErrorCode | int, so validation of the whole listing failed and every caller - including the Home Assistant integration, which could no longer set up - lost the device entirely.error_codeis nowErrorCode | int | Noneand defaults toNone: a null means the cloud reported no code, which is not the same claim asNO_ERROR. Callers that treated the field as always-present should handleNone.
Security¶
MAC address no longer logged in the clear when a reservations unsubscribe fails.
fetch_reservationsloggeddevice_info.mac_addressunredacted in its cleanup warning, while the identical warning inupdate_reservations_confirmeda few lines below passed it throughredact_mac. CodeQL flagged it aspy/clear-text-logging-sensitive-data(high). Both call sites now redact.
Version 9.3.1 (2026-08-29)¶
NavienMqttClient.request_tou_settings() is removed. Not a breaking
change in practice: the device never answered it, so no caller can have
been getting a schedule out of it. Code that still names the method will
now fail with AttributeError rather than time out. See Removed below.
Added¶
REST fields the cloud added since the models were written.
/device/listnow returns anerrorblock (errorCode,errorOccuredTime) and thedescalingblock previously seen only on/device/info, anddeviceInfogainedmodelTypeCodeandinstallerId. None of these were modelled, andNavienBaseModelignores unknown keys, so all of them were silently discarded.Devicegains optionalerror(DeviceErrorSummary) anddescaling(DescalingInfo) sections, andDeviceInfogainsmodel_type_codeandinstaller_id.errormakes the device’s last recorded fault readable without an MQTT connection, including while the device is offline. Every new field is optional, so responses that omit them - such as/device/info, which carries noerrorblock - parse unchanged.error_codeis typedErrorCode | int(validated left to right, so a known code becomes anErrorCodemember and only an unknown one stays a plain int) so an unrecognised code cannot make a whole listing unparseable.docs/openapi.yamlis updated to match.
Removed¶
``request_tou_settings()`` removed - the device has no MQTT read for its TOU schedule. The method published a
CommandCode.TOU_RESERVATIONmessage toctrl/tou/rdcarrying onlycontrollerSerialNumber, then waited for a reply onres/tou/rd. No reply ever comes: a live device with TOU provisioned (program_reservation_usetrue, a valid controller serial, and a plan the REST API returns in full) stayed silent for 45 seconds.ctrl/tou/rdwith that command code is the write - it is whatconfigure_tou_schedule()publishes and what the vendor app publishes from its TOU editor (TouScheduleViewmodel.setPublishMgppControlTou) - and the device answers onres/tou/rdonly to confirm such a write. The vendor app reads TOU over REST, which is the only TOU read the protocol has.Beyond returning nothing, the call published a write-shaped command with no schedule attached. This device ignored it -
touStatuswas unchanged across repeated calls - but a firmware that took it at face value could read it as “store an empty TOU schedule”.Migration: use
get_tou_info(), which returns the stored plan - rate name, utility, ZIP code and the seasonal pricing intervals. The read itself is pure REST; it is keyed by the controller serial number, which only the MQTT device-info response publishes, so fetch that once and cache it.subscribe_tou_response()is unaffected and still delivers write confirmations. Enabling and disabling TOU (set_tou_enabled(), command codesTOU_ON/TOU_OFF) is a separate path and is unaffected.
Fixed¶
CLI ``energy –months`` no longer duplicates ``–month`` output.
handle_get_energy_requestpicked the view from the length of the month list, so--months 5took the single-month branch and printed the same daily breakdown as--month 5instead of the monthly summary. The handler now takes an explicitdailyflag set by the option the user actually passed.energyalso rejects--monthand--monthstogether rather than silently preferring--month, and validates the--monthsrange and format the way--monthalready did. All of that validation now happens while Click parses the arguments, so a bad invocation is a usage error (exit code 2) before any authentication or MQTT connection is attempted - previously the command body raisedClickExceptionafter connecting, whereasync_command’s catch-all reported it as an “Unexpected Error” with a traceback, and anyone without working credentials saw an authentication failure instead of the usage error. That catch-all now re-raisesClickExceptionso any command can report a usage error as itself.``DeviceFeature.mixing_valve_use`` now reads the device’s actual key. The field was aliased to
mixingValveUse, but the device sendsmixingValueUse- “Value”, not “Valve” - and the base model ignores unknown keys, so the field silently reported itsFalsedefault on every device regardless of whether a thermostatic mixing valve was supported. The CLI’s “Mixing Valve” capability row was wrong for the same reason. The misspelling is Navien’s own: it appears in the vendor app’sKDResponseMgppDid$Featureclass and indocs/reference/protocol/device_features.rst, which documented the wire spelling correctly all along.``firmware_payload_capture.py`` captured almost nothing. Its wildcards covered
cmd/{type}/{client_id}/res/#and the event topic, but the device acknowledges control and query commands oncmd/{type}/navilink-{mac}/{client_id}/res- which neither pattern matches - so the device status and device info responses were missed, along with every request published on the device path. A capture run recorded 2 payloads where it should have recorded 11. It now also subscribes tocmd/{type}/navilink-{mac}/#, which additionally picks up traffic from other clients on the same device. Query results the device routes back to a client-keyed topic, such as reservations and energy usage, were already covered by the existing response wildcard.
Version 9.3.0 (2026-08-03)¶
BREAKING CHANGES: two independent corrections land together.
Tank energy values were wrong in two ways - a 2.5x unit-scale error and two actively misleading field names - and both are corrected. Reported tank energy is now 2.5x smaller and two public field names are removed.
Separately, eight status flags change type from bool to
bool | None so the device’s “unknown” state is no longer reported as
a definite OFF.
Changed¶
Energy unit scale corrected.
totalEnergyCapacityandavailableEnergyCapacitywere scaled by 10 on the assumption the device reported 10 Wh units. It does not. BecausetotalEnergyCapacityis a whole-tank quantity, its slope against the setpoint measures the quantum with no stratification assumption: on a 65-gallon NWP500 that is 70.25 raw counts per Kelvin. The field turns out to be bimodal - at a fixed setpoint it takes one of two values exactly 2 degC apart - but both branches give the same slope to within 0.2%, so the quantum is unaffected. Converting to Watt-hours needs a water mass, and a “65 gallon” tank does not hold 65 gallons - so taking the quantum to be round, as every other conversion in this protocol is, 4 Wh/count is the only candidate implying a water volume below the nameplate (241.7 L). Two further checks agree: 183 individual heating recoveries give 4.11 Wh/count by a noisier route, and integratingcurrentInstPowerover them implies a heat-pump COP of 2.89 at the new scale against 7.02 at the old, the latter being physically impossible. Reported tank energy is now 2.5x smaller. Historical series logged from earlier versions need rescaling by 0.4 to be comparable.Energy fields renamed.
availableEnergyCapacityis not available energy - it is the energy still needed to reach the setpoint. It falls as the tank heats and reaches zero when the tank is fully charged, so code treating it as stored energy had the signal backwards (regression against mean tank temperature: negative slope, R-squared 0.93, zero crossing at the setpoint). LikewisetotalEnergyCapacityis not a fixed tank size but the cost of a full recovery to the current setpoint, measured from the device’s own minimum setpoint of 104.9 degF; it moves by about 140 Wh per 0.5 degC of setpoint change.# OLD (removed) status.total_energy_capacity # 15800.0 status.available_energy_capacity # 11660.0 # NEW status.full_recovery_energy # 6320.0 status.energy_to_setpoint # 4664.0
The protocol field names on the wire are unchanged. CLI rows are relabelled from “Total Capacity”/”Available Capacity” to “Full Recovery”/”Energy to Setpoint”.
Status flags now preserve the device’s unknown state. The protocol encodes these flags as
0 = unknown, 1 = OFF, 2 = ON, and the library was collapsing 0 toFalse- inventing an OFF the device never claimed. Confirmed against Navien’s own NaviLink app (2.03.00, versionCode 141), which decodes exactly this set of fields through an enum declaredUNKNOWN(0), OFF(1), ON(2); two sibling enums render their zero as"-"and"Not Applied"rather than as an off state.Affected:
operation_busy,comp_use,anti_legionella_use,anti_legionella_operation_busy,heat_upper_use,heat_lower_use,air_filter_alarm_use,recirc_reservation_use.Noneis falsy, soif status.comp_use:is unaffected. Code that distinguishesis Falsefrom “not reported”, or does arithmetic or formatting on these fields, needs aNonecheck. For Home Assistant this is the wanted shape:Nonerenders as “Unknown” instead of writing a fabricated OFF into the recorder database.OnOffFlaggains the vendor’sUNKNOWN = 0member. It previously started atOFF = 1, leaving the device’s reserved value unrepresented.The CLI renders these flags as
Unknownrather thanNo. Both affected rows are updated: “Busy” under OPERATION STATUS and “Operation Busy” under ANTI-LEGIONELLA.
Added¶
``DeviceStatus.usable_energy``: drawable energy in Watt-hours, computed as
full_recovery_energy - energy_to_setpoint. Both raw fields are measured from the setpoint, so neither is a state of charge; subtracting them cancels the setpoint and leaves the tank’s heat above the device’s minimum operating temperature (104.9 degF), which is about the lowest temperature usable for a shower. Robust despitefull_recovery_energybeing bimodal, since both fields shift together: the implied tank temperature tracks the thermistor mean to a standard deviation of 0.57 degF over 12275 samples. Rendered by the CLI as “Usable Energy”.converters.device_tristate_to_pythonandmodels.status.DeviceTriStatefor flags the device may decline to report.converters.device_bool_to_pythonis unchanged and remains correct for capability flags.New
docs/explanation/unknown-values.rstrecording which field families use 0 as a sentinel and which do not, with the app evidence for each.
Removed¶
Misnamed energy fields: removed
DeviceStatus.total_energy_capacityandDeviceStatus.available_energy_capacityoutright rather than aliasing them, so a missed rename fails withAttributeErrorinstead of silently returning a number 2.5x too large. Usefull_recovery_energyandenergy_to_setpoint.Incorrect converter: removed
converters.mul_10, which existed only to apply the wrong 10 Wh scale. Useconverters.energy_count_to_whandconverters.WH_PER_ENERGY_COUNT.
Fixed¶
Documentation contradicted itself and the code on energy capacity. Three incompatible descriptions were published: Watt-hours with no conversion (protocol reference), Watt-hours with a x10 scale (the code), and a 0-100 percentage (
how-to/track-energy.rst,reference/python_api/models.rst,project/history.rst). The percentage claim was never true of any library version. All are now consistent.how-to/track-energy.rstdocumented four fields that do not exist onDeviceStatus(dhw_tank_upper_temp,dhw_tank_lower_temp,comp_temp,dhw_heatex_out_temp); replaced with the real names.dhwTemperatureis documented as an outlet temperature but is measured inside the tank: it trackstankUpperTemperatureto within one 0.5 degC step, and the device has no sensor downstream of itself. Navien’s own app agrees, labelling it “DHW Temp.” beside the tank thermistors and showingdischargeTemperatureseparately.docs/explanation/tank-energy.rsttabulatedtotalEnergyCapacityandavailableEnergyCapacityat ten times their raw wire values, under column headings naming the raw protocol fields. The series had been logged through the pre-fix library, which multiplied by 10. The prose beside the tables (“70.25 raw counts per Kelvin”, “35 counts”) was already in true raw counts, so the page contradicted itself by exactly the factor it exists to correct. Tables are now in raw counts; 1580 counts at a 145.4 degF setpoint matches the test fixtures and the 6320 Wh the CLI reports. No numeric conclusion changes.docs/explanation/tank-energy.rstreferred to a fieldhpUpperOnTemperatureSetting; the protocol name ishpUpperOnTempSetting.The setpoint-per-0.5-degC figure is 140 Wh, not 143 Wh, which is what the documented slope of 70.25 counts/K gives at 4 Wh/count. Corrected in the changelog, the model field description and the protocol reference.
New
docs/explanation/tank-energy.rstrecords what the two fields actually measure and the calibration evidence behind the scale change, including that Navien’s own NaviLink app (2.03.00, versionCode 141) reads neither field, so no vendor-side corroboration exists.Documented that temperature fields carry no sentinel at all. The app has no out-of-band constant (no
0xFFFF/-999/-1), no zero-guard in any display path, and formats whatever arrives - so a temperature of zero means zero. This closes a recurring source of bugs where zero-as-none was applied to temperature converters and reported working sensors as missing during cold-weather operation.Documented that capability flags are a distinct case: the app hides a feature’s entire UI when its DID
Useflag reads 0, so 0 there means “not fitted” and the existingboolmapping is correct.
Version 9.2.1 (2026-07-30)¶
Added¶
Confirmed reservation/TOU write helpers and canonical schedule comparison (#111):
update_reservationsandconfigure_tou_schedulewere fire-and-forget, returning only an MQTT publish packet id with no way to confirm the device applied the write. Newnwp500.reservations.update_reservations_confirmed()andnwp500.tou_schedule.configure_tou_schedule_confirmed()(also exported from the top-levelnwp500package) send the write and await the device’srsv/rd/tou/rdecho, returning the parsed schedule the device now holds (orNoneon timeout). Because the protocol has no request/response correlation id on these topics, a response is only accepted once it matches the content that was just written, so a stale/unrelated response from a concurrent read or a previous write can’t be mistaken for confirmation. Also addedReservationEntry.canonical_key()/ReservationSchedule.canonical()and theTOUPeriod/TOUReservationScheduleequivalents: a stable, order-independent, hashable representation of the raw protocol fields so a desired program can be compared against a device read-back withdesired.canonical() == confirmed.canonical()instead of hand-diffing.
Fixed¶
Freeze protection temperature now validated against device limits (#112):
set_freeze_protection_temperaturedocumented a 35-45°F valid range but sent the value straight through with no validation. It now fetches device features and validates againstfreeze_protection_temp_min/freeze_protection_temp_max(raisingRangeValidationErrorif out of range), matching the pattern already used byset_dhw_temperature. The command is also now gated behind a newfreeze_protection_usecapability (added toMqttDeviceCapabilityChecker._CAPABILITY_MAPand enforced with@requires_capability), so it’s no longer dispatched to devices that don’t support freeze protection at all.Demand response commands gated on the dr_setting_use capability (#114):
enable_demand_response/disable_demand_responsewere dispatched unconditionally even though thedr_setting_usecapability flag exists onDeviceFeature. Every other controllable feature follows flag +_CAPABILITY_MAPentry +@requires_capabilitydecorator; DR was the one gap. Added the missing map entry and decorators so DR now follows the same pattern.
Documentation¶
decode_reservation_hex docstring corrected (#113): the
parambyte was documented as “temperature offset by 20°F”, contradicting the half-degrees-Celsius encoding used consistently everywhere else (models/schedule.py,models/_converters.py). Corrected to “target temperature, half-degrees Celsius; °C = param/2”, and fixed the accompanying doctest-style example, which showed aminutekey where the function actually returnsmin.
Version 9.2.0 (2026-07-06)¶
Changed¶
CLI formatting stacks merged; Rich is the sole human renderer (#100): a new
src/nwp500/cli/presentation.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)
Fixed¶
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.
Documentation¶
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.
Added¶
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.
Version 9.0.0 (2026-07-05)¶
BREAKING CHANGES: Public API surface trimmed and dead code removed. These changes require a major version bump.
Modernization (Python 3.14)¶
Removed
from __future__ import 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.
Testing¶
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).
Removed¶
Deprecated ``.control`` property: removed
NavienMqttClient.control(deprecated shim slated for v9.0.0; the project policy is to remove rather than deprecate). Use the delegated methods on the client directly:# OLD (removed) await client.control.set_power(device, True) # NEW await client.set_power(device, True)
Unused exception classes: removed
TokenExpiredError,MqttSubscriptionError,DeviceNotFoundError,DeviceOfflineError, 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.
Changed¶
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.
Bug Fixes¶
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().
Improvements¶
MQTT operation acknowledgement timeouts: connect, publish, subscribe, unsubscribe, and disconnect acknowledgements are now awaited with a timeout (
MqttConnectionConfig.operation_timeout, default 30 seconds). Previously a half-open TCP connection could hang callers until the 20-minute keep-alive expired.Reconnection backoff jitter: reconnect delays are now randomized (±50%, capped at
max_reconnect_delay) so fleets of clients disconnected simultaneously (e.g. the AWS IoT 24-hour disconnect) no longer reconnect in synchronized waves.
Security¶
Restrict token cache file permissions:
~/.nwp500_tokens.json(refresh token and AWS credentials) was written world-readable. It is now created with mode0600, and existing files are tightened on save.
Version 8.1.3 (2026-06-15)¶
Bug Fixes¶
Fix MQTT reconnection storm caused by non-thread-safe Task.cancel(): The
on_connection_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.
Version 8.1.0 (2026-05-16)¶
Bug Fixes¶
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.
Version 8.0.0 (2026-05-13)¶
Bug Fixes¶
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)
Features¶
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.
BREAKING CHANGES: .on() event handler callbacks now receive a single typed
event dataclass instead of positional arguments. MqttDeviceController is no longer
accessible as .control on NavienMqttClient; all control methods are now
available directly on the client.
Breaking Changes¶
Typed event payloads: All
.on()event handler callbacks now receive a single typed event dataclass instance. The dataclasses are exported 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)
Migration Guide (from 7.x.x)¶
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.
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.
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).
Added¶
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.
Changed¶
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.
Version 7.4.10 (2026-04-13)¶
Changed¶
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.
Version 7.4.9 (2026-04-12)¶
Added¶
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.
Fixed¶
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.
Changed¶
Dependency updates: Bumped minimum versions to track current releases:
aiohttp >= 3.13.5,pydantic >= 2.12.5,click >= 8.3.0,rich >= 14.3.0.Dependency: awsiotsdk >= 1.28.2: Bumped minimum
awsiotsdkversion from>=1.27.0to>=1.28.2to track the current patch release.awscrt0.31.3 is pulled in transitively.
Version 7.4.8 (2026-02-17)¶
Added¶
Reservation CRUD Helpers: New public functions
fetch_reservations(),add_reservation(),delete_reservation(), 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.
Version 7.4.7 (2026-02-17)¶
Added¶
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.
Changed¶
``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.
Fixed¶
Week Bitfield Encoding (CRITICAL): Fixed MGPP week bitfield encoding to match NaviLink APK protocol. Sunday is now correctly bit 7 (128), Monday bit 6 (64), …, Saturday bit 1 (2); bit 0 is unused. Affects all reservation and TOU schedule operations. Verified against reference captures.
Enable/Disable Convention: Fixed reservation and TOU enable/disable flags to use standard device boolean convention (1=OFF, 2=ON) instead of inverted logic. This aligns with other device binary sensors and matches app behavior. Global reservation status now correctly shows DISABLED when
reservationUse=1.Reservation Set Command Timeout: Fixed
reservations 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.
Version 7.4.6 (2026-02-13)¶
Fixed¶
Converter Consistency:
div_10()andmul_10()now correctly apply division/multiplication to all input types afterfloat()conversion, not justint/floattypesReservation Decoding: Fixed
decode_reservation_hex()to validate chunk length before checking for empty entries, preventing potential out-of-bounds accessFactory Cleanup:
create_navien_clients()now properly cleans up auth session if authentication fails during context manager entryMQTT 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 completionDuplicate Handlers: Subscription manager now prevents duplicate callback registration for the same topic
Command Queue:
MqttCommandQueuenow raises onQueueFullinstead of silently swallowing the errorFlow Rate Metadata: Removed hardcoded
"GPM"unit fromrecirc_dhw_flow_ratefield; unit is now dynamic based on unit systemTemperature Rounding:
RawCelsiusFahrenheit conversion now uses a catch-all default for standard rounding instead of matching onlySTANDARDenum valueUnit System Default:
is_metric_preferred()now returnsFalse(Fahrenheit) instead ofNonewhen no unit system override or context is set
Security¶
Sensitive Data Logging: Redacted MQTT topics in subscription manager logging to prevent leaking device IDs (resolves CodeQL alerts)
Added¶
Auth Session Property: Added
NavienAuthClient.sessionproperty to access the activeaiohttpsession without usinggetattrUnsubscribe Feature: Added
unsubscribe_device_feature()method to MQTT client and subscription manager for targeted callback removalHypothesis Fuzzing: Added property-based fuzzing tests for MQTT payload handling
Bandit Security Scanning: Added bandit configuration for security analysis in CI
Version 7.4.5 (2026-02-04)¶
Fixed¶
Energy Capacity Unit Scaling: Corrected unit scaling for energy capacity fields that were off by a factor of 10
CLI Output: Fixed linting issue by replacing str and Enum with StrEnum for InstallType
Version 7.3.4 (2026-01-27)¶
Fixed¶
Temperature Delta Conversions: Fixed incorrect Fahrenheit conversion for differential temperature settings (heat pump and heater element on/off thresholds)
Created new
DeciCelsiusDeltaclass for temperature deltas that apply scale factor (9/5) but NOT the +32 offsetHeat pump and heater element differential settings now use
DeciCelsiusDeltainstead ofDeciCelsiushp_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 FahrenheitExample: A device value of 5 (representing 0.5°C delta) now correctly converts to 0.9°F delta instead of 32.9°F
CLI Output: Added display of heat pump and heater element differential temperature settings in device status output
Changed¶
Internal API: Added
div_10_celsius_delta_to_preferredconverter for temperature delta values in device models
Version 7.3.3 (2026-01-27)¶
Breaking Changes¶
Temperature Setpoint Limits: Replaced hardcoded temperature limits with device-provided values
set_dhw_temperature()now validates against device-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 overridesTemperature parameters now accept values in the user’s preferred unit (Celsius or Fahrenheit) based on global unit system context
Fixes Home Assistant and other integrations that prefer Celsius unit display
Migration guide:
# OLD (hardcoded 95-150°F) await mqtt.set_dhw_temperature(device, temperature_f=140.0) entry = build_reservation_entry( enabled=True, days=["Monday"], hour=6, minute=0, mode_id=3, temperature_f=140.0, ) # NEW (device-provided limits, unit-aware) # Temperature value automatically uses user's preferred unit await mqtt.set_dhw_temperature(device, 140.0) # Device features provide min/max in user's preferred unit features = await device_info_cache.get(device.device_info.mac_address) entry = build_reservation_entry( enabled=True, days=["Monday"], hour=6, minute=0, mode_id=3, temperature=140.0, temperature_min=features.dhw_temperature_min, temperature_max=features.dhw_temperature_max, )
Added¶
Reservation Temperature Conversion: New
reservation_param_to_preferred()utility function for unit-aware reservation displayConverts 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 functionConverts 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 pathsUsed internally by
set_dhw_temperature()andbuild_reservation_entry()
Changed¶
Unit System Agnostic Display: All logging and user-facing messages now respect global unit system context
Temperature change logs dynamically show °C or °F based on user preference
CLI monitoring and temperature setting messages use correct unit suffix
Event listener documentation updated with unit-aware examples
Reservation schedule examples now use
reservation_param_to_preferred()for proper unit handling
Fixed¶
Critical: Temperature Unit Bug in Set Operations: Fixed incorrect temperature conversion when setting DHW temperature and reservations
set_dhw_temperature()was callingfahrenheit_to_half_celsius()with unit-agnostic temperature parameterbuild_reservation_entry()had the same issueImpact: If user preferred Celsius, temperature would be interpreted as Fahrenheit, causing wrong setpoints
Fix: Use new
preferred_to_half_celsius()that respects unit system contextThis was a critical data correctness bug that would cause incorrect device behavior for Celsius users
Version 7.3.2 (2026-01-25)¶
Changed¶
Unit System Consistency: Removed deprecated
"imperial"unit system identifier in favor of"us_customary"for Home Assistant compatibilityChanged all references from
UnitSystem.IMPERIALtoUnitSystem.US_CUSTOMARYUpdated
set_unit_system()and related functions to use"us_customary"exclusively"metric"remains unchanged for Metric conversionsAffects API client, MQTT client, and CLI unit system handling
Improves consistency with Home Assistant naming conventions
Fixed¶
Unit System Consolidation: Consolidated duplicate
UnitSystemTypetype alias definitions into single canonical definition inunit_system.pyRemoved redundant type definitions from multiple modules
Improved code maintainability and consistency
All unit system operations now reference centralized type definition
Version 7.3.1 (2026-01-25)¶
Fixed¶
MQTT Client Initialization: Removed overly strict token validity check from
NavienMqttClient.__init__()The strict token validity check prevented creating MQTT clients with restored tokens that may have expired between application restarts
However,
NavienMqttClient.connect()already handles token refresh automatically, making the check redundantThis change allows integrations to create MQTT clients with expired tokens and let connect() handle validation
Simplifies application code by removing duplicate token refresh calls
Enables proper handling of restored authentication sessions
Fixes MQTT connection failures when using stored tokens across application restarts
Version 7.3.0 (2026-01-19)¶
Added¶
Dynamic Unit Conversion: All temperature, flow, and volume measurements now dynamically convert based on user’s region preference (Metric/Imperial)
Temperature fields convert between Celsius and Fahrenheit using standard formulas
Flow rate fields convert between LPM (Liters Per Minute) and GPM (Gallons Per Minute)
Volume fields convert between Liters and Gallons
Available devices and regions determine conversion support (validated at runtime)
get_field_unit()method retrieves the correct unit suffix for any 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_currentVolume:
tank_volumeand related storage fields
Integration patterns for Home Assistant, CLI, and custom integrations documented
Unit System Override: Allow applications and CLI users to override the device’s temperature preference and explicitly specify Metric or Imperial units
Library-level: Add optional
unit_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
Fixed¶
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 contextResult: Values and units now correctly convert when
--unit-systemoverride is specifiedTesting: All 393 tests pass including new unit system context override tests
Type Annotation Quotes: Removed unnecessary quoted type annotations (UP037 violations)
With
from __future__ import annotations, explicit string quotes are redundantUpdated
from_dict()methods inUserInfo,AuthTokens,AuthenticationResponse,DeviceStatus,DeviceFeature, andEnergyUsageResponseImproves code clarity and passes modern linting standards
Version 7.2.3 (2026-01-15)¶
Added¶
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
Fixed¶
Documentation: Fixed all warnings and broken cross-references in documentation
Fixed docstring formatting in field_factory.py module
Fixed broken cross-reference links in enumerations.rst, mqtt_diagnostics.rst, cli.rst, and models.rst
Fixed invalid JSON syntax in code examples (removed invalid […] and … tokens)
Suppressed duplicate object description warnings from re-exported classes
CLI Documentation: Updated documentation for all 19 CLI commands
Added missing device-info command documentation
Added –raw flag documentation for status, info, and device-info commands
Added –month option documentation to energy command
Clarified mutually exclusive options (–months vs –month)
RST Title Hierarchy: Fixed title level inconsistencies in device_control.rst
Read the Docs Configuration: Updated Python version requirement to 3.13 in Read the Docs config
CI Test Failures: Fixed
ModuleNotFoundErrorwhen running tests without CLI dependencies installedWrapped CLI module imports in try-except blocks in test modules
Tests are skipped gracefully when optional dependencies (click, rich) are not installed
Allows pytest to run without CLI extra, while supporting full test suite with tox
Network errors in authentication are now marked as retriable for better resilience
Changed¶
Installation Documentation: Updated installation instructions to clarify optional CLI and Rich dependencies
Version 7.2.2 (2025-12-25)¶
Fixed¶
TOU Status Always Showing False: Fixed
touStatusfield always reportingFalseregardless of actual device stateRoot cause: Version 7.2.1 incorrectly changed
touStatusto use device-specific 1/2 encoding, but the device uses standard 0/1 encodingSolution: Use Python’s built-in
bool()fortouStatusfield (handles 0=False, 1=True naturally)Updated documentation in
docs/protocol/quick_reference.rstto notetouStatusexceptionAdded tests verifying built-in
bool()handles 0/1 encoding correctlyDevice encoding: 0=OFF/disabled, 1=ON/enabled (standard Python truthiness)
Version 7.2.1 (2025-12-25)¶
Added¶
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 --raw
ConnectionStatus Enum: New
ConnectionStatusenum for device cloud connection stateConnectionStatus.DISCONNECTED= 1 - Device offline/not connectedConnectionStatus.CONNECTED= 2 - Device online and reachableUsed in
DeviceInfo.connectedfield with automatic validation
InstallType Enum: New
InstallTypeenum for device installation classificationInstallType.RESIDENTIAL= “R” - Residential useInstallType.COMMERCIAL= “C” - Commercial useUsed in
DeviceInfo.install_typefield with automatic validationIncludes
INSTALL_TYPE_TEXTmapping for display purposes
String Enum Validator: New
str_enum_validator()converter for string-based enums
Changed¶
DeviceInfo Model: -
connectedfield now usesConnectionStatusenum instead of plain int -install_typefield now usesInstallTypeenum instead of plain stringTOU 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) commandsType Annotations: Fixed CLI rich_output console type annotation to declare at class level
Removed¶
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
Version 7.2.0 (2025-12-23)¶
BREAKING CHANGES: Class names renamed for consistency with MQTT-specific functionality
Removed¶
Renamed Classes: Updated class names to clarify MQTT-specific implementations
# OLD (removed) from nwp500 import DeviceCapabilityChecker, DeviceInfoCache # NEW from nwp500 import MqttDeviceCapabilityChecker, MqttDeviceInfoCache
Rationale: The original names were too generic. These classes are specifically designed for MQTT client functionality (auto-fetching device info, caching, capability checking). The new names make it clear they’re MQTT-specific implementations, leaving room for future REST API versions if needed.
Migration: Simple find-and-replace:
DeviceCapabilityChecker→MqttDeviceCapabilityCheckerDeviceInfoCache→MqttDeviceInfoCache
All functionality remains identical - only the class names changed.
Added¶
Factory Function: New
create_navien_clients()factory for streamlined client initialization# Create both API and MQTT clients in one call from nwp500 import create_navien_clients async with create_navien_clients(email, password) as (api_client, mqtt_client): devices = await api_client.get_devices() await mqtt_client.connect() # Both clients ready to use
Automatic auth client management (created internally, shared by both clients)
Simplified initialization for common use case (API + MQTT)
Proper async context manager support
Reduces boilerplate in application code
Comprehensive documentation in
docs/guides/authentication.rstExample:
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 validationExported 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()methodsfrom_fahrenheit()class methods for reverse conversionsValidator functions for Pydantic integration
Centralized in new
temperature.pymoduleBetter 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 factoryComprehensive 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 definitionsFeature monitoring and capability detection events
Enhanced device capability monitoring in MQTT control module
New
mqtt_events.pymodule for event infrastructureImproved 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/nwp500Integrated into tox lint environment and CI workflows
Runs automatically with
make ci-lintorpython3 scripts/lint.pyAll source code now passes strict type checking (0 errors)
Improved type annotations across codebase
Dynamic Unit Extraction in CLI: CLI output now dynamically extracts units from DeviceStatus model metadata
New helper functions:
_get_unit_suffix()and_add_numeric_item()Eliminates hardcoded units in output formatter
Single source of truth: model metadata drives CLI display
Comprehensive Protocol Documentation: Complete protocol reference documentation
New
docs/protocol/quick_reference.rstwith command codes, field formats, and conversionsConverted protocol documentation to RST format for Sphinx integration
Added protocol reference links in source code comments
Improved cross-referencing between code and documentation
Changed¶
MQTT Module Reorganization: Consolidated 9 separate modules into cohesive
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 submodulesBetter package organization and structure
Clearer public vs internal APIs
New
mqtt/__init__.pywith clean public API exportsBackward compatibility maintained via main package exports
All 209 tests pass with zero type checking errors
CLI Framework Migration: Migrated from argparse to Click framework
Implemented
async_commanddecorator for automatic loop and connection managementAdded 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.pyReorganized CLI handlers into
src/nwp500/cli/handlers.pyBetter separation of concerns between CLI framework and business logic
Industry-standard CLI framework with better maintainability
Added click>=8.0.0 dependency
Examples Reorganization: Restructured examples into beginner/intermediate/advanced/testing categories
Created structured hierarchy in
examples/directoryRenamed and moved 35+ example scripts for better discoverability
Updated
examples/README.mdwith ‘Getting Started’ guide and categorized indexAdded 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
Authentication Documentation: Major improvements to authentication guide
Complete rewrite of
docs/guides/authentication.rstAdded factory function patterns and examples
Improved context manager documentation
Added best practices and common patterns
More comprehensive code examples
Model Refactoring: Updated to use new converter modules
Replaced 53 lines of scattered validators with imports
Updated
fahrenheit_to_half_celsius()to useHalfCelsiusclassCleaner model definitions with centralized conversion logic
No breaking changes to public API
CLI Output Formatter Refactoring: Restructured
print_device_status()to use dynamic unit extractionReduced 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 dependenciesFixed type narrowing issues in
encoding.pyUpdated
MqttConnectioncallback signature to useAwsCrtErrorAdded public properties and setters where needed for type checking
Fixed¶
Superheat Temperature Units: Target and Current SuperHeat now correctly display in °F instead of °C
Both fields use
DeciCelsiusToFconversion, now properly reflected in CLI outputFields were displaying inconsistent units compared to all other temperature readings
Missing CLI Output Units: Multiple fields now display with proper units from model metadata
current_dhw_flow_rate: Now shows GPM 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 topicChanged 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
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 purposesUpdated
volume_codeto use newVolumeCodeenum with validation
Type Checking Errors: Resolved all pyright type checking errors in source code
Fixed datetime imports and type annotations
Added missing public properties and setters
Removed unused imports and variables
All source code now passes strict type checking
Version 7.1.0 (2025-12-22)¶
Added¶
Device Capability System: New device capability detection and validation framework: -
DeviceCapabilityChecker: Validates device feature support based on device models -DeviceInfoCache: Efficient caching of device information with configurable update intervals -@requires_capabilitydecorator: Automatic capability validation for MQTT commands -DeviceCapabilityError: New exception for unsupported device featuresAdvanced 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 schedulingCLI 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 sectionModel Field Factory Pattern: New field factory to reduce boilerplate in model definitions - Automatic field conversion and validation - Cleaner model architecture
Changed¶
CLI Output: Numeric values in status output now rounded to one decimal place for better readability
MqttDeviceControllernow integrates device capability checking with auto-caching of device infoException 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 managementLogging Security: Enhanced sensitive data redaction - MAC addresses consistently redacted across all logging output - Token logging removed from docstrings and examples - Intermediate variables used for redacted data
Fixed¶
Type annotation consistency: Optional parameters now properly annotated as
type | Noneinstead oftypeType System Fixes: Resolved multiple type annotation issues for CI compatibility
Mixing Valve Field: Corrected alias field name and removed unused TOU status validator
Vacation Days Validation: Enforced maximum value validation for vacation mode days
CI Linting: Fixed line length violations and import sorting issues
Security Scanning: Resolved intermediate variable issues in redacted MAC address handling
Parser Regressions: Fixed data parsing issues introduced in MQTT refactoring
Version 7.0.1 (2025-12-18)¶
Fixed¶
Minor bug fixes and improvements
Version 7.0.0 (2025-12-17)¶
BREAKING CHANGES: - Minimum Python version raised to 3.13 - Enumerations refactored for type safety and consistency
Removed¶
Python 3.9-3.12 Support: Minimum Python version is now 3.13
Home Assistant has deprecated Python 3.12 support, making Python 3.13 the de facto minimum for this ecosystem.
Python 3.13 features and improvements:
Experimental free-threaded mode (PEP 703): Optional GIL removal for true parallelism
JIT compiler (PEP 744): Just-in-time compilation for performance improvements
Better error messages: Enhanced suggestions for NameError, AttributeError, and import errors
Type system enhancements: TypeVars with defaults (PEP 696), @deprecated decorator (PEP 702), ReadOnly TypedDict (PEP 705)
Performance: ~5-10% faster overall, optimized dictionary/set operations, better function calls
PEP 695: New type parameter syntax for generics
PEP 701: f-string improvements
Built-in
datetime.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
Added¶
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.utcNative union syntax (
X | Yinstead ofUnion[X, Y])Cleaner generic type annotations throughout codebase
Enumerations Module (``src/nwp500/enums.py``): Comprehensive type-safe enums for device control and status
Status value enums:
OnOffFlag,Operation,DhwOperationSetting,CurrentOperationMode,HeatSource,DREvent,WaterLevel,FilterChange,RecirculationModeTime of Use enums:
TouWeekType,TouRateTypeDevice capability enums:
CapabilityFlag,TemperatureType,DeviceTypeDevice control command enum:
CommandCode(all MQTT command codes)Error code enum:
ErrorCodewith complete error code mappingsHuman-readable text mappings for all enums (e.g.,
DHW_OPERATION_SETTING_TEXT,ERROR_CODE_TEXT)Exported from main package:
from nwp500 import OnOffFlag, ErrorCode, CommandCodeComprehensive documentation in
docs/enumerations.rstExample usage in
examples/error_code_demo.py
Changed¶
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_RESERVATIONAll 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 useCapabilityFlagtypeMqttRequest.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
Fixed¶
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
Version 6.1.1 (2025-12-08)¶
Added¶
MQTT Diagnostics Module: New
MqttDiagnosticsCollectorfor capturing MQTT message traffic for debuggingCaptures 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 patternsComprehensive documentation in
docs/MQTT_DIAGNOSTICS.rstExported from main package:
from nwp500 import MqttDiagnosticsCollector
Version 6.1.0 (2025-12-03)¶
BREAKING CHANGES: Temperature API simplified with Fahrenheit input
This release fixes incorrect temperature conversions and provides a cleaner API where users pass temperatures in Fahrenheit directly, with automatic conversion to the device’s internal format.
Changed¶
``build_reservation_entry()``: Now accepts
temperature_f(Fahrenheit) instead of 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)
Removed¶
``set_dhw_temperature_display()``: Removed. This method used an incorrect conversion formula (subtracting 20 instead of proper half-degrees Celsius encoding). Use
set_dhw_temperature()with Fahrenheit directly.
Added¶
``fahrenheit_to_half_celsius()``: New utility function for converting Fahrenheit to the device’s half-degrees Celsius format. Exported from the main package for advanced use cases.
from nwp500 import fahrenheit_to_half_celsius param = fahrenheit_to_half_celsius(140.0) # Returns 120
Fixed¶
Temperature Encoding Bug: Fixed
set_dhw_temperature()which was using an incorrect “subtract 20” conversion instead of proper half-degrees Celsius encoding. This caused temperatures to be set incorrectly for values other than 140°F (where both formulas happened to give the same result).
Version 6.0.8 (2025-12-02)¶
Changed¶
Maintenance Release: Version bump for PyPI release
Version 6.0.7 (2025-11-30)¶
Added¶
Documentation: Added TOU (Time-of-Use) enable/disable command payload formats to protocol documentation
Version 6.0.6 (2025-11-24)¶
Added¶
Field Descriptions: Added comprehensive Field descriptions to
DeviceStatusandDeviceFeaturemodels with full documentation details including units, ranges, and usage context
Fixed¶
Example Code: Fixed
device_status_callback.pyexample to use snake_case attribute names consistentlyField 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)
Version 6.0.5 (2025-11-21)¶
Fixed¶
CRITICAL Temperature Conversion Bug: Corrected temperature conversion formula for 8 sensor fields that were displaying values ~100°F higher than expected. The v6.0.4 change incorrectly used division by 5 (pentacelsius) instead of division by 10 (decicelsius) for these fields:
tank_upper_temperature- Water tank upper 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.
Changed¶
Documentation: Updated
data_conversions.rstanddevice_status.rstto reflect correctDeciCelsiusToFconversion for refrigerant circuit and tank temperature sensors
Version 6.0.4 (2025-11-21)¶
Fixed¶
Temperature Conversion Accuracy: Corrected temperature conversion logic based on analysis of the decompiled mobile application. Previous conversions used approximations; new logic uses exact formulas from the app:
Replaced
Add20validator withHalfCelsiusToFfor fields transmitted as half-degrees CelsiusReplaced
DeciCelsiusToFwithPentaCelsiusToFfor fields scaled by factor of 5Affects multiple temperature sensor readings for improved accuracy
CLI Output Formatting: Fixed formatting issues in command-line interface output
Changed¶
Documentation: Updated temperature conversion documentation to use precise 9/5 fraction instead of 1.8 approximation for clarity
Added¶
Test Coverage: Added
tests/test_models.pyto verify temperature conversion correctness
Version 6.0.3 (2025-11-20)¶
BREAKING CHANGES: Migration from custom dataclass-based models to Pydantic BaseModel implementations with automatic field validation and alias handling.
Removed¶
Removed legacy dataclass implementations for models (DeviceInfo, Location, Device, FirmwareInfo, DeviceStatus, DeviceFeature, EnergyUsage*). All models now inherit from
NavienBaseModel(Pydantic).Removed manual
from_dictconstructors relying on camelCase key mapping logic.Removed field metadata conversion system (
meta()+apply_field_conversions()) in favor of PydanticBeforeValidatorpipeline.
Changed¶
Models now use snake_case attribute names consistently; camelCase keys from API/MQTT are mapped automatically via Pydantic
alias_generator=to_camel.Boolean device fields now validated via
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.
Added¶
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).
Migration Guide (v6.0.2 -> v6.0.3)¶
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_temperatureRemove 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.
Quick Example¶
# OLD (v6.0.2)
raw = mqtt_payload["deviceStatus"]
converted = apply_field_conversions(DeviceStatus, raw)
status = DeviceStatus(**converted)
print(converted["dhwTemperatureSetting"] + 20) # manual offset
# NEW (v6.0.3)
status = DeviceStatus.model_validate(mqtt_payload["deviceStatus"])
print(status.dhw_temperature_setting) # already includes +20 offset
# OLD boolean and enum handling
is_heating = converted["currentHeatUse"] == 2
mode = OperationMode(converted["operationMode"]) if converted["operationMode"] in (0,32,64,96) else OperationMode.STANDBY
# NEW simplified
is_heating = status.current_heat_use
mode = status.operation_mode
Benefits¶
Declarative conversions reduce 400+ lines of imperative transformation logic.
Improved performance (single parse vs copy + transform).
Automatic camelCase key mapping; less brittle than manual dict key copying.
Rich validation errors for debugging malformed device messages.
Cleaner, shorter model definitions with clearer intent.
Easier extension: add new fields with conversion by combining Annotated + validator.
Version 6.0.2 (2025-11-15)¶
Fixed¶
DNS resolution in containerized environments using ThreadedResolver
Updated AWS IoT library version
Device status field conversions
Changed¶
Refactored ThreadedResolver session creation into helper method
Version 6.0.1 (2025-11-06)¶
Fixed¶
Minor bug fixes and improvements
Version 6.0.0 (2025-11-02)¶
BREAKING CHANGES: Removed constructor callbacks and backward compatibility re-exports
Removed¶
Constructor Callbacks: Removed
on_connection_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.
Changed¶
Migration Benefits:
Multiple listeners per event (not just one callback)
Consistent API with other events (temperature_changed, mode_changed, etc.)
Dynamic listener management (add/remove listeners at runtime)
Async handler support
Priority-based execution
Cleaner imports (exceptions from one module)
Updated
examples/command_queue_demo.pyto use event emitter patternUpdated
examples/reconnection_demo.pyto use event emitter patternUpdated
examples/device_status_callback.pyto import exceptions from correct moduleUpdated
examples/device_status_callback_debug.pyto import exceptions from correct moduleUpdated
examples/device_feature_callback.pyto import exceptions from correct moduleUpdated
examples/test_api_client.pyto import exceptions from correct moduleRemoved misleading “legacy state” comments from connection tracking code
Version 5.0.2 (2025-10-31)¶
Fixed¶
MQTT Future Cancellation: Fixed InvalidStateError exceptions during disconnect
Added asyncio.shield() to protect concurrent.futures.Future objects from cancellation
Applied consistent cancellation handling across all MQTT operations (connect, disconnect, subscribe, unsubscribe, publish)
AWS CRT callbacks can now complete independently without raising InvalidStateError
Added debug logging when operations are cancelled for better diagnostics
Ensures clean shutdown without spurious exception messages
Version 5.0.1 (2025-10-27)¶
Changed¶
Maintenance Release: Removed deprecated backward compatibility code
Removed
CMD_*backward compatibility aliases fromconstants.pyRemoved
cli.pybackward compatibility wrapper moduleUpdated setup.cfg entry point to use
nwp500.cli.__main__:rundirectlyUpdated all examples to use
CommandCodeenum instead ofCMD_*aliasesUpdated examples to use standalone functions (
build_tou_period,encode_price,decode_price,encode_week_bitfield,decode_week_bitfield) instead ofNavienAPIClient.*static methodsUpdated documentation to reference standalone functions
Fixed deprecated method name (
set_dhw_operation_setting→set_dhw_mode)Removed broken relative links from
README.rstAdded Read the Docs and GitHub links to
README.rstheader
Version 5.0.0 (2025-10-27)¶
BREAKING CHANGES: This release introduces a comprehensive enterprise exception architecture. See migration guide below for details on updating your code.
Added¶
Enterprise Exception Architecture: Complete exception hierarchy for better error handling
Created
exceptions.pymodule with comprehensive exception hierarchyAdded
Nwp500Erroras base exception for all library errorsAdded MQTT-specific exceptions:
MqttError,MqttConnectionError,MqttNotConnectedError,MqttPublishError,MqttSubscriptionError,MqttCredentialsErrorAdded validation exceptions:
ValidationError,ParameterValidationError,RangeValidationErrorAdded device exceptions:
DeviceError,DeviceNotFoundError,DeviceOfflineError,DeviceOperationErrorAll exceptions now include
error_code,details, andretriableattributesAdded
to_dict()method to all exceptions for structured loggingAdded comprehensive test suite in
tests/test_exceptions.pyAdded comprehensive exception handling example (
examples/exception_handling_example.py)Updated key examples to demonstrate new exception handling patterns
Changed¶
Exception Handling Improvements:
All exception wrapping now uses exception chaining (
raise ... from e) to preserve stack tracesReplaced 19+ instances of
RuntimeError("Not connected to MQTT broker")withMqttNotConnectedErrorReplaced
ValueErrorin validation code withRangeValidationErrorandParameterValidationErrorReplaced
ValueErrorfor credentials withMqttCredentialsErrorReplaced
RuntimeErrorfor connection issues withMqttConnectionErrorEnhanced
AwsCrtErrorwrapping in MQTT code with proper exception chainingMoved authentication exceptions from
auth.pytoexceptions.pyMoved
APIErrorfromapi_client.pytoexceptions.pyCLI now handles specific exception types with better error messages and user guidance
Migration Guide (v4.x to v5.0)¶
Breaking Changes Summary:
The library now uses specific exception types instead of generic RuntimeError and ValueError.
This improves error handling but requires updates to exception handling code.
1. MQTT Connection Errors
# OLD CODE (v4.x) - will break
try:
await mqtt_client.request_device_status(device)
except RuntimeError as e:
if "Not connected" in str(e):
await mqtt_client.connect()
# NEW CODE (v5.0+)
from nwp500 import MqttNotConnectedError, MqttError
try:
await mqtt_client.request_device_status(device)
except MqttNotConnectedError:
# Handle not connected - attempt reconnection
await mqtt_client.connect()
await mqtt_client.request_device_status(device)
except MqttError as e:
# Handle other MQTT errors
logger.error(f"MQTT error: {e}")
2. Validation Errors
# OLD CODE (v4.x) - will break
try:
set_vacation_mode(device, days=35)
except ValueError as e:
print(f"Invalid input: {e}")
# NEW CODE (v5.0+)
from nwp500 import RangeValidationError, ValidationError
try:
set_vacation_mode(device, days=35)
except RangeValidationError as e:
# Access structured error information
print(f"Invalid {e.field}: must be {e.min_value}-{e.max_value}")
print(f"You provided: {e.value}")
except ValidationError as e:
# Handle other validation errors
print(f"Validation error: {e}")
3. AWS Credentials Errors
# OLD CODE (v4.x) - will break
try:
mqtt_client = NavienMqttClient(auth_client)
except ValueError as e:
if "credentials" in str(e).lower():
# handle missing credentials
# NEW CODE (v5.0+)
from nwp500 import MqttCredentialsError
try:
mqtt_client = NavienMqttClient(auth_client)
except MqttCredentialsError as e:
# Handle missing or invalid AWS credentials
logger.error(f"Credentials error: {e}")
await re_authenticate()
4. Catching All Library Errors
# NEW CODE (v5.0+) - catch all library exceptions
from nwp500 import Nwp500Error
try:
# Any library operation
await mqtt_client.request_device_status(device)
except Nwp500Error as e:
# All nwp500 exceptions inherit from Nwp500Error
logger.error(f"Library error: {e.to_dict()}")
# Check if retriable
if e.retriable:
await retry_operation()
5. Enhanced Error Information
All exceptions now include structured information:
from nwp500 import MqttPublishError
try:
await mqtt_client.publish(topic, payload)
except MqttPublishError as e:
# Access structured error information
error_info = e.to_dict()
# {
# 'error_type': 'MqttPublishError',
# 'message': 'Publish failed',
# 'error_code': 'AWS_ERROR_...',
# 'details': {},
# 'retriable': True
# }
# Log for monitoring/alerting
logger.error("Publish failed", extra=error_info)
# Implement retry logic
if e.retriable:
await asyncio.sleep(1)
await mqtt_client.publish(topic, payload)
Quick Migration Strategy:
Import new exception types:
from nwp500 import MqttNotConnectedError, MqttError, ValidationErrorReplace
except RuntimeErrorwithexcept MqttNotConnectedErrorfor connection checksReplace
except ValueErrorwithexcept ValidationErrorfor parameter validationUse
except Nwp500Errorto catch all library errorsTest error handling paths thoroughly
Benefits of New Architecture:
Specific exception types for specific errors (no more string matching)
Preserved stack traces with exception chaining (
from e)Structured error information via
to_dict()Retriable flag for implementing retry logic
Better integration with monitoring/logging systems
Type-safe error handling
Clearer API documentation
Version 4.8.0 (2025-10-27)¶
Added¶
Token Restoration Support: Enable session persistence across application restarts
Added
stored_tokensparameter toNavienAuthClient.__init__()for restoring saved tokensAdded
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 providedAutomatically 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 workflowUpdated authentication documentation with token restoration guide
Benefits: Reduces API load, improves startup time, prevents rate limiting for frequently restarting applications (e.g., Home Assistant)
Version 4.7.1 (2025-10-27)¶
Changed¶
Patch Release: No code changes, updated version format to full semantic versioning
Version 4.7 (2025-10-27)¶
Added¶
MQTT Reconnection: Two-tier reconnection strategy with unlimited retries
Implemented quick reconnection (attempts 1-9) for fast recovery from transient network issues
Implemented deep reconnection (every 10th attempt) with full connection rebuild and credential refresh
Changed default
max_reconnect_attemptsfrom 10 to -1 (unlimited retries)Added
deep_reconnect_thresholdconfiguration parameter (default: 10)Added
has_stored_credentialsproperty toNavienAuthClientAdded
re_authenticate()method toNavienAuthClientfor credential-based re-authenticationAdded
resubscribe_all()method toMqttSubscriptionManagerfor subscription recoveryDeep reconnection now performs token refresh and falls back to full re-authentication if needed
Deep reconnection automatically re-establishes all subscriptions after rebuild
Connection now continues retrying indefinitely instead of giving up after 10 attempts
Improved¶
Exception Handling: Replaced 25 catch-all exception handlers with specific exception types
mqtt_client.py: 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.nameattributeOptimized
resubscribe_all()to break after first failure per topic (reduces redundant error logs)Fixed subscription failure tracking to use sets for unique topic counting
Improved code clarity with intermediate variables for complex boolean expressions
Fixed¶
MQTT Reconnection: Eliminated duplicate “Connection interrupted” log messages
Removed duplicate logging from
mqtt_client.py(kept inmqtt_reconnection.py)
Version 3.1.4 (2025-10-26)¶
Fixed¶
MQTT Reconnection: Fixed MQTT reconnection failures due to expired AWS credentials
Added AWS credential expiration tracking (
_aws_expires_atfield inAuthTokens)Added
are_aws_credentials_expiredproperty to check AWS credential validityModified
ensure_valid_token()to prioritize AWS credential expiration checkTriggers full re-authentication (not just token refresh) when AWS credentials expire
Preserves AWS credential expiration timestamps during token refresh
Prevents reconnection failures when connection interrupts after AWS credentials expire but before JWT tokens expire
Resolves AWS_ERROR_HTTP_WEBSOCKET_UPGRADE_FAILURE errors during reconnection attempts
Improved test coverage for auth module from 31% to 60% with comprehensive test suite
Version 3.1.3 (2025-10-24)¶
Fixed¶
MQTT Reconnection: Improved MQTT reconnection reliability with active reconnection
Breaking Internal Change:
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 exhaustedImproved 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 beforeCompatible with existing auto-recovery examples (
auto_recovery_example.py,simple_auto_recovery.py)
Version 3.1.2 (2025-01-23)¶
Fixed¶
Authentication: Fixed 401 authentication errors with automatic token refresh
Add automatic token refresh on 401 Unauthorized responses in API client
Preserve AWS credentials when refreshing tokens (required for MQTT)
Save refreshed tokens to cache after successful API calls
Add retry logic to prevent infinite retry loops
Validate refresh_token exists before attempting refresh
Use specific exception types (TokenRefreshError, AuthenticationError) in error handling
Prevents masking unexpected errors during token refresh
Resolves ‘API request failed: 401’ error when using cached tokens
Version 3.1.1 (2025-01-22)¶
Fixed¶
MQTT Client: Fixed connection interrupted callback signature for AWS SDK
Updated callback to match latest AWS IoT SDK signature:
(connection, error, **kwargs)Fixed type annotations in
MqttConnectionfor proper type checkingResolves mypy type checking errors and ensures AWS SDK compatibility
Fixed E501 line length linting issue in connection interruption handler
Version 3.0.0 (Unreleased)¶
Breaking Changes
REMOVED:
OperationModeenum has been removedThis 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()functionRemoved
enable_deprecation_warnings()functionRemoved migration documentation files (MIGRATION.md, BREAKING_CHANGES_V3.md)
All functionality available through
DhwOperationSettingandCurrentOperationMode
Version 2.0.0 (Unreleased)¶
Breaking Changes (Planned for v3.0.0)
DEPRECATION:
OperationModeenum is deprecated and will be removed in v3.0.0Use
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
Added¶
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 instructionsValue 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 typesCode examples use new enums with proper imports
Clear distinction between configuration vs real-time status
Changed¶
DeviceStatus Model: Updated to use specific enum types
operationModefield now usesCurrentOperationModetypedhwOperationSettingfield now usesDhwOperationSettingtypeMaintains backward compatibility through value preservation
Example Scripts: Updated to demonstrate new enum usage
event_emitter_demo.pyupdated to useCurrentOperationModeFixed incorrect enum references (HEAT_PUMP_ONLY → HEAT_PUMP_MODE)
All examples remain functional with new type system
Deprecated¶
OperationMode enum: Will be removed in v3.0.0
All functionality preserved for backward compatibility
Migration guide available in
MIGRATION.mdHelper function
migrate_operation_mode_usage()provides guidanceOriginal enum remains available during transition period
Version 1.2.2 (2025-10-17)¶
Fixed¶
Release version 1.2.2
Version 0.2 (Unreleased)¶
Added¶
Local/CI Linting Synchronization: Complete tooling to ensure consistent linting results
Multiple sync methods: tox (recommended), direct scripts, pre-commit hooks, Makefile commands
CI-identical scripts:
scripts/lint.pyandscripts/format.pymirrortox -e lintandtox -e formatPre-commit hooks configuration for automatic checking
Comprehensive documentation:
LINTING_SETUP.md,DEVELOPMENT.md,FIX_LINTING.mdMakefile commands:
make ci-lint,make ci-format,make ci-checkStandardized 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 FuturesEliminates “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 eventAsync and sync handler support
Priority-based execution order (higher priority executes first)
One-time listeners with
once()methodDynamic listener management with
on(),off(),remove_all_listeners()Event statistics tracking (
listener_count(),event_count())wait_for()pattern for waiting on specific eventsThread-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_resumedNavienMqttClient now inherits from EventEmitter
Full backward compatibility with existing callback API
19 unit tests with 93% code coverage
Example:
event_emitter_demo.pyDocumentation:
EVENT_EMITTER.rst,EVENT_QUICK_REFERENCE.rst,EVENT_ARCHITECTURE.rst
Authentication: Simplified constructor-based authentication
NavienAuthClientnow requiresuser_idandpasswordin constructorAutomatic authentication when entering async context manager
No need to call
sign_in()manuallyBreaking 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 managementIntegrates seamlessly with automatic reconnection
Example:
command_queue_demo.pyDocumentation:
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_attemptsUser callbacks for connection interruption and resumption events
Manual disconnect detection to prevent unwanted reconnection
MqttConnectionConfigwith reconnection settingsExample:
reconnection_demo.pyDocumentation: 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 intoEnergyUsageResponseobjectsType-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 metricsHeat 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
Changed¶
Breaking: Python version requirement updated to 3.9+
Minimum Python version is now 3.9 (was 3.8)
Migrated to native type hints (PEP 585):
dict[str, Any]instead ofDict[str, Any]Removed
typing.Dict,typing.List,typing.DequeimportsCleaner, 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 signatureNow requires
user_idandpasswordas first parametersOld:
NavienAuthClient()thenawait client.sign_in(email, password)New:
NavienAuthClient(email, password)- authentication is automaticMigration: Pass credentials to constructor instead of sign_in()
All 18 example files updated to new pattern
All documentation updated with new examples
Documentation: Major updates across all files
Fixed all RST formatting issues (title underlines, tables)
Updated authentication examples in 8 documentation files
Fixed broken documentation links (local file paths)
Removed “Optional Feature” and “not required for basic operation” phrases
Fixed table rendering in DEVICE_STATUS_FIELDS.rst
Fixed JSON syntax in code examples
Added comprehensive reconnection documentation
Added comprehensive command queue documentation
Cleaned up backward compatibility references (new library)
Fixed¶
Critical Bug: Thread-safe reconnection task creation from MQTT callbacks
Fixed
RuntimeError: no running event loopwhen connection is interruptedFixed
RuntimeWarning: coroutine '_reconnect_with_backoff' was never awaitedConnection interruption callbacks run in separate threads without event loops
Implemented
_start_reconnect_task()helper method to properly create reconnection tasksUses existing
_schedule_coroutine()method for thread-safe task schedulingPrevents crashes during automatic reconnection after connection interruptions
Ensures reconnection tasks are properly awaited and executed
Critical Bug: Thread-safe event emission from MQTT callbacks
Fixed
RuntimeError: no running event loop in thread 'Dummy-1'MQTT callbacks run in separate threads created by AWS IoT SDK
Implemented
_schedule_coroutine()method for thread-safe schedulingEvent loop reference captured during
connect()for cross-thread accessUses
asyncio.run_coroutine_threadsafe()for safe event emissionPrevents crashes when emitting events from MQTT message handlers
All event emissions now work correctly from any thread
Bug: Incorrect method parameter passing in temperature control
Fixed
set_dhw_temperature_display()callingset_dhw_temperature()with wrong parametersWas passing individual parameters (
device_id,device_type,additional_value)Now correctly passes
Deviceobject as expected by method signatureSimplified implementation to just calculate offset and delegate to base method
Updated docstrings to match actual method signatures
Enhancement: Anonymized MAC addresses in documentation
Replaced all occurrences of real MAC address (
04786332fca0) with placeholder (aabbccddeeff)Updated
API_CLIENT.rst,MQTT_CLIENT.rst,MQTT_MESSAGES.rstUpdated built HTML documentation files
Protects privacy in public documentation
Critical Bug: Device control command codes
Fixed incorrect command code usage causing unintended power-off
Power-off now uses command code
33554433Power-on now uses command code
33554434DHW mode control now uses command code
33554437Discovered through network traffic analysis of official app
Critical Bug: MQTT topic pattern matching with wildcards
Fixed
_topic_matches_pattern()to correctly handle#wildcardTopics now match when message arrives on base topic (e.g.,
cmd/52/device/res)Topics also match subtopics (e.g.,
cmd/52/device/res/extra)Added length validation to prevent index out of bounds errors
Enables callbacks to receive messages correctly
Bug: Missing
OperationMode.STANDBYenum valueAdded
STANDBY = 0toOperationModeenumDevice 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
Bug: Insufficient topic subscriptions
Examples now subscribe to broader topic patterns
Subscribe to
cmd/{device_type}/{device_topic}/#to catch all command messagesSubscribe to
evt/{device_type}/{device_topic}/#to catch all event messagesEnsures 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
STANDBYUnknown temperature types default to
FAHRENHEITPrevents parsing failures from unexpected values
Documentation: Updated MQTT_MESSAGES.rst with correct command codes and temperature offset
Verified¶
Device Control: Real-world testing with Navien NWP500 device
Successfully changed DHW mode from Heat Pump to Energy Saver
Successfully changed DHW mode from Energy Saver to High Demand
Successfully changed DHW temperature (discovered 20°F offset between message and display)
Commands confirmed to reach and control physical device
Documented in DEVICE_CONTROL_VERIFIED.md
Version 0.1¶
Initial Documentation