Skip to content

Add confirmed reservation/TOU write + canonical schedule comparison#118

Merged
eman merged 4 commits into
mainfrom
feat/111-confirmed-reservation-write
Jul 23, 2026
Merged

Add confirmed reservation/TOU write + canonical schedule comparison#118
eman merged 4 commits into
mainfrom
feat/111-confirmed-reservation-write

Conversation

@eman

@eman eman commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

update_reservations and configure_tou_schedule are fire-and-forget — they return only the MQTT publish packet id, with no way to confirm the device actually applied a write. This adds a program → verify loop:

  • update_reservations_confirmed() (nwp500.reservations) and configure_tou_schedule_confirmed() (new nwp500.tou_schedule module): both send the write and await the matching rsv/rd / tou/rd echo, returning the parsed schedule the device now holds instead of just a packet id. Mirrors the existing fetch_reservations subscribe/future/timeout pattern, so behavior (timeout, unsubscribe-on-exit, ignoring late/duplicate responses) is consistent with the read path.
  • Canonical/normalized representation: ReservationEntry.canonical_key() / ReservationSchedule.canonical(), and the TOUPeriod / TOUReservationSchedule equivalents. Each returns a stable, order-independent, hashable tuple of raw protocol fields, so a consumer can compare a desired program against a device read-back with desired.canonical() == confirmed.canonical() (or hash it) without hand-diffing or caring what order the device returned entries in.

Both new public helpers are exported from the top-level nwp500 package (update_reservations_confirmed, configure_tou_schedule_confirmed).

Not included here

The stale decode_reservation_hex docstring ("param (temperature offset by 20°F)", should be half-degrees Celsius) mentioned in this issue is fixed separately in #113 to keep that doc fix isolated and easy to review.

Fixes #111

Test plan

  • tests/test_reservations.py: new tests for update_reservations_confirmed — success, timeout, and order-independent canonical() match against the device echo (mirrors existing fetch_reservations tests).
  • tests/test_tou_schedule.py (new): equivalent coverage for configure_tou_schedule_confirmed.
  • tests/test_canonical_schedule.py (new): golden-vector tests for canonical_key()/canonical() — including a vector decoded from decode_reservation_hex's own docstring example, order-independence, hash stability, and that differing programs compare unequal.
  • Full suite: 620 passed, 4 skipped. ruff check / ruff format --check clean on all touched/new files.

…mparison

update_reservations and configure_tou_schedule were fire-and-forget: they
returned only the MQTT publish packet id, with no way to confirm the
device actually applied the write. Consumers programming a native
reservation schedule from an external source (e.g. an ML demand
forecast) need a program->verify loop.

- Add update_reservations_confirmed() (nwp500.reservations) and
  configure_tou_schedule_confirmed() (new nwp500.tou_schedule module):
  both send the write and await the matching rsv/rd or tou/rd echo,
  returning the parsed schedule the device now holds (mirrors the
  existing fetch_reservations subscribe/future/timeout pattern).
- Add ReservationEntry.canonical_key() / ReservationSchedule.canonical()
  and the TOUPeriod/TOUReservationSchedule equivalents: a stable,
  order-independent, hashable representation of raw protocol fields so
  a desired program can be compared against a device read-back with
  `a.canonical() == b.canonical()` instead of hand-diffing.

The stale "param (temperature offset by 20°F)" docstring in
decode_reservation_hex (encoding.py) is fixed separately in #113.

Fixes #111
Comment thread src/nwp500/tou_schedule.py Fixed

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 adds “write then verify” helper APIs for reservation and TOU schedule updates over MQTT, plus canonical (order-independent) schedule representations to make desired-vs-device comparisons stable and hashable.

Changes:

  • Add update_reservations_confirmed() and configure_tou_schedule_confirmed() helpers that await the device’s rsv/rd / tou/rd echo and return the parsed schedule (or None on timeout).
  • Add canonical tuple representations for reservation entries/schedules and TOU periods/schedules to enable stable equality/hash comparisons independent of device return ordering.
  • Add/extend tests to cover confirmed-write success/timeout paths and canonical equality/hash behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/nwp500/reservations.py Adds update_reservations_confirmed() using the subscribe/future/timeout pattern.
