Releases: eman/nwp500-python
v8.1.1
v8.1.0
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, triggering
on_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: Thefuture.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_thresholdis 0: Config validation
now clampsdeep_reconnect_thresholdto a minimum of 1, preventing a
ZeroDivisionErrorin 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 each
on_connection_interruptedevent. -
shortest_session_secondsnot JSON-serialisable: The diagnostics
to_dict()method usedfloat('inf')as the initial value for
shortest_session_seconds, which is not valid JSON. Changed toNone
so serialisation succeeds when no session has completed yet. -
wait_for()future not bound to running loop:wait_for()created a
bareasyncio.Future()rather than
asyncio.get_running_loop().create_future(), which could bind the future to
a different loop in multi-loop test setups. -
Reservation temperature validation was US-only:
build_reservation_entry
validated set-point temperatures against hardcoded Fahrenheit bounds (95–150 °F)
regardless of the active unit system. Validation now uses the current unit system
context: 35–65 °C in metric mode, 95–150 °F in US mode. Celsius users previously
received 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
PeriodicRequestTypesilently 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.
v8.0.0
Release 8.0.0
v7.4.10
Changed
- Loosened pydantic version requirement: Changed from
pydantic>=2.12.5to
pydantic>=2.0.0to resolve dependency conflicts with Home Assistant, which
ships withpydantic==2.12.2.
v7.4.9
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_expiryused
datetime.now()(naive, local time). During DST transitions or timezone
changes this could cause incorrect expiry detection, leading to premature
re-authentication or use of an actually-expired token. Fixed by using
datetime.now(UTC)throughout, switching 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()used
CommandCode.GOOUT_DAY(33554466), which the device silently accepted
but did not activate vacation mode — the operating mode remained unchanged.
HAR capture of the official Navien app confirms the correct command is
DHW_MODE(33554437) 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 returnedNoneon
TimeoutError, violating the type contract. Now returns
"us_customary"consistent with the warning message. - Once-Listener Becomes Permanent With Duplicate Callbacks:
emit()
identified once-listeners via 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.once
directly on theEventListenerobject. - Auth Session Leaked on Client Construction Failure: In
create_navien_clients(), ifNavienAPIClientor
NavienMqttClientconstruction raised after a successful
auth_client.__aenter__(), the auth session and its underlying
aiohttpsession would leak. Client construction is now wrapped in a
try/exceptthat callsauth_client.__aexit__()on failure.
Additionally, bothexcept BaseExceptionblocks have been replaced with
except Exception(passing real exception info to__aexit__) plus a
separateexcept asyncio.CancelledErrorblock that uses
asyncio.shield()to ensure cleanup completes even when the task is
being cancelled. - Hypothesis Tests Broke All Test Collection:
test_mqtt_hypothesis.py
importedhypothesisat 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.
v7.4.8
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.
v7.4.7
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. Usenwp-cli anti-legionella set-period 7to update cycle period.
Changed
examples/advanced/tou_openei.py: Rewritten to use the newOpenEIClientandconvert_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.
v7.4.6
Fixed
- Converter Consistency:
div_10()andmul_10()now correctly apply division/multiplication to all input types afterfloat()conversion, not justint/floattypes - Reservation Decoding: Fixed
decode_reservation_hex()to validate chunk length before checking for empty entries, preventing potential out-of-bounds access - Factory Cleanup:
create_navien_clients()now properly cleans up auth session if authentication fails during context manager entry - MQTT Reconnection: MQTT client now resubscribes to all topics after successful reconnection
- Subscription Leak: Fixed resource leak where
wait_for_device_feature()did not unsubscribe its callback after completion - Duplicate Handlers: Subscription manager now prevents duplicate callback registration for the same topic
- Command Queue:
MqttCommandQueuenow raises onQueueFullinstead of silently swallowing the error - Flow Rate Metadata: Removed hardcoded
"GPM"unit fromrecirc_dhw_flow_ratefield; unit is now dynamic based on unit system - Temperature Rounding:
RawCelsiusFahrenheit conversion now uses a catch-all default for standard rounding instead of matching onlySTANDARDenum value - Unit System Default:
is_metric_preferred()now returnsFalse(Fahrenheit) instead ofNonewhen no unit system override or context is set
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 usinggetattr - Unsubscribe Feature: Added
unsubscribe_device_feature()method to MQTT client and subscription manager for targeted callback removal - Hypothesis Fuzzing: Added property-based fuzzing tests for MQTT payload handling
- Bandit Security Scanning: Added bandit configuration for security analysis in CI
v7.4.5
Fixed
- Energy Capacity Unit Scaling: Corrected unit scaling for energy capacity fields that were off by a factor of 10
- CLI Output: Fixed linting issue by replacing str and Enum with StrEnum for InstallType
v7.3.4
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 offset - Heat pump and heater element differential settings now use
DeciCelsiusDeltainstead ofDeciCelsius hp_upper_on_diff_temp_setting,hp_lower_on_diff_temp_setting,he_upper_on_diff_temp_setting,he_lower_on_diff_temp_setting, and related off settings now convert correctly to Fahrenheit- Example: A device value of 5 (representing 0.5°C delta) now correctly converts to 0.9°F delta instead of 32.9°F
- Created new
-
CLI Output: Added display of heat pump and heater element differential temperature settings in device status output
Changed
- Internal API: Added
div_10_celsius_delta_to_preferredconverter for temperature delta values in device models