Skip to content

fix: device protocol correctness#94

Merged
eman merged 3 commits into
mainfrom
fix/protocol-correctness
Jul 5, 2026
Merged

fix: device protocol correctness#94
eman merged 3 commits into
mainfrom
fix/protocol-correctness

Conversation

@eman

@eman eman commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Fifth batch from the comprehensive repo evaluation: fixes for what the library actually sends to (and decodes from) the physical appliance.

Bugs fixed

  • Malformed schedule commands (mqtt/control.py, _base.py): update_weekly_reservation() and configure_recirculation_schedule() dumped their schedule models as-is, which:

    1. double-nested the payload (request.reservation.reservation / request.schedule.schedule) — inconsistent with the documented flat shape update_reservations() sends, and
    2. leaked pydantic computed_fields into the command — enabled, days, time, mode_name, and a unit-converted temperature (e.g. 140.0 °F) alongside the raw half-Celsius param: 120.

    Both now send flat lists of raw protocol entries via a new NavienBaseModel.to_protocol_dict() helper (declared fields only, by alias).

  • Command queue reordering (mqtt/command_queue.py): a command that failed mid-flush was re-queued at the tail, behind newer commands — [power_on, set_temp] became [set_temp, power_on] on the next flush. The queue is now a deque and failed commands are re-inserted at the front.

  • Stale command replay (mqtt/command_queue.py, mqtt/utils.py): QueuedCommand.timestamp was stored but never checked, so a multi-hour outage replayed hours-old set_power/set_dhw_temperature commands to a physical water heater on reconnect. Commands older than MqttConnectionConfig.max_queued_command_age (default 300 s; None disables) are now discarded at send time with a warning.

  • Sub-zero Fahrenheit off-by-one (temperature.py): the ASYMMETRIC rounding formula used Python's floored % (always non-negative: -11 % 10 == 9) where the firmware/NaviLink app uses a truncated remainder (-1). Raw -11 (-5.5 °C) decoded to 22 °F vs the app's 23 °F — routine for the outdoor temperature sensor in winter. Now uses math.fmod semantics; positive values unchanged.

  • Freeze protection defaults in the wrong unit domain (models/status.py): defaults 43/65 were Fahrenheit display values stored in raw half-Celsius fields, decoding to 70.7 °F / 90.5 °F when the device omits the fields — any UI clamping a slider to them would allow out-of-range setpoints. Corrected to raw 12/20 = 43 °F / 50 °F, matching the fixed limits documented in data_conversions.rst.

  • Missing error-change events (mqtt/state_tracker.py): error_detected fired only on 0 → non-zero; an E799 → E407 transition emitted nothing, so consumers kept displaying the stale error.

  • TOU price encoding (encoding.py): encode_price() used round() (banker's rounding), under-encoding exact halves at even boundaries (0.125 @ dp=2 → 12 instead of 13); now Decimal ROUND_HALF_UP. build_tou_period() treated bool prices as pre-encoded integers (True → price 1) because bool subclasses int.

  • Protocol doc errors (encoding.py, temperature.py): enable-flag convention documented inverted (1=enabled instead of 2=enabled); build_reservation_entry() example showed week: 158 for Mon/Wed/Fri instead of 84 (64+16+4); temperature doctests showed int raw values where floats are returned (all temperature.py doctests now actually pass).

Tests

New tests/test_protocol_correctness.py (17 tests): exact payload shape for both schedule commands, to_protocol_dict() exclusions, queue order preservation across failed flushes, expiry (incl. None disable and overflow), negative/positive ASYMMETRIC conversions, freeze defaults, error-change/unchanged/cleared events, half-up price encoding, bool/int TOU price handling.

Validation

  • pytest: 525 passed
  • ruff check / ruff format --check: clean
  • pyright src/nwp500: 0 errors
  • mypy src/nwp500: no issues

- Send flat raw protocol payloads for weekly reservation and
  recirculation schedules; the model dump double-nested the request
  and leaked computed display fields (incl. unit-converted temperature)
  into device commands. Add NavienBaseModel.to_protocol_dict()
- Preserve command queue order on failed flush (deque with re-insert
  at front instead of tail)
- Discard queued commands older than max_queued_command_age (default
  300s) instead of replaying stale control commands after long outages
- Use truncated remainder (firmware semantics) instead of Python's
  floored modulo in the ASYMMETRIC Fahrenheit formula; fixes
  off-by-one conversions for sub-zero temperatures
- Correct freeze protection default limits to raw half-Celsius 12/20
  (43F/50F documented fixed range) instead of 43/65
- Emit error_detected when the error code changes between two
  non-zero codes
- Round TOU prices half-up via Decimal (round() banker's rounding
  under-encoded exact halves); encode bool prices instead of passing
  them through as pre-encoded ints
- Fix inverted enable-flag doc, wrong week-bitfield example (158->84),
  and int/float doctest values

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens device protocol correctness across MQTT command payloads, queued command handling, and multiple encoding/decoding edge cases, with a new regression test suite to lock in the intended on-wire behavior.

Changes:

  • Fix schedule command payloads to send flat lists of raw protocol entries (via NavienBaseModel.to_protocol_dict()), avoiding double-nesting and computed/display-field leakage.
  • Rework MQTT command queue flushing to preserve ordering on failure and to discard stale queued commands based on MqttConnectionConfig.max_queued_command_age.
  • Correct protocol encoding/decoding edge cases (negative ASYMMETRIC Fahrenheit rounding, freeze protection raw defaults, error-change event emission, TOU price encoding rounding/bool handling) and document them.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_protocol_correctness.py Adds targeted regression tests for protocol payload shapes, queue ordering/expiry, temperature rounding, defaults, events, and TOU price encoding.
src/nwp500/_base.py Introduces to_protocol_dict() to dump only declared fields (by alias) for device-bound payloads.
src/nwp500/mqtt/control.py Updates schedule-related commands to send flat raw protocol entries rather than dumping full models.
src/nwp500/mqtt/command_queue.py Switches queue to deque, preserves order on failed flushes, and adds send-time expiry logic.
src/nwp500/mqtt/utils.py Adds max_queued_command_age config field (used by the queue expiry behavior).
src/nwp500/mqtt/state_tracker.py Emits error_detected when the error code changes between non-zero values.
src/nwp500/temperature.py Fixes ASYMMETRIC rounding for negative raw values and updates doctest expectations.
src/nwp500/models/status.py Corrects freeze protection default raw values to match documented limits.
src/nwp500/encoding.py Implements HALF_UP TOU price rounding and prevents bool from silently passing as pre-encoded int in TOU builders; fixes protocol docs/examples.
CHANGELOG.rst Documents the protocol correctness fixes under Unreleased.

Comment thread src/nwp500/temperature.py Outdated
Comment thread src/nwp500/encoding.py
Comment thread src/nwp500/mqtt/utils.py
emmanuel added 2 commits July 5, 2026 09:38
- RawCelsius.to_fahrenheit() now returns float as annotated (round()
  without ndigits returns int); doctest updated to 140.0
- encode_price() builds the Decimal from the original value instead of
  round-tripping through float, which lost precision for Decimal inputs
  and introduced binary floating-point artifacts; signature widened to
  accept Decimal explicitly
- MqttConnectionConfig.__post_init__ validates max_queued_commands >= 1,
  max_queued_command_age >= 0 (or None), and operation_timeout > 0 to
  fail fast instead of producing hard-to-debug runtime errors
- Tests for config validation, Decimal-input precision, and the float
  return type
@eman
eman merged commit 59165da into main Jul 5, 2026
5 checks passed
@eman
eman deleted the fix/protocol-correctness branch July 5, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants