From e1d1d55b7fd6909e368f360d93399e3936962969 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Sat, 29 Aug 2026 07:08:45 -0700 Subject: [PATCH 1/2] Remove request_tou_settings; there is no MQTT TOU read request_tou_settings published a TOU_RESERVATION message to ctrl/tou/rd carrying only controllerSerialNumber, then waited for a reply on res/tou/rd. That reply never comes. On a live device with TOU fully provisioned - program_reservation_use true, valid controller serial, and a PG&E plan the REST API returns in full - the request produced silence for 45 seconds. ctrl/tou/rd with that command code is the *write*. It is what configure_tou_schedule publishes, and what the vendor app publishes from its TOU editor (TouScheduleViewmodel.setPublishMgppControlTou passes the on/off flag and the interval list). The device replies on res/tou/rd to confirm a write; it does not answer a request with no schedule in it. The vendor app reads TOU over REST - /device/tou and /device/tou/convert are the only TOU reads in any of the HAR captures. So the method could not have worked, and it published a write-shaped command while pretending to read. This device ignores it - touStatus was unchanged across repeated calls - but a firmware that took it at face value could read it as "store an empty TOU schedule", which is not a risk worth carrying for a call that returns nothing. Reads go through NavienAPIClient.get_tou_info. subscribe_tou_response keeps working for write confirmations, and set_tou_enabled is a separate command pair and is untouched. The docs and the two examples that called it are updated, including the payload-capture example, which should not have been publishing a control command during a read-only capture. Claude-Session: https://claude.ai/code/session_01XVj9BYvuLj7Th3iFUVoeCn --- CHANGELOG.rst | 33 +++++++ docs/how-to/optimize-tou.rst | 96 ++++++++----------- docs/reference/python_api/mqtt_client.rst | 13 +-- examples/advanced/firmware_payload_capture.py | 15 +-- examples/advanced/tou_schedule.py | 8 +- src/nwp500/models/tou.py | 11 ++- src/nwp500/mqtt/_control_commands.py | 8 -- src/nwp500/mqtt/_device_subscriptions.py | 7 +- src/nwp500/mqtt/control.py | 32 ------- src/nwp500/mqtt/subscriptions.py | 9 +- tests/test_public_api.py | 13 +++ 11 files changed, 113 insertions(+), 132 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 146ebe87..308bc7d7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,9 @@ Changelog Unreleased ========== +**BREAKING CHANGE**: ``NavienMqttClient.request_tou_settings()`` is removed. +It could never have worked. + Added ----- - **REST fields the cloud added since the models were written.** @@ -26,6 +29,36 @@ Added plain int) so an unrecognised code cannot make a whole listing unparseable. ``docs/openapi.yaml`` is updated to match. +Removed +------- +- **``request_tou_settings()`` removed - the device has no MQTT read for its + TOU schedule.** The method published a ``CommandCode.TOU_RESERVATION`` + message to ``ctrl/tou/rd`` carrying only ``controllerSerialNumber``, then + waited for a reply on ``res/tou/rd``. No reply ever comes: a live device + with TOU provisioned (``program_reservation_use`` true, a valid controller + serial, and a plan the REST API returns in full) stayed silent for 45 + seconds. ``ctrl/tou/rd`` with that command code is the *write* - it is what + :meth:`~nwp500.NavienMqttClient.configure_tou_schedule` publishes and what + the vendor app publishes from its TOU editor + (``TouScheduleViewmodel.setPublishMgppControlTou``) - and the device answers + on ``res/tou/rd`` only 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 - ``touStatus`` was unchanged + across repeated calls - but a firmware that took it at face value could read + it as "store an empty TOU schedule". + + **Migration**: use :meth:`~nwp500.NavienAPIClient.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. + :meth:`~nwp500.NavienMqttClient.subscribe_tou_response` is unaffected and + still delivers write confirmations. Enabling and disabling TOU + (:meth:`~nwp500.NavienMqttClient.set_tou_enabled`, command codes + ``TOU_ON``/``TOU_OFF``) is a separate path and is unaffected. + Fixed ----- - **CLI ``energy --months`` no longer duplicates ``--month`` output.** diff --git a/docs/how-to/optimize-tou.rst b/docs/how-to/optimize-tou.rst index a6c936aa..555750ec 100644 --- a/docs/how-to/optimize-tou.rst +++ b/docs/how-to/optimize-tou.rst @@ -339,28 +339,36 @@ Enables or disables TOU operation without changing the schedule. * ``device``: Device object * ``enabled``: ``True`` to enable TOU, ``False`` to disable -MQTT: Request TOU Settings -~~~~~~~~~~~~~~~~~~~~~~~~~~ +REST: Read the Current TOU Schedule +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python - async def request_tou_settings( - device: Device, - controller_serial_number: str - ) -> None + async def get_tou_info( + mac_address: str, + additional_value: str, + controller_id: str, + user_type: str = "O", + ) -> TOUInfo -Requests the current TOU configuration from the device. +Returns the stored TOU plan: rate name, utility, ZIP code, and the seasonal +schedule with its pricing intervals. **Parameters:** -* ``device``: Device object -* ``controller_serial_number``: Controller serial number +* ``mac_address``: Device MAC address +* ``additional_value``: Additional device identifier +* ``controller_id``: Controller serial number +* ``user_type``: User type (default ``"O"``) -The device will respond on the topic: +.. note:: -.. code-block:: text - - cmd/{deviceType}/{deviceId}/res/tou/rd + There is no MQTT read for the TOU schedule. ``ctrl/tou/rd`` with + ``CommandCode.TOU_RESERVATION`` is the *write* - it is what + :meth:`~nwp500.NavienMqttClient.configure_tou_schedule` publishes, and what + the vendor app publishes from its TOU editor. The device replies on + ``cmd/{deviceType}/{clientId}/res/tou/rd`` to confirm such a write; it does + not answer a request that carries no schedule. Read the plan over REST. Building TOU Periods -------------------- @@ -651,58 +659,38 @@ Configure different rates for summer and winter: Example 3: Retrieve Current TOU Settings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Query the device for its current TOU configuration: +Read the stored TOU plan over the REST API. No MQTT connection is needed: .. code-block:: python - from nwp500.encoding import decode_week_bitfield, decode_price + from nwp500.encoding import decode_price, decode_week_bitfield async def check_tou_settings(): async with NavienAuthClient("user@example.com", "password") as auth_client: api_client = NavienAPIClient(auth_client=auth_client) device = await api_client.get_first_device() - - mqtt_client = NavienMqttClient(auth_client) - await mqtt_client.connect() - + # ... get controller_serial (same as Example 1) ... - - # Set up response handler - response_topic = f"cmd/{device.device_info.device_type}/{mqtt_client.config.client_id}/res/tou/rd" - - def on_tou_response(topic: str, message: dict): - response = message.get("response", {}) - enabled = response.get("reservationUse") - periods = response.get("reservation", []) - - print(f"TOU Enabled: {enabled}") - print(f"Number of periods: {len(periods)}") - - for i, period in enumerate(periods, 1): - days = decode_week_bitfield(period.get("week", 0)) - price_min = decode_price( - period.get("priceMin", 0), - period.get("decimalPoint", 0) - ) - price_max = decode_price( - period.get("priceMax", 0), - period.get("decimalPoint", 0) - ) - + + tou_info = await api_client.get_tou_info( + mac_address=device.device_info.mac_address, + additional_value=device.device_info.additional_value, + controller_id=controller_serial, + ) + + print(f"Plan: {tou_info.name} ({tou_info.utility})") + + # TOUSchedule.intervals holds the raw interval dicts + for season in tou_info.schedule: + for i, interval in enumerate(season.intervals, 1): + days = decode_week_bitfield(interval["week"]) + dp = interval["decimalPoint"] print(f"\nPeriod {i}:") print(f" Days: {', '.join(days)}") - print(f" Time: {period['startHour']:02d}:{period['startMinute']:02d} " - f"- {period['endHour']:02d}:{period['endMinute']:02d}") - print(f" Price: ${price_min:.5f} - ${price_max:.5f}/kWh") - - await mqtt_client.subscribe(response_topic, on_tou_response) - - # Request current settings - await mqtt_client.request_tou_settings(device, controller_serial) - - # Wait for response - await asyncio.sleep(5) - await mqtt_client.disconnect() + print(f" Time: {interval['startHour']:02d}:{interval['startMinute']:02d} " + f"- {interval['endHour']:02d}:{interval['endMinute']:02d}") + print(f" Price: ${decode_price(interval['priceMin'], dp):.5f} " + f"- ${decode_price(interval['priceMax'], dp):.5f}/kWh") asyncio.run(check_tou_settings()) diff --git a/docs/reference/python_api/mqtt_client.rst b/docs/reference/python_api/mqtt_client.rst index 0b8f5ff1..3eb2f223 100644 --- a/docs/reference/python_api/mqtt_client.rst +++ b/docs/reference/python_api/mqtt_client.rst @@ -582,13 +582,6 @@ configure_tou_schedule() **Capability Required:** ``program_reservation_use`` -request_tou_settings() -^^^^^^^^^^^^^^^^^^^^^^ - -.. py:method:: request_tou_settings(device, controller_serial_number) - - Request the current TOU schedule. - subscribe_tou_response() ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -597,9 +590,9 @@ subscribe_tou_response() Subscribe to parsed TOU schedule responses. The callback is invoked with a :class:`~nwp500.models.TOUReservationSchedule` - whenever the device responds to a :meth:`request_tou_settings` read or a - :meth:`configure_tou_schedule` write (both use the ``tou/rd`` response - topic). + when the device confirms a :meth:`configure_tou_schedule` write on the + ``tou/rd`` response topic. The device has no MQTT read for its TOU schedule; + to read the stored plan, use :meth:`~nwp500.NavienAPIClient.get_tou_info`. :param callback: Called with the parsed TOU schedule on each response. :type callback: Callable[[TOUReservationSchedule], None] diff --git a/examples/advanced/firmware_payload_capture.py b/examples/advanced/firmware_payload_capture.py index 0a29519d..7ab78a95 100755 --- a/examples/advanced/firmware_payload_capture.py +++ b/examples/advanced/firmware_payload_capture.py @@ -8,7 +8,6 @@ Specifically captures: - Weekly reservations (rsv/rd) - - Time-of-Use schedule (tou/rd) - Device info (firmware versions, capabilities) - Device status (current operating state) - All other response/event topics (via wildcards) @@ -37,7 +36,6 @@ from typing import Any from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient -from nwp500.exceptions import Nwp500Error from nwp500.models import DeviceFeature from nwp500.mqtt.utils import redact, redact_topic from nwp500.topic_builder import MqttTopicBuilder @@ -156,16 +154,9 @@ def on_feature(feature: DeviceFeature) -> None: await mqtt_client.request_reservations(device) await asyncio.sleep(5) - # --- Step 4: request TOU schedule (requires controller serial number) --- - if device_feature and device_feature.program_reservation_use: - serial = device_feature.controller_serial_number - if serial: - print("Requesting TOU schedule...") - try: - await mqtt_client.request_tou_settings(device, serial) - await asyncio.sleep(5) - except Nwp500Error as exc: - print(f" TOU request failed: {exc}") + # No TOU step: the device has no MQTT read for its TOU schedule. + # ctrl/tou/rd carries the *write*, which a capture tool must not send. + # Read the schedule over REST instead (api_client.get_tou_info). # --- Step 5: wait a bit more to catch any late-arriving messages --- print("\nWaiting for any remaining messages...") diff --git a/examples/advanced/tou_schedule.py b/examples/advanced/tou_schedule.py index 39dce5f6..b0188124 100755 --- a/examples/advanced/tou_schedule.py +++ b/examples/advanced/tou_schedule.py @@ -119,10 +119,10 @@ def on_tou_response(topic: str, message: dict[str, Any]) -> None: enabled=True, ) - print("Requesting current TOU settings for confirmation...") - await mqtt_client.request_tou_settings(device, controller_serial) - - print("Waiting up to 15 seconds for TOU responses...") + # The device has no MQTT read for its TOU schedule; the write above + # is confirmed on the same tou/rd topic. To read the stored schedule + # back, use the REST API: api_client.get_tou_info(...). + print("Waiting up to 15 seconds for the TOU write confirmation...") await asyncio.sleep(15) print("Toggling TOU off for quick test...") diff --git a/src/nwp500/models/tou.py b/src/nwp500/models/tou.py index 124665a8..d127d442 100644 --- a/src/nwp500/models/tou.py +++ b/src/nwp500/models/tou.py @@ -145,11 +145,12 @@ def canonical_key( class TOUReservationSchedule(NavienBaseModel): """TOU schedule as returned by the MQTT ``tou/rd`` response topic. - This model matches the raw MQTT payload for both - :meth:`~nwp500.NavienMqttClient.request_tou_settings` read responses - and :meth:`~nwp500.NavienMqttClient.configure_tou_schedule` write - confirmations — both use ``CommandCode.TOU_RESERVATION`` and the - ``tou/rd`` response topic. + This model matches the raw MQTT payload the device sends to confirm a + :meth:`~nwp500.NavienMqttClient.configure_tou_schedule` write. The device + has no MQTT read for its TOU schedule - ``ctrl/tou/rd`` with + ``CommandCode.TOU_RESERVATION`` is the write - so this payload only ever + arrives as a write confirmation. To read the current schedule, use the + REST :meth:`~nwp500.NavienAPIClient.get_tou_info`. The payload structure is:: diff --git a/src/nwp500/mqtt/_control_commands.py b/src/nwp500/mqtt/_control_commands.py index 2c1b5bd1..b1129a6e 100644 --- a/src/nwp500/mqtt/_control_commands.py +++ b/src/nwp500/mqtt/_control_commands.py @@ -99,14 +99,6 @@ async def configure_tou_schedule( device, controller_serial_number, periods, enabled=enabled ) - async def request_tou_settings( - self, device: Device, controller_serial_number: str - ) -> int: - """Request the current TOU settings from the device.""" - return await self._device_controller.request_tou_settings( - device, controller_serial_number - ) - async def set_tou_enabled(self, device: Device, enabled: bool) -> int: """Enable or disable Time-of-Use optimization.""" return await self._device_controller.set_tou_enabled(device, enabled) diff --git a/src/nwp500/mqtt/_device_subscriptions.py b/src/nwp500/mqtt/_device_subscriptions.py index d353aa10..1fa32fb9 100644 --- a/src/nwp500/mqtt/_device_subscriptions.py +++ b/src/nwp500/mqtt/_device_subscriptions.py @@ -199,9 +199,10 @@ async def subscribe_tou_response( Subscribes to the ``tou/rd`` response topic for the given device. The callback receives a fully-parsed - :class:`~nwp500.models.TOUReservationSchedule` whenever the device - responds to a TOU read or configure request (triggered by - :meth:`request_tou_settings` or :meth:`configure_tou_schedule`). + :class:`~nwp500.models.TOUReservationSchedule` when the device + confirms a TOU write (triggered by :meth:`configure_tou_schedule`). + There is no MQTT read to subscribe to; to read the current schedule, + use the REST :meth:`~nwp500.NavienAPIClient.get_tou_info`. Args: device: Device whose TOU responses to receive. diff --git a/src/nwp500/mqtt/control.py b/src/nwp500/mqtt/control.py index 65b3c93c..f97e850d 100644 --- a/src/nwp500/mqtt/control.py +++ b/src/nwp500/mqtt/control.py @@ -539,38 +539,6 @@ async def configure_tou_schedule( reservation=reservation_payload, ) - async def request_tou_settings( - self, - device: Device, - controller_serial_number: str, - ) -> int: - """ - Request current Time-of-Use schedule from the device. - - Args: - device: Device object - controller_serial_number: Controller serial number - - Returns: - Publish packet ID - - Raises: - ValueError: If controller_serial_number is empty - """ - if not controller_serial_number: - raise ParameterValidationError( - "controller_serial_number is required", - parameter="controller_serial_number", - ) - - return await self._send_command( - device=device, - command_code=CommandCode.TOU_RESERVATION, - topic_suffix="ctrl/tou/rd", - response_topic_suffix="tou/rd", - controllerSerialNumber=controller_serial_number, - ) - @requires_capability("program_reservation_use") async def set_tou_enabled(self, device: Device, enabled: bool) -> int: """Toggle Time-of-Use functionality.""" diff --git a/src/nwp500/mqtt/subscriptions.py b/src/nwp500/mqtt/subscriptions.py index 0d97e3de..ccbdd3ce 100644 --- a/src/nwp500/mqtt/subscriptions.py +++ b/src/nwp500/mqtt/subscriptions.py @@ -816,10 +816,11 @@ async def subscribe_tou_response( Subscribes to the ``tou/rd`` response topic for the given device. The callback receives a fully-parsed - :class:`~nwp500.models.TOUReservationSchedule` whenever the device - responds to a TOU read or configure request (triggered by - :meth:`~nwp500.NavienMqttClient.request_tou_settings` or - :meth:`~nwp500.NavienMqttClient.configure_tou_schedule`). + :class:`~nwp500.models.TOUReservationSchedule` when the device + confirms a TOU write (triggered by + :meth:`~nwp500.NavienMqttClient.configure_tou_schedule`). There is no + MQTT read to subscribe to; to read the current schedule, use the REST + :meth:`~nwp500.NavienAPIClient.get_tou_info`. Args: device: Device whose TOU responses to receive. diff --git a/tests/test_public_api.py b/tests/test_public_api.py index 6dd02b6d..8695a0e8 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -59,3 +59,16 @@ def test_encoding_helpers_importable_from_submodule(): assert callable(decode_price) assert callable(encode_week_bitfield) assert callable(decode_week_bitfield) + + +def test_mqtt_tou_read_is_gone(): + """There is no MQTT read for the TOU schedule; ctrl/tou/rd is the write. + + ``request_tou_settings`` published a TOU_RESERVATION control message with + no schedule and waited for a reply the device never sends. Reads go + through ``NavienAPIClient.get_tou_info``. + """ + from nwp500 import NavienAPIClient, NavienMqttClient + + assert not hasattr(NavienMqttClient, "request_tou_settings") + assert hasattr(NavienAPIClient, "get_tou_info") From 4d96cd56de4d180d902895318368da57a8b581ac Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Sat, 29 Aug 2026 07:19:52 -0700 Subject: [PATCH 2/2] Make the TOU docstrings and read example self-consistent Two subscribe_tou_response docstrings kept a summary line calling these 'schedule read responses' while the body below said write confirmations; the summaries now say write confirmations too, as does the method entry in the client reference. The rewritten Example 3 used an undefined controller_serial while claiming no MQTT connection was needed. Both halves are now honest: the read is REST, but it is keyed by the controller serial, and the MQTT device-info response is the only thing that publishes it. The example fetches it once in a helper and takes a cached value; the parameter list says where it comes from. No REST endpoint returns it - the vendor app sends /device/tou a serial it already holds. Example 1 read it as feature.controllerSerialNumber, an attribute the model does not have; corrected while in the same file. Claude-Session: https://claude.ai/code/session_01XVj9BYvuLj7Th3iFUVoeCn --- docs/how-to/optimize-tou.rst | 37 ++++++++++++++++++++--- docs/reference/python_api/mqtt_client.rst | 2 +- src/nwp500/mqtt/_device_subscriptions.py | 4 +-- src/nwp500/mqtt/subscriptions.py | 4 +-- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/docs/how-to/optimize-tou.rst b/docs/how-to/optimize-tou.rst index 555750ec..2b7a970a 100644 --- a/docs/how-to/optimize-tou.rst +++ b/docs/how-to/optimize-tou.rst @@ -358,7 +358,9 @@ schedule with its pricing intervals. * ``mac_address``: Device MAC address * ``additional_value``: Additional device identifier -* ``controller_id``: Controller serial number +* ``controller_id``: Controller serial number. Only the MQTT device-info + response publishes it (``DeviceFeature.controller_serial_number``); it is a + hardware property, so fetch it once and cache it. See Example 3. * ``user_type``: User type (default ``"O"``) .. note:: @@ -535,7 +537,7 @@ Configure two rate periods - off-peak and peak pricing: await mqtt_client.subscribe_device_feature(device, capture_feature) await mqtt_client.request_device_info(device) feature = await asyncio.wait_for(feature_future, timeout=15) - controller_serial = feature.controllerSerialNumber + controller_serial = feature.controller_serial_number # Define off-peak period (midnight to 2 PM, weekdays) off_peak = build_tou_period( @@ -659,18 +661,43 @@ Configure different rates for summer and winter: Example 3: Retrieve Current TOU Settings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Read the stored TOU plan over the REST API. No MQTT connection is needed: +The stored TOU plan is read over the REST API. The read itself needs no MQTT +connection, but it is keyed by the controller serial number, and the only +place that is published is the MQTT device-info response. The serial is a +hardware property that never changes, so fetch it once, keep it, and every +later read is pure REST: .. code-block:: python + import asyncio + from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient from nwp500.encoding import decode_price, decode_week_bitfield - async def check_tou_settings(): + async def get_controller_serial(auth_client, device) -> str: + """One-time lookup: the controller serial is only published over MQTT.""" + mqtt_client = NavienMqttClient(auth_client) + await mqtt_client.connect() + try: + feature_future = asyncio.Future() + + def capture_feature(feature): + if not feature_future.done(): + feature_future.set_result(feature) + + await mqtt_client.subscribe_device_feature(device, capture_feature) + await mqtt_client.request_device_info(device) + feature = await asyncio.wait_for(feature_future, timeout=15) + return feature.controller_serial_number + finally: + await mqtt_client.disconnect() + + async def check_tou_settings(controller_serial: str | None = None): async with NavienAuthClient("user@example.com", "password") as auth_client: api_client = NavienAPIClient(auth_client=auth_client) device = await api_client.get_first_device() - # ... get controller_serial (same as Example 1) ... + if controller_serial is None: + controller_serial = await get_controller_serial(auth_client, device) tou_info = await api_client.get_tou_info( mac_address=device.device_info.mac_address, diff --git a/docs/reference/python_api/mqtt_client.rst b/docs/reference/python_api/mqtt_client.rst index 3eb2f223..98ffc43d 100644 --- a/docs/reference/python_api/mqtt_client.rst +++ b/docs/reference/python_api/mqtt_client.rst @@ -587,7 +587,7 @@ subscribe_tou_response() .. py:method:: subscribe_tou_response(device, callback) - Subscribe to parsed TOU schedule responses. + Subscribe to parsed TOU schedule write confirmations. The callback is invoked with a :class:`~nwp500.models.TOUReservationSchedule` when the device confirms a :meth:`configure_tou_schedule` write on the diff --git a/src/nwp500/mqtt/_device_subscriptions.py b/src/nwp500/mqtt/_device_subscriptions.py index 1fa32fb9..bdb2fad7 100644 --- a/src/nwp500/mqtt/_device_subscriptions.py +++ b/src/nwp500/mqtt/_device_subscriptions.py @@ -194,8 +194,8 @@ async def subscribe_tou_response( device: Device, callback: Callable[[TOUReservationSchedule], None], ) -> int: - """Subscribe to Time-of-Use schedule read responses with automatic - parsing. + """Subscribe to Time-of-Use schedule write confirmations with + automatic parsing. Subscribes to the ``tou/rd`` response topic for the given device. The callback receives a fully-parsed diff --git a/src/nwp500/mqtt/subscriptions.py b/src/nwp500/mqtt/subscriptions.py index ccbdd3ce..9a28e7e1 100644 --- a/src/nwp500/mqtt/subscriptions.py +++ b/src/nwp500/mqtt/subscriptions.py @@ -811,8 +811,8 @@ async def subscribe_tou_response( device: Device, callback: Callable[[TOUReservationSchedule], None], ) -> int: - """Subscribe to Time-of-Use schedule read responses with automatic - parsing. + """Subscribe to Time-of-Use schedule write confirmations with + automatic parsing. Subscribes to the ``tou/rd`` response topic for the given device. The callback receives a fully-parsed