src/nwp500/tou_schedule.py New module with configure_tou_schedule_confirmed() implementing confirmed TOU writes.
src/nwp500/models/schedule.py Adds ReservationEntry.canonical_key() and ReservationSchedule.canonical() for stable comparisons.
src/nwp500/models/tou.py Adds TOUPeriod.canonical_key() and TOUReservationSchedule.canonical() for stable comparisons.
src/nwp500/__init__.py Exports the two new public helpers from the top-level package.
tests/test_reservations.py Adds tests for update_reservations_confirmed() (success/timeout/canonical match).
tests/test_tou_schedule.py New tests for configure_tou_schedule_confirmed() (success/timeout/canonical match).
tests/test_canonical_schedule.py New golden-vector tests for canonical key/canonical schedule behavior (reservation + TOU).

Comment on lines +111 to +123
future: asyncio.Future[ReservationSchedule] = (
asyncio.get_running_loop().create_future()
)

def on_schedule(schedule: ReservationSchedule) -> None:
if not future.done():
future.set_result(schedule)

await mqtt.subscribe_reservation_response(device, on_schedule)
try:
await mqtt.update_reservations(device, reservations, enabled=enabled)
try:
return await asyncio.wait_for(future, timeout=timeout)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch — fixed in 7c13dca. The device protocol has no request/response correlation id on rsv/rd, so instead of resolving on the first message received, the callback now only accepts a response whose canonical() form matches what was just written, ignoring stale/unrelated responses until a match arrives or the timeout expires. Added test_update_reservations_confirmed_ignores_stale_response covering this.

Comment on lines +60 to +73
future: asyncio.Future[TOUReservationSchedule] = (
asyncio.get_running_loop().create_future()
)

def on_schedule(schedule: TOUReservationSchedule) -> None:
if not future.done():
future.set_result(schedule)

await mqtt.subscribe_tou_response(device, on_schedule)
try:
await mqtt.configure_tou_schedule(
device, controller_serial_number, periods, enabled=enabled
)
try:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Same fix applied here in 7c13dca — the TOU echo is now matched against the canonical() form of what was configured, so a stale/unrelated tou/rd response is ignored instead of resolving the future early. Added test_configure_tou_schedule_confirmed_ignores_stale_response.

eman added 2 commits July 23, 2026 08:26
Review feedback on #118 (Copilot): update_reservations_confirmed and
configure_tou_schedule_confirmed resolved on the FIRST rsv/rd or tou/rd
message received after subscribing, including a stale/unrelated response
from a concurrent read or a previous write. The device protocol has no
request/response correlation id on these topics, so instead only accept
a response whose canonical() form matches what was just written -
ignore anything else and keep waiting until a match arrives or timeout
expires.
CodeQL flagged clear-text logging of sensitive data (py/clear-text-
logging-sensitive-data, high severity) in configure_tou_schedule_confirmed's
unsubscribe failure handler. update_reservations_confirmed had the same
pattern. Both now use the existing nwp500.mqtt.utils.redact_mac() helper
(already used for this exact purpose elsewhere, e.g. device_info_cache.py,
mqtt/client.py) instead of logging the raw MAC address.
Comment thread src/nwp500/reservations.py Dismissed
Comment thread src/nwp500/tou_schedule.py Dismissed
pyright flagged passing raw dicts directly into ReservationSchedule /
TOUReservationSchedule's `reservation` field: list[dict] isn't
assignable to list[ReservationEntry] / list[TOUPeriod] under pyright's
invariant list typing, even though pydantic validates and coerces the
dicts fine at runtime. Construct the entry/period models explicitly
instead, satisfying the type checker that CI's tox run enforces.
@eman
eman merged commit e4c7c8b into main Jul 23, 2026
7 checks passed
eman added a commit that referenced this pull request Jul 23, 2026
Documents the four merged PRs (#115-#118): confirmed reservation/TOU
write helpers with canonical schedule comparison (#111), freeze
protection temperature validation (#112), the decode_reservation_hex
docstring fix (#113), and demand response capability gating (#114).
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.

Add confirmed reservation write + canonical schedule representation for verification

3 participants