diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3f6cd80..3e89dda 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,6 +25,16 @@ Changed 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 of ``NavienMqttClient`` into + two focused mixins, ``DeviceControlCommandsMixin`` + (``mqtt/_control_commands.py``) and ``DeviceSubscriptionsMixin`` + (``mqtt/_device_subscriptions.py``). ``NavienMqttClient`` now inherits + both, so its public API is unchanged, while ``mqtt/client.py`` shrinks + 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.py`` and ``mqtt_events.py`` are not two competing event mechanisms. diff --git a/src/nwp500/mqtt/_control_commands.py b/src/nwp500/mqtt/_control_commands.py new file mode 100644 index 0000000..2c1b5bd --- /dev/null +++ b/src/nwp500/mqtt/_control_commands.py @@ -0,0 +1,217 @@ +"""Device control command proxies for :class:`NavienMqttClient`. + +This mixin holds the thin public convenience methods that forward Navien +device-control commands to :class:`~nwp500.mqtt.control.MqttDeviceController`. +Splitting them out of ``client.py`` keeps :class:`NavienMqttClient` focused on +connection orchestration while preserving its public API surface. +""" + +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any + +from .control import MqttDeviceController + +if TYPE_CHECKING: + from ..models import ( + Device, + OtaCommitPayload, + RecirculationSchedule, + WeeklyReservationSchedule, + ) + +__author__ = "Emmanuel Levijarvi" +__copyright__ = "Emmanuel Levijarvi" +__license__ = "MIT" + + +class DeviceControlCommandsMixin: + """Public device-control commands that delegate to the controller.""" + + # Provided by NavienMqttClient.__init__. + _device_controller: MqttDeviceController + + async def request_device_status(self, device: Device) -> int: + """Request general device status.""" + return await self._device_controller.request_device_status(device) + + async def request_device_info(self, device: Device) -> int: + """Request device information (features, firmware, etc.).""" + return await self._device_controller.request_device_info(device) + + async def set_power(self, device: Device, power_on: bool) -> int: + """Turn device on or off.""" + return await self._device_controller.set_power(device, power_on) + + async def set_dhw_mode( + self, device: Device, mode_id: int, vacation_days: int | None = None + ) -> int: + """Set DHW operation mode.""" + return await self._device_controller.set_dhw_mode( + device, mode_id, vacation_days + ) + + async def enable_anti_legionella( + self, device: Device, period_days: int + ) -> int: + """Enable Anti-Legionella disinfection.""" + return await self._device_controller.enable_anti_legionella( + device, period_days + ) + + async def disable_anti_legionella(self, device: Device) -> int: + """Disable the Anti-Legionella disinfection cycle.""" + return await self._device_controller.disable_anti_legionella(device) + + async def set_dhw_temperature( + self, device: Device, temperature: float + ) -> int: + """Set DHW target temperature in the user's preferred unit.""" + return await self._device_controller.set_dhw_temperature( + device, temperature + ) + + async def update_reservations( + self, + device: Device, + reservations: Sequence[dict[str, Any]], + *, + enabled: bool = True, + ) -> int: + """Update programmed reservations.""" + return await self._device_controller.update_reservations( + device, reservations, enabled=enabled + ) + + async def request_reservations(self, device: Device) -> int: + """Request the current reservation program from the device.""" + return await self._device_controller.request_reservations(device) + + async def configure_tou_schedule( + self, + device: Device, + controller_serial_number: str, + periods: Sequence[dict[str, Any]], + *, + enabled: bool = True, + ) -> int: + """Configure the Time-of-Use rate schedule.""" + return await self._device_controller.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) + + async def request_energy_usage( + self, device: Device, year: int, months: list[int] + ) -> int: + """Request daily energy usage data for specified month(s).""" + return await self._device_controller.request_energy_usage( + device, year, months + ) + + async def signal_app_connection(self, device: Device) -> int: + """Signal that the app has connected.""" + return await self._device_controller.signal_app_connection(device) + + async def enable_demand_response(self, device: Device) -> int: + """Enable utility demand response participation.""" + return await self._device_controller.enable_demand_response(device) + + async def disable_demand_response(self, device: Device) -> int: + """Disable utility demand response participation.""" + return await self._device_controller.disable_demand_response(device) + + async def reset_air_filter(self, device: Device) -> int: + """Reset air filter maintenance timer.""" + return await self._device_controller.reset_air_filter(device) + + async def set_vacation_days(self, device: Device, days: int) -> int: + """Set vacation/away mode duration (1-30 days).""" + return await self._device_controller.set_vacation_days(device, days) + + async def update_weekly_reservation( + self, device: Device, schedule: WeeklyReservationSchedule + ) -> int: + """Configure the weekly temperature reservation schedule.""" + return await self._device_controller.update_weekly_reservation( + device, schedule + ) + + async def configure_reservation_water_program(self, device: Device) -> int: + """Enable/configure water program reservation mode.""" + controller = self._device_controller + return await controller.configure_reservation_water_program(device) + + async def configure_recirculation_schedule( + self, device: Device, schedule: RecirculationSchedule + ) -> int: + """Configure the recirculation pump timed schedule.""" + return await self._device_controller.configure_recirculation_schedule( + device, schedule + ) + + async def set_recirculation_mode(self, device: Device, mode: int) -> int: + """Set recirculation pump operation mode (1-4).""" + return await self._device_controller.set_recirculation_mode( + device, mode + ) + + async def trigger_recirculation_hot_button(self, device: Device) -> int: + """Manually trigger the recirculation pump hot button.""" + return await self._device_controller.trigger_recirculation_hot_button( + device + ) + + async def check_firmware_update(self, device: Device) -> int: + """Check for available over-the-air firmware updates.""" + return await self._device_controller.check_firmware_update(device) + + async def commit_firmware_update( + self, device: Device, payload: OtaCommitPayload + ) -> int: + """Commit a previously downloaded firmware update.""" + return await self._device_controller.commit_firmware_update( + device, payload + ) + + async def reconnect_wifi(self, device: Device) -> int: + """Trigger a WiFi reconnection on the device.""" + return await self._device_controller.reconnect_wifi(device) + + async def reset_wifi(self, device: Device) -> int: + """Reset WiFi settings to factory defaults.""" + return await self._device_controller.reset_wifi(device) + + async def set_freeze_protection_temperature( + self, device: Device, temperature: float + ) -> int: + """Set the freeze protection activation temperature.""" + return await self._device_controller.set_freeze_protection_temperature( + device, temperature + ) + + async def run_smart_diagnostic(self, device: Device) -> int: + """Trigger the smart diagnostic routine on the device.""" + return await self._device_controller.run_smart_diagnostic(device) + + async def enable_intelligent_scheduling(self, device: Device) -> int: + """Enable intelligent/adaptive heating mode.""" + return await self._device_controller.enable_intelligent_scheduling( + device + ) + + async def disable_intelligent_scheduling(self, device: Device) -> int: + """Disable intelligent/adaptive heating mode.""" + return await self._device_controller.disable_intelligent_scheduling( + device + ) diff --git a/src/nwp500/mqtt/_device_subscriptions.py b/src/nwp500/mqtt/_device_subscriptions.py new file mode 100644 index 0000000..d353aa1 --- /dev/null +++ b/src/nwp500/mqtt/_device_subscriptions.py @@ -0,0 +1,227 @@ +"""Typed device-subscription proxies for :class:`NavienMqttClient`. + +This mixin holds the public ``subscribe_*``/``unsubscribe_*`` convenience +methods that forward to the +:class:`~nwp500.mqtt.subscriptions.MqttSubscriptionManager`. +Splitting them out of ``client.py`` keeps :class:`NavienMqttClient` focused on +connection orchestration while preserving its public API surface. +""" + +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, cast + +from ..exceptions import MqttNotConnectedError +from .subscriptions import MqttSubscriptionManager + +if TYPE_CHECKING: + from ..models import ( + Device, + DeviceFeature, + DeviceStatus, + EnergyUsageResponse, + RecirculationSchedule, + ReservationSchedule, + TOUReservationSchedule, + WeeklyReservationSchedule, + ) + +__author__ = "Emmanuel Levijarvi" +__copyright__ = "Emmanuel Levijarvi" +__license__ = "MIT" + + +class DeviceSubscriptionsMixin: + """Public typed device subscriptions that delegate to the manager.""" + + # Provided by NavienMqttClient.__init__. + _connected: bool + _subscription_manager: MqttSubscriptionManager | None + + async def subscribe_device( + self, device: Device, callback: Callable[[str, dict[str, Any]], None] + ) -> int: + """ + Subscribe to all messages from a specific device. + + Args: + device: Device object + callback: Message handler + + Returns: + Subscription packet ID + """ + if not self._connected or not self._subscription_manager: + raise MqttNotConnectedError("Not connected to MQTT broker") + + # Delegate to subscription manager + return await self._subscription_manager.subscribe_device( + device, callback + ) + + async def _delegate_subscription(self, method_name: str, *args: Any) -> int: + """Helper to delegate subscription to subscription manager.""" + if not self._connected or not self._subscription_manager: + raise MqttNotConnectedError("Not connected to MQTT broker") + method = getattr(self._subscription_manager, method_name) + return cast(int, await method(*args)) + + async def subscribe_device_status( + self, device: Device, callback: Callable[[DeviceStatus], None] + ) -> int: + """Subscribe to device status messages with automatic parsing.""" + return await self._delegate_subscription( + "subscribe_device_status", device, callback + ) + + async def unsubscribe_device_status( + self, device: Device, callback: Callable[[DeviceStatus], None] + ) -> None: + """Unsubscribe a specific device status callback.""" + if not self._connected or not self._subscription_manager: + return + await self._subscription_manager.unsubscribe_device_status( + device, callback + ) + + async def subscribe_device_feature( + self, device: Device, callback: Callable[[DeviceFeature], None] + ) -> int: + """Subscribe to device feature/info messages with automatic parsing.""" + return await self._delegate_subscription( + "subscribe_device_feature", device, callback + ) + + async def unsubscribe_device_feature( + self, device: Device, callback: Callable[[DeviceFeature], None] + ) -> None: + """Unsubscribe a specific device feature callback.""" + if not self._connected or not self._subscription_manager: + return + await self._subscription_manager.unsubscribe_device_feature( + device, callback + ) + + async def subscribe_energy_usage( + self, + device: Device, + callback: Callable[[EnergyUsageResponse], None], + ) -> int: + """Subscribe to energy usage query responses with automatic parsing.""" + return await self._delegate_subscription( + "subscribe_energy_usage", device, callback + ) + + async def unsubscribe_energy_usage( + self, + device: Device, + callback: Callable[[EnergyUsageResponse], None], + ) -> None: + """Unsubscribe a specific energy usage callback.""" + if not self._connected or not self._subscription_manager: + return + await self._subscription_manager.unsubscribe_energy_usage( + device, callback + ) + + async def subscribe_reservation_response( + self, + device: Device, + callback: Callable[[ReservationSchedule], None], + ) -> int: + """Subscribe to reservation read responses with automatic parsing.""" + return await self._delegate_subscription( + "subscribe_reservation_response", device, callback + ) + + async def unsubscribe_reservation_response( + self, + device: Device, + callback: Callable[[ReservationSchedule], None], + ) -> None: + """Unsubscribe a specific reservation response callback.""" + if not self._connected or not self._subscription_manager: + return + await self._subscription_manager.unsubscribe_reservation_response( + device, callback + ) + + async def subscribe_weekly_reservation_response( + self, + device: Device, + callback: Callable[[WeeklyReservationSchedule], None], + ) -> int: + """Subscribe to weekly reservation read responses.""" + return await self._delegate_subscription( + "subscribe_weekly_reservation_response", device, callback + ) + + async def unsubscribe_weekly_reservation_response( + self, + device: Device, + callback: Callable[[WeeklyReservationSchedule], None], + ) -> None: + """Unsubscribe a specific weekly reservation callback.""" + if not self._connected or not self._subscription_manager: + return + manager = self._subscription_manager + await manager.unsubscribe_weekly_reservation_response(device, callback) + + async def subscribe_recirculation_schedule_response( + self, + device: Device, + callback: Callable[[RecirculationSchedule], None], + ) -> int: + """Subscribe to recirculation schedule read responses.""" + return await self._delegate_subscription( + "subscribe_recirculation_schedule_response", device, callback + ) + + async def unsubscribe_recirculation_schedule_response( + self, + device: Device, + callback: Callable[[RecirculationSchedule], None], + ) -> None: + """Unsubscribe a specific recirculation schedule callback.""" + if not self._connected or not self._subscription_manager: + return + manager = self._subscription_manager + await manager.unsubscribe_recirculation_schedule_response( + device, callback + ) + + async def subscribe_tou_response( + self, + device: Device, + callback: Callable[[TOUReservationSchedule], None], + ) -> int: + """Subscribe to Time-of-Use schedule read responses with automatic + parsing. + + 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`). + + Args: + device: Device whose TOU responses to receive. + callback: Called with the parsed schedule on each response. + + Returns: + Publish packet ID from the MQTT subscribe call. + """ + return await self._delegate_subscription( + "subscribe_tou_response", device, callback + ) + + async def unsubscribe_tou_response( + self, + device: Device, + callback: Callable[[TOUReservationSchedule], None], + ) -> None: + """Unsubscribe a specific TOU response callback.""" + if not self._connected or not self._subscription_manager: + return + await self._subscription_manager.unsubscribe_tou_response( + device, callback + ) diff --git a/src/nwp500/mqtt/client.py b/src/nwp500/mqtt/client.py index 2335ffe..7db623e 100644 --- a/src/nwp500/mqtt/client.py +++ b/src/nwp500/mqtt/client.py @@ -13,8 +13,8 @@ import concurrent.futures import logging import uuid -from collections.abc import Callable, Sequence -from typing import TYPE_CHECKING, Any, cast +from collections.abc import Callable +from typing import TYPE_CHECKING, Any from awscrt.exceptions import AwsCrtError @@ -34,6 +34,8 @@ MqttClientEvents, ) from ..unit_system import UnitSystemType +from ._control_commands import DeviceControlCommandsMixin +from ._device_subscriptions import DeviceSubscriptionsMixin from .command_queue import MqttCommandQueue from .connection import MqttConnection from .control import MqttDeviceController @@ -51,13 +53,6 @@ from ..models import ( Device, DeviceFeature, - DeviceStatus, - EnergyUsageResponse, - OtaCommitPayload, - RecirculationSchedule, - ReservationSchedule, - TOUReservationSchedule, - WeeklyReservationSchedule, ) __author__ = "Emmanuel Levijarvi" @@ -92,7 +87,9 @@ def _log_scheduled_coroutine_result( ) -class NavienMqttClient(EventEmitter): +class NavienMqttClient( + DeviceControlCommandsMixin, DeviceSubscriptionsMixin, EventEmitter +): """ Async MQTT client for Navien device communication over AWS IoT. @@ -999,387 +996,6 @@ async def publish( retriable=True, ) from e - # Navien-specific convenience methods - - async def subscribe_device( - self, device: Device, callback: Callable[[str, dict[str, Any]], None] - ) -> int: - """ - Subscribe to all messages from a specific device. - - Args: - device: Device object - callback: Message handler - - Returns: - Subscription packet ID - """ - if not self._connected or not self._subscription_manager: - raise MqttNotConnectedError("Not connected to MQTT broker") - - # Delegate to subscription manager - return await self._subscription_manager.subscribe_device( - device, callback - ) - - async def _delegate_subscription(self, method_name: str, *args: Any) -> int: - """Helper to delegate subscription to subscription manager.""" - if not self._connected or not self._subscription_manager: - raise MqttNotConnectedError("Not connected to MQTT broker") - method = getattr(self._subscription_manager, method_name) - return cast(int, await method(*args)) - - async def subscribe_device_status( - self, device: Device, callback: Callable[[DeviceStatus], None] - ) -> int: - """Subscribe to device status messages with automatic parsing.""" - return await self._delegate_subscription( - "subscribe_device_status", device, callback - ) - - async def unsubscribe_device_status( - self, device: Device, callback: Callable[[DeviceStatus], None] - ) -> None: - """Unsubscribe a specific device status callback.""" - if not self._connected or not self._subscription_manager: - return - await self._subscription_manager.unsubscribe_device_status( - device, callback - ) - - async def subscribe_device_feature( - self, device: Device, callback: Callable[[DeviceFeature], None] - ) -> int: - """Subscribe to device feature/info messages with automatic parsing.""" - return await self._delegate_subscription( - "subscribe_device_feature", device, callback - ) - - async def unsubscribe_device_feature( - self, device: Device, callback: Callable[[DeviceFeature], None] - ) -> None: - """Unsubscribe a specific device feature callback.""" - if not self._connected or not self._subscription_manager: - return - await self._subscription_manager.unsubscribe_device_feature( - device, callback - ) - - async def subscribe_energy_usage( - self, - device: Device, - callback: Callable[[EnergyUsageResponse], None], - ) -> int: - """Subscribe to energy usage query responses with automatic parsing.""" - return await self._delegate_subscription( - "subscribe_energy_usage", device, callback - ) - - async def unsubscribe_energy_usage( - self, - device: Device, - callback: Callable[[EnergyUsageResponse], None], - ) -> None: - """Unsubscribe a specific energy usage callback.""" - if not self._connected or not self._subscription_manager: - return - await self._subscription_manager.unsubscribe_energy_usage( - device, callback - ) - - async def subscribe_reservation_response( - self, - device: Device, - callback: Callable[[ReservationSchedule], None], - ) -> int: - """Subscribe to reservation read responses with automatic parsing.""" - return await self._delegate_subscription( - "subscribe_reservation_response", device, callback - ) - - async def unsubscribe_reservation_response( - self, - device: Device, - callback: Callable[[ReservationSchedule], None], - ) -> None: - """Unsubscribe a specific reservation response callback.""" - if not self._connected or not self._subscription_manager: - return - await self._subscription_manager.unsubscribe_reservation_response( - device, callback - ) - - async def subscribe_weekly_reservation_response( - self, - device: Device, - callback: Callable[[WeeklyReservationSchedule], None], - ) -> int: - """Subscribe to weekly reservation read responses.""" - return await self._delegate_subscription( - "subscribe_weekly_reservation_response", device, callback - ) - - async def unsubscribe_weekly_reservation_response( - self, - device: Device, - callback: Callable[[WeeklyReservationSchedule], None], - ) -> None: - """Unsubscribe a specific weekly reservation callback.""" - if not self._connected or not self._subscription_manager: - return - manager = self._subscription_manager - await manager.unsubscribe_weekly_reservation_response(device, callback) - - async def subscribe_recirculation_schedule_response( - self, - device: Device, - callback: Callable[[RecirculationSchedule], None], - ) -> int: - """Subscribe to recirculation schedule read responses.""" - return await self._delegate_subscription( - "subscribe_recirculation_schedule_response", device, callback - ) - - async def unsubscribe_recirculation_schedule_response( - self, - device: Device, - callback: Callable[[RecirculationSchedule], None], - ) -> None: - """Unsubscribe a specific recirculation schedule callback.""" - if not self._connected or not self._subscription_manager: - return - manager = self._subscription_manager - await manager.unsubscribe_recirculation_schedule_response( - device, callback - ) - - async def subscribe_tou_response( - self, - device: Device, - callback: Callable[[TOUReservationSchedule], None], - ) -> int: - """Subscribe to Time-of-Use schedule read responses with automatic - parsing. - - 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`). - - Args: - device: Device whose TOU responses to receive. - callback: Called with the parsed schedule on each response. - - Returns: - Publish packet ID from the MQTT subscribe call. - """ - return await self._delegate_subscription( - "subscribe_tou_response", device, callback - ) - - async def unsubscribe_tou_response( - self, - device: Device, - callback: Callable[[TOUReservationSchedule], None], - ) -> None: - """Unsubscribe a specific TOU response callback.""" - if not self._connected or not self._subscription_manager: - return - await self._subscription_manager.unsubscribe_tou_response( - device, callback - ) - - # ------------------------------------------------------------------------- - # Device control proxies (delegate to self.control) - # ------------------------------------------------------------------------- - - async def request_device_status(self, device: Device) -> int: - """Request general device status.""" - return await self._device_controller.request_device_status(device) - - async def request_device_info(self, device: Device) -> int: - """Request device information (features, firmware, etc.).""" - return await self._device_controller.request_device_info(device) - - async def set_power(self, device: Device, power_on: bool) -> int: - """Turn device on or off.""" - return await self._device_controller.set_power(device, power_on) - - async def set_dhw_mode( - self, device: Device, mode_id: int, vacation_days: int | None = None - ) -> int: - """Set DHW operation mode.""" - return await self._device_controller.set_dhw_mode( - device, mode_id, vacation_days - ) - - async def enable_anti_legionella( - self, device: Device, period_days: int - ) -> int: - """Enable Anti-Legionella disinfection.""" - return await self._device_controller.enable_anti_legionella( - device, period_days - ) - - async def disable_anti_legionella(self, device: Device) -> int: - """Disable the Anti-Legionella disinfection cycle.""" - return await self._device_controller.disable_anti_legionella(device) - - async def set_dhw_temperature( - self, device: Device, temperature: float - ) -> int: - """Set DHW target temperature in the user's preferred unit.""" - return await self._device_controller.set_dhw_temperature( - device, temperature - ) - - async def update_reservations( - self, - device: Device, - reservations: Sequence[dict[str, Any]], - *, - enabled: bool = True, - ) -> int: - """Update programmed reservations.""" - return await self._device_controller.update_reservations( - device, reservations, enabled=enabled - ) - - async def request_reservations(self, device: Device) -> int: - """Request the current reservation program from the device.""" - return await self._device_controller.request_reservations(device) - - async def configure_tou_schedule( - self, - device: Device, - controller_serial_number: str, - periods: Sequence[dict[str, Any]], - *, - enabled: bool = True, - ) -> int: - """Configure the Time-of-Use rate schedule.""" - return await self._device_controller.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) - - async def request_energy_usage( - self, device: Device, year: int, months: list[int] - ) -> int: - """Request daily energy usage data for specified month(s).""" - return await self._device_controller.request_energy_usage( - device, year, months - ) - - async def signal_app_connection(self, device: Device) -> int: - """Signal that the app has connected.""" - return await self._device_controller.signal_app_connection(device) - - async def enable_demand_response(self, device: Device) -> int: - """Enable utility demand response participation.""" - return await self._device_controller.enable_demand_response(device) - - async def disable_demand_response(self, device: Device) -> int: - """Disable utility demand response participation.""" - return await self._device_controller.disable_demand_response(device) - - async def reset_air_filter(self, device: Device) -> int: - """Reset air filter maintenance timer.""" - return await self._device_controller.reset_air_filter(device) - - async def set_vacation_days(self, device: Device, days: int) -> int: - """Set vacation/away mode duration (1-30 days).""" - return await self._device_controller.set_vacation_days(device, days) - - async def update_weekly_reservation( - self, device: Device, schedule: WeeklyReservationSchedule - ) -> int: - """Configure the weekly temperature reservation schedule.""" - return await self._device_controller.update_weekly_reservation( - device, schedule - ) - - async def configure_reservation_water_program(self, device: Device) -> int: - """Enable/configure water program reservation mode.""" - controller = self._device_controller - return await controller.configure_reservation_water_program(device) - - async def configure_recirculation_schedule( - self, device: Device, schedule: RecirculationSchedule - ) -> int: - """Configure the recirculation pump timed schedule.""" - return await self._device_controller.configure_recirculation_schedule( - device, schedule - ) - - async def set_recirculation_mode(self, device: Device, mode: int) -> int: - """Set recirculation pump operation mode (1-4).""" - return await self._device_controller.set_recirculation_mode( - device, mode - ) - - async def trigger_recirculation_hot_button(self, device: Device) -> int: - """Manually trigger the recirculation pump hot button.""" - return await self._device_controller.trigger_recirculation_hot_button( - device - ) - - async def check_firmware_update(self, device: Device) -> int: - """Check for available over-the-air firmware updates.""" - return await self._device_controller.check_firmware_update(device) - - async def commit_firmware_update( - self, device: Device, payload: OtaCommitPayload - ) -> int: - """Commit a previously downloaded firmware update.""" - return await self._device_controller.commit_firmware_update( - device, payload - ) - - async def reconnect_wifi(self, device: Device) -> int: - """Trigger a WiFi reconnection on the device.""" - return await self._device_controller.reconnect_wifi(device) - - async def reset_wifi(self, device: Device) -> int: - """Reset WiFi settings to factory defaults.""" - return await self._device_controller.reset_wifi(device) - - async def set_freeze_protection_temperature( - self, device: Device, temperature: float - ) -> int: - """Set the freeze protection activation temperature.""" - return await self._device_controller.set_freeze_protection_temperature( - device, temperature - ) - - async def run_smart_diagnostic(self, device: Device) -> int: - """Trigger the smart diagnostic routine on the device.""" - return await self._device_controller.run_smart_diagnostic(device) - - async def enable_intelligent_scheduling(self, device: Device) -> int: - """Enable intelligent/adaptive heating mode.""" - return await self._device_controller.enable_intelligent_scheduling( - device - ) - - async def disable_intelligent_scheduling(self, device: Device) -> int: - """Disable intelligent/adaptive heating mode.""" - return await self._device_controller.disable_intelligent_scheduling( - device - ) - async def ensure_device_info_cached( self, device: Device, timeout: float = 30.0 ) -> bool: