diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 57b4a0a..cf620ea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,88 @@ Changelog ========= +Version 6.2.0 (2025-12-17) +========================== + +**BREAKING CHANGES**: Enumerations refactored for type safety and consistency + +- **CommandCode moved**: Import from ``nwp500.enums`` instead of ``nwp500.constants`` + + .. code-block:: python + + # OLD (removed) + from nwp500.constants import CommandCode + + # NEW + from nwp500.enums import CommandCode + # OR + from nwp500 import CommandCode # Still works + +Added +----- + +- **Enumerations Module (``src/nwp500/enums.py``)**: Comprehensive type-safe enums for device control and status + + - Status value enums: ``OnOffFlag``, ``Operation``, ``DhwOperationSetting``, ``CurrentOperationMode``, ``HeatSource``, ``DREvent``, ``WaterLevel``, ``FilterChange``, ``RecirculationMode`` + - Time of Use enums: ``TouWeekType``, ``TouRateType`` + - Device capability enums: ``CapabilityFlag``, ``TemperatureType``, ``DeviceType`` + - Device control command enum: ``CommandCode`` (all MQTT command codes) + - Error code enum: ``ErrorCode`` with complete error code mappings + - Human-readable text mappings for all enums (e.g., ``DHW_OPERATION_SETTING_TEXT``, ``ERROR_CODE_TEXT``) + - Exported from main package: ``from nwp500 import OnOffFlag, ErrorCode, CommandCode`` + - Comprehensive documentation in ``docs/enumerations.rst`` + - Example usage in ``examples/error_code_demo.py`` + +Changed +------- + +- **Command Code Constants**: Migrated from ``constants.py`` to ``CommandCode`` enum in ``enums.py`` + + - ``ANTI_LEGIONELLA_ENABLE`` → ``CommandCode.ANTI_LEGIONELLA_ON`` + - ``ANTI_LEGIONELLA_DISABLE`` → ``CommandCode.ANTI_LEGIONELLA_OFF`` + - ``TOU_ENABLE`` → ``CommandCode.TOU_ON`` + - ``TOU_DISABLE`` → ``CommandCode.TOU_OFF`` + - ``TOU_SETTINGS`` → ``CommandCode.TOU_RESERVATION`` + - All command constants now use consistent naming in ``CommandCode`` enum + +- **Model Enumerations**: Updated type annotations for clarity and type safety + + - ``TemperatureUnit`` → ``TemperatureType`` (matches device protocol field names) + - All capability flags (e.g., ``power_use``, ``dhw_use``) now use ``CapabilityFlag`` type + - ``MqttRequest.device_type`` now accepts ``Union[DeviceType, int]`` for flexibility + +- **Model Serialization**: Enums automatically serialize to human-readable names + + - `model_dump()` converts enums to names (e.g., `DhwOperationSetting.HEAT_PUMP` → `"HEAT_PUMP"`) + - CLI and other consumers benefit from automatic enum name serialization + - Text mappings available for custom formatting (e.g., `DHW_OPERATION_TEXT[enum]` → "Heat Pump Only") + +- **Documentation**: Comprehensive updates across protocol and API documentation + + - ``docs/guides/time_of_use.rst``: Clarified TOU override status behavior (1=OFF/override active, 2=ON/normal operation) + - ``docs/protocol/data_conversions.rst``: Updated TOU field descriptions with correct enum values + - ``docs/protocol/device_features.rst``: Added capability flag pattern explanation (2=supported, 1=not supported) + - ``docs/protocol/mqtt_protocol.rst``: Updated command code references to use new enum names + - ``docs/python_api/models.rst``: Updated model field type annotations + +- **Examples**: Updated to use new enums for type-safe device control + + - ``examples/anti_legionella_example.py``: Uses ``CommandCode`` enum + - ``examples/device_feature_callback.py``: Uses capability enums + - ``examples/event_emitter_demo.py``: Uses status enums + - ``examples/mqtt_diagnostics_example.py``: Uses command enums + +- **CLI Code Cleanup**: Refactored JSON formatting to use shared utility function + + - Extracted repeated `json.dumps()` calls to `format_json_output()` helper + - Cleaner code with consistent formatting across all commands + +Fixed +----- + +- **Temperature Conversion Test**: Corrected ``test_device_status_div10`` to use ``HalfCelsiusToF`` conversion (100 → 122°F, not 50.0) +- **Documentation**: Fixed references to non-existent ``OperationMode`` enum - replaced with correct ``DhwOperationSetting`` and ``CurrentOperationMode`` enums + Version 6.1.1 (2025-12-08) ========================== @@ -238,7 +320,7 @@ Quick Example # OLD boolean and enum handling is_heating = converted["currentHeatUse"] == 2 - mode = CurrentOperationMode(converted["operationMode"]) if converted["operationMode"] in (0,32,64,96) else CurrentOperationMode.STANDBY + mode = OperationMode(converted["operationMode"]) if converted["operationMode"] in (0,32,64,96) else OperationMode.STANDBY # NEW simplified is_heating = status.current_heat_use diff --git a/docs/enumerations.rst b/docs/enumerations.rst new file mode 100644 index 0000000..28ea228 --- /dev/null +++ b/docs/enumerations.rst @@ -0,0 +1,293 @@ +Enumerations Reference +====================== + +This document provides a comprehensive reference for all enumerations used in +the Navien NWP500 protocol. + +Device Control Commands +----------------------- + +.. autoclass:: nwp500.enums.CommandCode + :members: + :undoc-members: + +These command IDs are used in MQTT control messages to change device settings +and trigger actions. The most commonly used commands include: + +- **Power Control**: ``POWER_ON``, ``POWER_OFF`` +- **Temperature**: ``DHW_TEMPERATURE`` +- **Operation Mode**: ``DHW_MODE`` +- **TOU**: ``TOU_ON``, ``TOU_OFF`` +- **Maintenance**: ``AIR_FILTER_RESET``, ``ANTI_LEGIONELLA_ON`` + +Example usage:: + + from nwp500 import CommandCode + + # Send temperature command + command = CommandCode.DHW_TEMPERATURE + params = [120] # 60°C in half-degree units + +Status Value Enumerations +-------------------------- + +OnOffFlag +~~~~~~~~~ + +.. autoclass:: nwp500.enums.OnOffFlag + :members: + :undoc-members: + +Generic on/off flag used throughout status fields for power status, TOU status, +recirculation status, vacation mode, anti-legionella, and other boolean settings. + +**Note**: Device uses ``1=OFF, 2=ON`` (not standard 0/1 boolean). + +Operation +~~~~~~~~~ + +.. autoclass:: nwp500.enums.Operation + :members: + :undoc-members: + +Device operation state indicating overall device activity. + +DhwOperationSetting +~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: nwp500.enums.DhwOperationSetting + :members: + :undoc-members: + +User-configured DHW heating mode preference. This determines which heat source(s) +the device will use when heating is needed: + +- **HEAT_PUMP**: Most efficient but slower heating +- **ELECTRIC**: Fastest but uses most energy +- **ENERGY_SAVER**: Hybrid mode - balanced efficiency +- **HIGH_DEMAND**: Hybrid mode - maximum heating capacity +- **VACATION**: Energy-saving mode for extended absences +- **POWER_OFF**: Device powered off + +Example:: + + from nwp500 import DhwOperationSetting + from nwp500.enums import DHW_OPERATION_SETTING_TEXT + + mode = DhwOperationSetting.ENERGY_SAVER + print(f"Current mode: {DHW_OPERATION_SETTING_TEXT[mode]}") # "Hybrid: Efficiency" + +CurrentOperationMode +~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: nwp500.enums.CurrentOperationMode + :members: + :undoc-members: + +Real-time operational state (read-only). This reflects what the device is actually +doing right now, which may differ from the configured mode setting: + +- **STANDBY**: Device idle, not actively heating +- **HEAT_PUMP_MODE**: Heat pump actively running +- **HYBRID_EFFICIENCY_MODE**: Actively heating in Energy Saver mode +- **HYBRID_BOOST_MODE**: Actively heating in High Demand mode + +Example:: + + from nwp500 import CurrentOperationMode + from nwp500.enums import CURRENT_OPERATION_MODE_TEXT + + mode = CurrentOperationMode.HEAT_PUMP_MODE + print(f"Device state: {CURRENT_OPERATION_MODE_TEXT[mode]}") # "Heat Pump" + +HeatSource +~~~~~~~~~~ + +.. autoclass:: nwp500.enums.HeatSource + :members: + :undoc-members: + +Currently active heat source (read-only status). This reflects what the device +is *currently* using, not what mode it's set to. In Hybrid mode, this field +shows which heat source(s) are active at any moment. + +DREvent +~~~~~~~ + +.. autoclass:: nwp500.enums.DREvent + :members: + :undoc-members: + +Demand Response event status. Allows utilities to manage grid load by signaling +water heaters to reduce consumption (shed) or pre-heat (load up) before peak periods. + +WaterLevel +~~~~~~~~~~ + +.. autoclass:: nwp500.enums.WaterLevel + :members: + :undoc-members: + +Hot water level indicator displayed as gauge in app. IDs are non-sequential, +likely represent bit positions for multi-level displays. + +FilterChange +~~~~~~~~~~~~ + +.. autoclass:: nwp500.enums.FilterChange + :members: + :undoc-members: + +Air filter status for heat pump models. Indicates when air filter maintenance +is needed. + +RecirculationMode +~~~~~~~~~~~~~~~~~ + +.. autoclass:: nwp500.enums.RecirculationMode + :members: + :undoc-members: + +Recirculation pump operation mode: + +- **ALWAYS**: Pump continuously runs +- **BUTTON**: Manual activation only (hot button) +- **SCHEDULE**: Runs on configured schedule +- **TEMPERATURE**: Activates when pipe temp drops below setpoint + +Time of Use (TOU) Enumerations +------------------------------- + +TouWeekType +~~~~~~~~~~~ + +.. autoclass:: nwp500.enums.TouWeekType + :members: + :undoc-members: + +Day grouping for TOU schedules. Allows separate schedules for weekdays and +weekends to account for different electricity rates and usage patterns. + +TouRateType +~~~~~~~~~~~ + +.. autoclass:: nwp500.enums.TouRateType + :members: + :undoc-members: + +Electricity rate period type. Device behavior can be configured for each period: + +- **OFF_PEAK**: Lowest rates - device heats aggressively +- **MID_PEAK**: Medium rates - device heats normally +- **ON_PEAK**: Highest rates - device minimizes heating + +Temperature and Unit Enumerations +---------------------------------- + +TemperatureType +~~~~~~~~~~~~~~~ + +.. autoclass:: nwp500.enums.TemperatureType + :members: + :undoc-members: + +Temperature display unit preference (Celsius or Fahrenheit). + +**Alias**: ``TemperatureUnit`` in models.py for backward compatibility. + +TempFormulaType +~~~~~~~~~~~~~~~ + +.. autoclass:: nwp500.enums.TempFormulaType + :members: + :undoc-members: + +Temperature conversion formula type. Different device models use slightly different +rounding algorithms when converting internal Celsius values to Fahrenheit: + +- **ASYMMETRIC** (Type 0): Special rounding based on raw value remainder +- **STANDARD** (Type 1): Simple round to nearest integer + +This ensures the mobile app matches the device's built-in display exactly. + +Device Type Enumerations +------------------------- + +UnitType +~~~~~~~~ + +.. autoclass:: nwp500.enums.UnitType + :members: + :undoc-members: + +Navien device/unit model types. Common values: + +- **NPF** (513): Heat pump water heater (primary model for this library) +- **NPE**: Tankless water heater +- **NCB**: Condensing boiler +- **NPN**: Condensing water heater + +Values with ``CAS_`` prefix indicate cascading systems where multiple units +work together. + +DeviceType +~~~~~~~~~~ + +.. autoclass:: nwp500.enums.DeviceType + :members: + :undoc-members: + +Communication device type (WiFi module model). + +FirmwareType +~~~~~~~~~~~~ + +.. autoclass:: nwp500.enums.FirmwareType + :members: + :undoc-members: + +Firmware component types. Devices may have multiple firmware components that +can be updated independently. + +Display Text Helpers +-------------------- + +The enums module also provides dictionaries for converting enum values to +user-friendly display text: + +.. code-block:: python + + from nwp500.enums import ( + DHW_OPERATION_SETTING_TEXT, + CURRENT_OPERATION_MODE_TEXT, + HEAT_SOURCE_TEXT, + DR_EVENT_TEXT, + RECIRC_MODE_TEXT, + TOU_RATE_TEXT, + FILTER_STATUS_TEXT, + ERROR_CODE_TEXT, + ) + + # Usage examples + from nwp500 import DhwOperationSetting, CurrentOperationMode, ErrorCode + + # User-configured mode + mode = DhwOperationSetting.ENERGY_SAVER + print(DHW_OPERATION_SETTING_TEXT[mode]) # "Hybrid: Efficiency" + + # Current operational state + state = CurrentOperationMode.HEAT_PUMP_MODE + print(CURRENT_OPERATION_MODE_TEXT[state]) # "Heat Pump" + + # Error codes + error = ErrorCode.E407_DHW_TEMP_SENSOR + print(ERROR_CODE_TEXT[error]) # "Abnormal DHW Temperature Sensor" + +Related Documentation +--------------------- + +For detailed protocol documentation, see: + +- :doc:`protocol/device_status` - Status field definitions +- :doc:`guides/time_of_use` - TOU scheduling and rate types +- :doc:`protocol/control_commands` - Control command usage diff --git a/docs/guides/time_of_use.rst b/docs/guides/time_of_use.rst index e186df0..66730aa 100644 --- a/docs/guides/time_of_use.rst +++ b/docs/guides/time_of_use.rst @@ -914,11 +914,11 @@ The device status includes TOU-related fields: { "touStatus": 1, - "touOverrideStatus": 0 + "touOverrideStatus": 2 } -* ``touStatus``: ``1`` if TOU scheduling is active, ``0`` if inactive -* ``touOverrideStatus``: ``1`` if user has temporarily overridden TOU schedule +* ``touStatus``: ``1`` if TOU scheduling is enabled/active, ``0`` if disabled/inactive +* ``touOverrideStatus``: ``2`` (ON) = TOU schedule is operating normally, ``1`` (OFF) = user has overridden TOU to force immediate heating See :doc:`../protocol/device_status` for more details. diff --git a/docs/index.rst b/docs/index.rst index bf50a3d..236ca4c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -101,6 +101,7 @@ Documentation Index python_api/api_client python_api/mqtt_client python_api/models + enumerations python_api/constants python_api/events python_api/exceptions diff --git a/docs/protocol/data_conversions.rst b/docs/protocol/data_conversions.rst index 92b363d..93115cd 100644 --- a/docs/protocol/data_conversions.rst +++ b/docs/protocol/data_conversions.rst @@ -487,7 +487,7 @@ Vacation and Scheduling Fields * - ``touStatus`` - None (direct value) - See values below - - **Time-of-Use (TOU) schedule status**. 0=inactive, 1=active. Controls heating based on electricity rate periods. + - **Time-of-Use (TOU) schedule status**. 0 = inactive/disabled, 1 = active/enabled. Controls heating based on electricity rate periods. * - ``drEventStatus`` - None (direct value) - Bitfield @@ -499,7 +499,7 @@ Vacation and Scheduling Fields * - ``touOverrideStatus`` - None (direct value) - See explanation - - **User temporary override of TOU schedule**. Similar to DR override - user can override schedule temporarily. + - **TOU schedule operation status**. 1 (OFF) = user has overridden TOU to force immediate heating (override lasts up to 72 hours), 2 (ON) = TOU schedule is operating normally. Network and Diagnostic Fields ----------------------------- diff --git a/docs/protocol/device_features.rst b/docs/protocol/device_features.rst index 9b56866..9d7abe5 100644 --- a/docs/protocol/device_features.rst +++ b/docs/protocol/device_features.rst @@ -6,7 +6,16 @@ This document lists the fields found in the ``feature`` object (also known as .. warning:: This document describes the underlying protocol details. Most users should use the Python client library (:doc:`../python_api/mqtt_client`) instead of implementing - the protocol directly.by MQTT device info requests. + the protocol directly. + +.. note:: + **Capability Flag Pattern**: All capability flags (fields ending in ``Use``) follow the same pattern as :class:`~nwp500.enums.OnOffFlag`: + + - **2 = Supported/Available** (feature is present on this device) + - **1 = Not Supported/Unavailable** (feature is not present on this device) + + This is the standard Navien protocol pattern for boolean-like values, not traditional 0/1 booleans. + The Python library automatically converts these to Python ``bool`` (True/False). The DeviceFeature data contains comprehensive device capabilities, configuration, and firmware information received via MQTT when calling ``request_device_info()``. This data is much more detailed than the basic device information available through the REST API and corresponds to the actual device specifications and capabilities as documented in the official Navien NWP500 Installation and User manuals. @@ -77,27 +86,27 @@ The DeviceFeature data contains comprehensive device capabilities, configuration * - ``powerUse`` - int - Boolean - - Power control capability (1=supported) - can be turned on/off via controls (always 1 for NWP500) + - Power control capability (2=supported, 1=not supported) - can be turned on/off via controls (always 2=supported for NWP500) - None * - ``holidayUse`` - int - Boolean - - Vacation mode support (1=supported) - energy-saving mode for 0-99 days with minimal operations + - Vacation mode support (2=supported, 1=not supported) - energy-saving mode for 0-99 days with minimal operations - None * - ``programReservationUse`` - int - Boolean - - Scheduled operation support (1=supported) - programmable heating schedules and timers + - Scheduled operation support (2=supported, 1=not supported) - programmable heating schedules and timers - None * - ``dhwUse`` - int - Boolean - - Domestic hot water functionality (1=available) - primary function of water heater (always 1) + - Domestic hot water functionality (2=supported, 1=not supported) - primary function of water heater (always 2=supported) - None * - ``dhwTemperatureSettingUse`` - int - Boolean - - Temperature adjustment capability (1=supported) - user can modify target temperature + - Temperature adjustment capability (2=supported, 1=not supported) - user can modify target temperature - None * - ``dhwTemperatureMin`` - int @@ -112,12 +121,12 @@ The DeviceFeature data contains comprehensive device capabilities, configuration * - ``smartDiagnosticUse`` - int - Boolean - - Self-diagnostic capability (1=available) - 10-minute startup diagnostic, error code system + - Self-diagnostic capability (2=supported, 1=not supported) - 10-minute startup diagnostic, error code system - None * - ``wifiRssiUse`` - int - Boolean - - WiFi signal monitoring (1=supported) - reports signal strength in dBm for connectivity diagnostics + - WiFi signal monitoring (2=supported, 1=not supported) - reports signal strength in dBm for connectivity diagnostics - None * - ``temperatureType`` - TemperatureUnit @@ -132,12 +141,12 @@ The DeviceFeature data contains comprehensive device capabilities, configuration * - ``energyUsageUse`` - int - Boolean - - Energy monitoring support (1=available) - tracks kWh consumption for heat pump and electric elements + - Energy monitoring support (2=supported, 1=not supported) - tracks kWh consumption for heat pump and electric elements - None * - ``freezeProtectionUse`` - int - Boolean - - Freeze protection capability (1=available) - automatic heating when tank drops below threshold + - Freeze protection capability (2=supported, 1=not supported) - automatic heating when tank drops below threshold - None * - ``freezeProtectionTempMin`` - int @@ -152,52 +161,52 @@ The DeviceFeature data contains comprehensive device capabilities, configuration * - ``mixingValueUse`` - int - Boolean - - Thermostatic mixing valve support (1=available) - for temperature limiting at point of use + - Thermostatic mixing valve support (2=supported, 1=not supported) - for temperature limiting at point of use - None * - ``drSettingUse`` - int - Boolean - - Demand Response support (1=available) - CTA-2045 compliance for utility load management + - Demand Response support (2=supported, 1=not supported) - CTA-2045 compliance for utility load management - None * - ``antiLegionellaSettingUse`` - int - Boolean - - Anti-Legionella function (1=available) - periodic heating to 140°F (60°C) to prevent bacteria + - Anti-Legionella function (2=supported, 1=not supported) - periodic heating to 140°F (60°C) to prevent bacteria - None * - ``hpwhUse`` - int - Boolean - - Heat Pump Water Heater mode (1=supported) - primary efficient heating method using refrigeration cycle + - Heat Pump Water Heater mode (2=supported, 1=not supported) - primary efficient heating method using refrigeration cycle - None * - ``dhwRefillUse`` - int - Boolean - - Tank refill detection (1=supported) - monitors for "dry fire" conditions during refill + - Tank refill detection (2=supported, 1=not supported) - monitors for "dry fire" conditions during refill - None * - ``ecoUse`` - int - Boolean - - ECO safety switch (1=available) - Energy Cut Off high-temperature limit protection + - ECO safety switch (2=supported, 1=not supported) - Energy Cut Off high-temperature limit protection - None * - ``electricUse`` - int - Boolean - - Electric-only mode (1=supported) - heating element only operation for maximum recovery speed + - Electric-only mode (2=supported, 1=not supported) - heating element only operation for maximum recovery speed - None * - ``heatpumpUse`` - int - Boolean - - Heat pump only mode (1=supported) - most efficient operation using only refrigeration cycle + - Heat pump only mode (2=supported, 1=not supported) - most efficient operation using only refrigeration cycle - None * - ``energySaverUse`` - int - Boolean - - Energy Saver mode (1=supported) - hybrid efficiency mode balancing speed and efficiency (default) + - Energy Saver mode (2=supported, 1=not supported) - hybrid efficiency mode balancing speed and efficiency (default) - None * - ``highDemandUse`` - int - Boolean - - High Demand mode (1=supported) - hybrid boost mode prioritizing fast recovery over efficiency + - High Demand mode (2=supported, 1=not supported) - hybrid boost mode prioritizing fast recovery over efficiency - None Operation Mode Support Matrix diff --git a/docs/protocol/mqtt_protocol.rst b/docs/protocol/mqtt_protocol.rst index ac84d1d..356dfd0 100644 --- a/docs/protocol/mqtt_protocol.rst +++ b/docs/protocol/mqtt_protocol.rst @@ -135,6 +135,11 @@ Status and Info Requests Control Commands ---------------- +These commands control device operation, settings, and special functions. + +Power Control +~~~~~~~~~~~~~ + .. list-table:: :header-rows: 1 :widths: 40 20 40 @@ -142,36 +147,197 @@ Control Commands * - Command - Code - Description - * - Power On - - 33554434 - - Turn device on * - Power Off - 33554433 - Turn device off - * - Set DHW Mode + * - Power On + - 33554434 + - Turn device on + +Operation Mode Control +~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 40 20 40 + + * - Command + - Code + - Description + * - Set DHW Operation Mode - 33554437 - - Change operation mode + - Change DHW heating mode (Heat Pump/Electric/Hybrid) * - Set DHW Temperature + - 33554464 + - Set target water temperature + +Scheduling and Reservations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 40 20 40 + + * - Command + - Code + - Description + * - Update Weekly Reservations - 33554438 - - Set target temperature - * - Enable Anti-Legionella - - 33554472 - - Enable anti-Legionella cycle - * - Disable Anti-Legionella - - 33554471 - - Disable anti-Legionella - * - Update Reservations - - 16777226 - - Update reservation schedule - * - Configure TOU + - Configure weekly temperature schedule + * - Configure TOU Schedule - 33554439 - - Configure TOU schedule - * - Enable TOU - - 33554476 - - Enable TOU optimization + - Configure Time-of-Use pricing schedule + * - Configure Recirculation Schedule + - 33554440 + - Configure recirculation pump schedule + * - Configure Water Program (Reservation Mode) + - 33554441 + - Enable/configure water program reservation mode + +Time-of-Use (TOU) Control +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 40 20 40 + + * - Command + - Code + - Description * - Disable TOU - 33554475 - Disable TOU optimization + * - Enable TOU + - 33554476 + - Enable TOU optimization + +Recirculation Pump Control +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 40 20 40 + + * - Command + - Code + - Description + * - Trigger Recirculation Hot Button + - 33554444 + - Manually activate recirculation pump + * - Set Recirculation Mode + - 33554445 + - Set recirculation operation mode + +Special Functions +~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 40 20 40 + + * - Command + - Code + - Description + * - Set Freeze Protection Temperature + - 33554451 + - Configure freeze protection activation temperature + * - Trigger Smart Diagnostic + - 33554455 + - Run smart diagnostic routine + * - Set Vacation Days + - 33554466 + - Configure vacation mode duration + * - Disable Intelligent Mode + - 33554467 + - Turn off intelligent/adaptive heating + * - Enable Intelligent Mode + - 33554468 + - Turn on intelligent/adaptive heating + +Demand Response Control +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 40 20 40 + + * - Command + - Code + - Description + * - Disable Demand Response + - 33554469 + - Disable utility demand response + * - Enable Demand Response + - 33554470 + - Enable utility demand response + +Anti-Legionella Control +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 40 20 40 + + * - Command + - Code + - Description + * - Disable Anti-Legionella + - 33554471 + - Disable anti-Legionella cycle + * - Enable Anti-Legionella + - 33554472 + - Enable anti-Legionella cycle + +Maintenance +~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 40 20 40 + + * - Command + - Code + - Description + * - Reset Air Filter + - 33554473 + - Reset air filter maintenance timer + * - Set Air Filter Life + - 33554474 + - Configure air filter replacement interval + +Firmware Updates +~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 40 20 40 + + * - Command + - Code + - Description + * - Commit OTA Update + - 33554442 + - Commit pending firmware update + * - Check for OTA Updates + - 33554443 + - Check for available firmware updates + +WiFi Management +~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 40 20 40 + + * - Command + - Code + - Description + * - Reconnect WiFi + - 33554446 + - Trigger WiFi reconnection + * - Reset WiFi + - 33554447 + - Reset WiFi settings Control Command Details ======================= @@ -217,11 +383,13 @@ DHW Mode * 1 = Heat Pump Only * 2 = Electric Only -* 3 = Energy Saver +* 3 = Energy Saver (Hybrid mode) * 4 = High Demand * 5 = Vacation (requires second param: days) -**Vacation Example:** +**Vacation Mode Example:** + +When mode is 5 (VACATION), a second parameter specifies number of days: .. code-block:: json @@ -232,13 +400,16 @@ DHW Mode "paramStr": "" } +.. note:: + Vacation mode is the only DHW mode that requires two parameters. + DHW Temperature --------------- .. code-block:: json { - "command": 33554438, + "command": 33554464, "mode": "dhw-temperature", "param": [120], "paramStr": "" @@ -248,6 +419,7 @@ DHW Temperature Temperature values are encoded in **half-degrees Celsius**. Use formula: ``fahrenheit = (param / 2.0) * 9/5 + 32`` For 140°F, send ``param=120`` (which is 60°C × 2). + Valid range: 95-150°F (70-150 raw value). Anti-Legionella --------------- @@ -301,6 +473,260 @@ Enable or disable Time-of-Use optimization without changing the configured sched "paramStr": "" } +Reservation Water Program +-------------------------- + +Enable/configure water program reservation mode. + +**Configure Reservation Mode (command 33554441):** + +.. code-block:: json + + { + "command": 33554441, + "mode": "reservation-mode", + "param": [], + "paramStr": "" + } + +.. note:: + This command enables or configures the water program reservation system. + +Vacation Mode +------------- + +Set vacation/away mode for extended periods. + +**Set Vacation Days (command 33554466):** + +.. code-block:: json + + { + "command": 33554466, + "mode": "goout-day", + "param": [7] + } + +.. note:: + Vacation days parameter: Number of days (e.g., 7). Device will operate in + energy-saving mode to minimize consumption during absence. + +Intelligent/Adaptive Mode +-------------------------- + +Control intelligent heating that learns usage patterns. + +**Enable Intelligent Mode (command 33554468):** + +.. code-block:: json + + { + "command": 33554468, + "mode": "intelligent-on", + "param": [], + "paramStr": "" + } + +**Disable Intelligent Mode (command 33554467):** + +.. code-block:: json + + { + "command": 33554467, + "mode": "intelligent-off", + "param": [], + "paramStr": "" + } + +Demand Response +--------------- + +Control utility demand response participation. + +**Enable Demand Response (command 33554470):** + +.. code-block:: json + + { + "command": 33554470, + "mode": "dr-on", + "param": [], + "paramStr": "" + } + +**Disable Demand Response (command 33554469):** + +.. code-block:: json + + { + "command": 33554469, + "mode": "dr-off", + "param": [], + "paramStr": "" + } + +.. note:: + Demand response allows utilities to manage grid load by signaling water heaters + to reduce consumption (shed) or pre-heat (load up) before peak periods. + +Recirculation Control +--------------------- + +Control recirculation pump operation. + +**Hot Button (command 33554444):** + +.. code-block:: json + + { + "command": 33554444, + "mode": "recirc-hotbtn", + "param": [1], + "paramStr": "" + } + +.. note:: + The param array contains a parameter (typically 1 to activate). + +**Set Recirculation Mode (command 33554445):** + +.. code-block:: json + + { + "command": 33554445, + "mode": "recirc-mode", + "param": [3], + "paramStr": "" + } + +**Recirculation Mode Values:** + +* 1 = Always On +* 2 = Button Only (manual activation) +* 3 = Schedule (follow configured schedule) +* 4 = Temperature (activate when pipe temp drops) + +**Note:** The param array contains a single integer parameter passed to the function. + +Air Filter Maintenance +---------------------- + +Manage air filter maintenance for heat pump models. + +**Reset Air Filter Timer (command 33554473):** + +.. code-block:: json + + { + "command": 33554473, + "mode": "air-filter-reset", + "param": [], + "paramStr": "" + } + +**Set Air Filter Life (command 33554474):** + +.. code-block:: json + + { + "command": 33554474, + "mode": "air-filter-life", + "param": [180], + "paramStr": "" + } + +.. note:: + Air filter life parameter: days between cleanings/replacements (typically 90-180 days) + +Freeze Protection +----------------- + +Configure freeze protection settings. + +**Set Freeze Protection Temperature (command 33554451):** + +.. code-block:: json + + { + "command": 33554451 + } + +.. note:: + This command is defined in the enum but payload structure not found in + decompiled code. May require additional parameters or use default payload. + +Smart Diagnostics +----------------- + +Run smart diagnostic routine. + +**Trigger Smart Diagnostic (command 33554455):** + +.. code-block:: json + + { + "command": 33554455 + } + +.. note:: + This command is defined in the enum but payload structure not found in + decompiled code. May require additional parameters or use default payload. + +WiFi Management +--------------- + +Control WiFi connectivity. + +**Reconnect WiFi (command 33554446):** + +.. code-block:: json + + { + "command": 33554446 + } + +**Reset WiFi Settings (command 33554447):** + +.. code-block:: json + + { + "command": 33554447 + } + +.. warning:: + WiFi reset will clear stored credentials and require re-provisioning. + +.. note:: + These commands are defined in the enum but payload structures not found in + decompiled code. They likely use minimal/default payloads. + +Firmware Updates +---------------- + +Manage over-the-air firmware updates. + +**Commit Update (command 33554442):** + +This command uses a special RequestControlOta structure: + +.. code-block:: json + + { + "command": 33554442, + "deviceType": 52, + "macAddress": "...", + "additionalValue": "...", + "commitOta": { + "swCode": 1, + "swVersion": 184614912 + } + } + +.. note:: + - swCode: Software component code (1=Controller, 2=Panel, 4=WiFi module) + - swVersion: Version number to commit + - This command does not use the standard mode/param/paramStr structure + Energy Usage Query ------------------ diff --git a/docs/python_api/models.rst b/docs/python_api/models.rst index dbf3f29..9432cd0 100644 --- a/docs/python_api/models.rst +++ b/docs/python_api/models.rst @@ -19,131 +19,32 @@ All models are **immutable dataclasses** with: Enumerations ============ -DhwOperationSetting -------------------- - -DHW (Domestic Hot Water) operation modes - the user's configured heating -preference. - -.. py:class:: DhwOperationSetting(Enum) - - **Values:** - - * ``HEAT_PUMP = 1`` - Heat Pump Only - - Most efficient mode - - Uses only heat pump (no electric heaters) - - Slowest recovery time - - Lowest operating cost - - Best for normal daily use - - * ``ELECTRIC = 2`` - Electric Only - - Fast recovery mode - - Uses only electric resistance heaters - - Fastest recovery time - - Highest operating cost - - Use for high-demand situations - - * ``ENERGY_SAVER = 3`` - Energy Saver (Hybrid) - - **Recommended for most users** - - Balanced efficiency and performance - - Uses heat pump primarily, electric when needed - - Good recovery time - - Moderate operating cost - - * ``HIGH_DEMAND = 4`` - High Demand - - Maximum heating capacity - - Uses both heat pump and electric heaters - - Fast recovery with continuous demand - - Higher operating cost - - Best for large families or frequent use - - * ``VACATION = 5`` - Vacation Mode - - Low-power standby mode - - Maintains minimum temperature - - Prevents freezing - - Lowest energy consumption - - Requires vacation_days parameter - - **Example:** - - .. code-block:: python - - from nwp500 import DhwOperationSetting, NavienMqttClient - - # Set to Energy Saver (recommended) - await mqtt.set_dhw_mode(device, DhwOperationSetting.ENERGY_SAVER.value) - - # Set to Heat Pump Only (most efficient) - await mqtt.set_dhw_mode(device, DhwOperationSetting.HEAT_PUMP.value) - - # Set vacation mode for 7 days - await mqtt.set_dhw_mode( - device, - DhwOperationSetting.VACATION.value, - vacation_days=7 - ) - - # Check current mode from status - def on_status(status): - if status.dhw_operation_setting == DhwOperationSetting.ENERGY_SAVER: - print("Running in Energy Saver mode") - -CurrentOperationMode --------------------- - -Current real-time operational state - what the device is doing **right now**. - -.. py:class:: CurrentOperationMode(Enum) - - Unlike ``DhwOperationSetting`` (user preference), this reflects the actual - real-time operation and changes dynamically. - - **Values:** - - * ``IDLE = 0`` - Device is idle, not heating - * ``HEAT_PUMP = 1`` - Heat pump actively running - * ``ELECTRIC_HEATER = 2`` - Electric heater actively running - * ``HEAT_PUMP_AND_HEATER = 3`` - Both heat pump and electric running - - **Example:** - - .. code-block:: python - - from nwp500 import CurrentOperationMode - - def on_status(status): - mode = status.operation_mode - - if mode == CurrentOperationMode.IDLE: - print("Device idle") - elif mode == CurrentOperationMode.HEAT_PUMP: - print(f"Heat pump running at {status.current_inst_power}W") - elif mode == CurrentOperationMode.ELECTRIC_HEATER: - print(f"Electric heater at {status.current_inst_power}W") - elif mode == CurrentOperationMode.HEAT_PUMP_AND_HEATER: - print(f"Both running at {status.current_inst_power}W") - -TemperatureUnit ---------------- - -Temperature scale enumeration. - -.. py:class:: TemperatureUnit(Enum) - - **Values:** - - * ``CELSIUS = 1`` - Celsius (°C) - * ``FAHRENHEIT = 2`` - Fahrenheit (°F) - - **Example:** - - .. code-block:: python - - def on_status(status): - if status.temperature_type == TemperatureUnit.FAHRENHEIT: - print(f"Temperature: {status.dhw_temperature}°F") - else: - print(f"Temperature: {status.dhw_temperature}°C") +See :doc:`../enumerations` for the complete enumeration reference including: + +* **DhwOperationSetting** - User-configured DHW heating modes (Heat Pump/Hybrid/Electric) +* **CurrentOperationMode** - Real-time operational state (Standby/Heat Pump/Hybrid modes) +* **HeatSource** - Currently active heat source +* **TemperatureType** - Temperature unit (Celsius/Fahrenheit) +* **CommandCode** - All control command IDs +* **TouRateType** - Time-of-Use rate periods +* And many more protocol enumerations + +**Quick Reference:** + +.. code-block:: python + + from nwp500 import DhwOperationSetting, CurrentOperationMode, HeatSource, TemperatureType + + # Set operation mode (user preference) + await mqtt.set_dhw_mode(device, DhwOperationSetting.ENERGY_SAVER.value) + + # Check current heat source + if status.current_heat_use == HeatSource.HEATPUMP: + print("Heat pump is active") + + # Check temperature unit + if status.temperature_type == TemperatureType.FAHRENHEIT: + print(f"Temperature: {status.dhw_temperature}°F") Device Models ============= @@ -304,9 +205,10 @@ Complete real-time device status with 100+ fields. **Operation Mode Fields:** - * ``operation_mode`` (CurrentOperationMode) - Current operational state - * ``dhw_operation_setting`` (DhwOperationSetting) - User's mode preference - * ``temperature_type`` (TemperatureUnit) - Temperature unit + * ``operation_mode`` (CurrentOperationMode) - Current operational state (read-only) + * ``dhw_operation_setting`` (DhwOperationSetting) - User's configured mode preference + * ``current_heat_use`` (HeatSource) - Currently active heat source + * ``temperature_type`` (TemperatureType) - Temperature unit **Boolean Status Fields:** @@ -422,8 +324,8 @@ Device capabilities, features, and firmware information. * ``model_type_code`` (int) - Model type * ``control_type_code`` (int) - Control type * ``volume_code`` (int) - Tank volume code - * ``temp_formula_type`` (int) - Temperature formula type - * ``temperature_type`` (TemperatureUnit) - Temperature unit + * ``temp_formula_type`` (TempFormulaType) - Temperature formula type + * ``temperature_type`` (TemperatureType) - Temperature unit **Temperature Limits:** @@ -667,7 +569,7 @@ Best Practices .. code-block:: python - # [OK] Type-safe + # ✓ Type-safe from nwp500 import DhwOperationSetting await mqtt.set_dhw_mode(device, DhwOperationSetting.ENERGY_SAVER.value) @@ -690,12 +592,12 @@ Best Practices def on_status(status): # User's mode preference user_mode = status.dhw_operation_setting - - # Current real-time state - current_state = status.operation_mode + + # Currently active heat source + active_source = status.current_heat_use # These can differ! - # User sets ENERGY_SAVER, device might be in HEAT_PUMP state + # User sets ENERGY_SAVER (hybrid mode), but device might only be using heat pump at the moment Related Documentation ===================== diff --git a/examples/anti_legionella_example.py b/examples/anti_legionella_example.py index 9bc5c37..cd70a9c 100644 --- a/examples/anti_legionella_example.py +++ b/examples/anti_legionella_example.py @@ -17,15 +17,15 @@ from typing import Any from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient -from nwp500.constants import CommandCode +from nwp500.enums import CommandCode, OnOffFlag def display_anti_legionella_status(status: dict[str, Any], label: str = "") -> None: """Display Anti-Legionella status in a formatted way.""" period = status.get("antiLegionellaPeriod") use_value = status.get("antiLegionellaUse") - enabled = use_value == 2 - busy = status.get("antiLegionellaOperationBusy") == 2 + enabled = use_value == OnOffFlag.ON + busy = status.get("antiLegionellaOperationBusy") == OnOffFlag.ON if period is not None and use_value is not None: prefix = f"{label}: " if label else "" @@ -133,7 +133,7 @@ def on_status(topic: str, message: dict[str, Any]) -> None: print("STEP 2: Enabling Anti-Legionella cycle every 7 days...") print("=" * 70) status_received.clear() - expected_command = CommandCode.ANTI_LEGIONELLA_ENABLE + expected_command = CommandCode.ANTI_LEGIONELLA_ON await mqtt_client.enable_anti_legionella(device, period_days=7) try: @@ -151,7 +151,7 @@ def on_status(topic: str, message: dict[str, Any]) -> None: print("WARNING: This reduces protection against Legionella bacteria!") print("=" * 70) status_received.clear() - expected_command = CommandCode.ANTI_LEGIONELLA_DISABLE + expected_command = CommandCode.ANTI_LEGIONELLA_OFF await mqtt_client.disable_anti_legionella(device) try: @@ -168,7 +168,7 @@ def on_status(topic: str, message: dict[str, Any]) -> None: print("STEP 4: Re-enabling Anti-Legionella with 14-day cycle...") print("=" * 70) status_received.clear() - expected_command = CommandCode.ANTI_LEGIONELLA_ENABLE + expected_command = CommandCode.ANTI_LEGIONELLA_ON await mqtt_client.enable_anti_legionella(device, period_days=14) try: diff --git a/examples/combined_callbacks.py b/examples/combined_callbacks.py index 5d42b79..31f5033 100644 --- a/examples/combined_callbacks.py +++ b/examples/combined_callbacks.py @@ -30,6 +30,7 @@ from nwp500.api_client import NavienAPIClient from nwp500.auth import NavienAuthClient +from nwp500.enums import OnOffFlag from nwp500.models import DeviceFeature, DeviceStatus from nwp500.mqtt_client import NavienMqttClient @@ -100,9 +101,11 @@ def on_feature(feature: DeviceFeature): f" Temp Range: {feature.dhw_temperature_min}-{feature.dhw_temperature_max}°F" ) print( - f" Heat Pump: {'Yes' if feature.heatpump_use == 2 else 'No'}" + f" Heat Pump: {'Yes' if feature.heatpump_use == OnOffFlag.ON else 'No'}" + ) + print( + f" Electric: {'Yes' if feature.electric_use == OnOffFlag.ON else 'No'}" ) - print(f" Electric: {'Yes' if feature.electric_use == 2 else 'No'}") # Subscribe to broader topics to catch all messages print("Subscribing to status and feature callbacks...") diff --git a/examples/device_feature_callback.py b/examples/device_feature_callback.py index 44e4ae9..3896e42 100644 --- a/examples/device_feature_callback.py +++ b/examples/device_feature_callback.py @@ -32,6 +32,7 @@ from nwp500.api_client import NavienAPIClient from nwp500.auth import NavienAuthClient +from nwp500.enums import OnOffFlag from nwp500.exceptions import AuthenticationError from nwp500.models import DeviceFeature from nwp500.mqtt_client import NavienMqttClient @@ -137,8 +138,8 @@ def on_device_feature(feature: DeviceFeature): ) print("\nConfiguration:") - print(f" Temperature Unit: {feature.temperatureType.name}") - print(f" Temp Formula Type: {feature.tempFormulaType}") + print(f" Temperature Unit: {feature.temperature_type.name}") + print(f" Temp Formula Type: {feature.temp_formula_type}") print( f" DHW Temp Range: {feature.dhw_temperature_min}°F - {feature.dhw_temperature_max}°F" ) @@ -148,63 +149,63 @@ def on_device_feature(feature: DeviceFeature): print("\nFeature Support:") print( - f" Power Control: {'Supported' if feature.powerUse == 2 else 'Not Available'}" + f" Power Control: {'Yes' if feature.power_use == OnOffFlag.ON else 'No'}" ) print( - f" DHW Control: {'Supported' if feature.dhw_use == 2 else 'Not Available'}" + f" DHW Control: {'Yes' if feature.dhw_use == OnOffFlag.ON else 'No'}" ) print( - f" DHW Temp Setting: Level {feature.dhw_temperature_settingUse}" + f" DHW Temp Setting: Level {feature.dhw_temperature_setting_use}" ) print( - f" Heat Pump Mode: {'Supported' if feature.heatpump_use == 2 else 'Not Available'}" + f" Heat Pump Mode: {'Yes' if feature.heatpump_use == OnOffFlag.ON else 'No'}" ) print( - f" Electric Mode: {'Supported' if feature.electric_use == 2 else 'Not Available'}" + f" Electric Mode: {'Yes' if feature.electric_use == OnOffFlag.ON else 'No'}" ) print( - f" Energy Saver: {'Supported' if feature.energySaverUse == 2 else 'Not Available'}" + f" Energy Saver: {'Yes' if feature.energy_saver_use == OnOffFlag.ON else 'No'}" ) print( - f" High Demand: {'Supported' if feature.highDemandUse == 2 else 'Not Available'}" + f" High Demand: {'Yes' if feature.high_demand_use == OnOffFlag.ON else 'No'}" ) print( - f" Eco Mode: {'Supported' if feature.eco_use == 2 else 'Not Available'}" + f" Eco Mode: {'Yes' if feature.eco_use == OnOffFlag.ON else 'No'}" ) print("\nAdvanced Features:") print( - f" Holiday Mode: {'Supported' if feature.holidayUse == 2 else 'Not Available'}" + f" Holiday Mode: {'Yes' if feature.holiday_use == OnOffFlag.ON else 'No'}" ) print( - f" Program Schedule: {'Supported' if feature.program_reservation_use == 2 else 'Not Available'}" + f" Program Schedule: {'Yes' if feature.program_reservation_use == OnOffFlag.ON else 'No'}" ) print( - f" Smart Diagnostic: {'Supported' if feature.smart_diagnosticUse == 1 else 'Not Available'}" + f" Smart Diagnostic: {'Yes' if feature.smart_diagnostic_use == OnOffFlag.ON else 'No'}" ) print( - f" WiFi RSSI: {'Supported' if feature.wifi_rssiUse == 2 else 'Not Available'}" + f" WiFi RSSI: {'Yes' if feature.wifi_rssi_use == OnOffFlag.ON else 'No'}" ) print( - f" Energy Usage: {'Supported' if feature.energyUsageUse == 2 else 'Not Available'}" + f" Energy Usage: {'Yes' if feature.energy_usage_use == OnOffFlag.ON else 'No'}" ) print( - f" Freeze Protection: {'Supported' if feature.freeze_protection_use == 2 else 'Not Available'}" + f" Freeze Protection: {'Yes' if feature.freeze_protection_use == OnOffFlag.ON else 'No'}" ) print( - f" Mixing Valve: {'Supported' if feature.mixingValueUse == 1 else 'Not Available'}" + f" Mixing Valve: {'Yes' if feature.mixing_value_use == OnOffFlag.ON else 'No'}" ) print( - f" DR Settings: {'Supported' if feature.drSettingUse == 2 else 'Not Available'}" + f" DR Settings: {'Yes' if feature.dr_setting_use == OnOffFlag.ON else 'No'}" ) print( - f" Anti-Legionella: {'Supported' if feature.antiLegionellaSettingUse == 2 else 'Not Available'}" + f" Anti-Legionella: {'Yes' if feature.anti_legionella_setting_use == OnOffFlag.ON else 'No'}" ) print( - f" HPWH: {'Supported' if feature.hpwhUse == 2 else 'Not Available'}" + f" HPWH: {'Yes' if feature.hpwh_use == OnOffFlag.ON else 'No'}" ) print( - f" DHW Refill: {'Supported' if feature.dhwRefillUse == 2 else 'Not Available'}" + f" DHW Refill: {'Yes' if feature.dhw_refill_use == OnOffFlag.ON else 'No'}" ) print("=" * 60) diff --git a/examples/error_code_demo.py b/examples/error_code_demo.py new file mode 100644 index 0000000..ff0acdb --- /dev/null +++ b/examples/error_code_demo.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Example demonstrating ErrorCode enum usage. + +This example shows how to use the ErrorCode enum to interpret device errors +in a type-safe manner without needing a full device status object. +""" + +from nwp500 import ErrorCode +from nwp500.enums import ERROR_CODE_TEXT + + +def diagnose_error(error_code: ErrorCode, sub_code: int = 0) -> None: + """Provide diagnostic information for a given error code.""" + + if error_code == ErrorCode.NO_ERROR: + print("✓ Device operating normally - no errors detected") + return + + # Type-safe error code comparison + print(f"⚠ Error detected: {error_code.name}") + print(f" Code: E{error_code.value:03d} (Sub-code: {sub_code:02d})") + print(f" Description: {ERROR_CODE_TEXT.get(error_code, 'Unknown error')}") + + # Provide specific guidance based on error type + if error_code in (ErrorCode.E096_UPPER_HEATER, ErrorCode.E097_LOWER_HEATER): + print(" → Check heating element resistance and wiring") + print(" → Verify circuit breaker is 30A rated") + + elif error_code == ErrorCode.E799_WATER_LEAK: + print(" → CRITICAL: Check all plumbing connections for leaks!") + print(" → Inspect tank for water damage") + print(" → If tank is leaking, replace entire tank assembly") + + elif error_code == ErrorCode.E326_DRY_FIRE: + print(" → Refill tank until all air is expelled from outlet") + + elif error_code in ( + ErrorCode.E407_DHW_TEMP_SENSOR, + ErrorCode.E480_TANK_UPPER_TEMP_SENSOR, + ErrorCode.E481_TANK_LOWER_TEMP_SENSOR, + ): + print(" → Check temperature sensor wiring connections") + print(" → Device can operate with reduced capacity using opposite element") + if sub_code == 1: + print(" → Sensor reading below lower limit") + elif sub_code == 2: + print(" → Sensor reading above upper limit") + + elif error_code == ErrorCode.E596_WIFI: + print(" → Check WiFi signal strength") + print(" → Verify network connectivity") + print(" → Try restarting the device") + + elif error_code == ErrorCode.E990_CONDENSATE_OVERFLOW: + print(" → Clear condensate drain tubing of obstructions") + print(" → Check for foreign objects in condensate system") + + +def main(): + """Demonstrate error code enum usage.""" + + print("=" * 70) + print("ErrorCode Enum Demonstration") + print("=" * 70) + + # Example 1: No error + print("\n1. Normal Operation") + print("-" * 70) + diagnose_error(ErrorCode.NO_ERROR) + + # Example 2: Water leak error (critical) + print("\n\n2. Critical Error: Water Leak") + print("-" * 70) + diagnose_error(ErrorCode.E799_WATER_LEAK, sub_code=0) + + # Example 3: Temperature sensor error with sub-code + print("\n\n3. Temperature Sensor Fault (Lower Limit)") + print("-" * 70) + diagnose_error(ErrorCode.E480_TANK_UPPER_TEMP_SENSOR, sub_code=1) + + # Example 4: Heating element error + print("\n\n4. Heating Element Error") + print("-" * 70) + diagnose_error(ErrorCode.E096_UPPER_HEATER, sub_code=0) + + # Example 5: List all temperature sensor errors + print("\n\n5. All Temperature Sensor Error Codes") + print("-" * 70) + temp_sensor_errors = [ + ErrorCode.E407_DHW_TEMP_SENSOR, + ErrorCode.E480_TANK_UPPER_TEMP_SENSOR, + ErrorCode.E481_TANK_LOWER_TEMP_SENSOR, + ErrorCode.E910_DISCHARGE_TEMP_SENSOR, + ErrorCode.E912_SUCTION_TEMP_SENSOR, + ErrorCode.E914_EVAPORATOR_TEMP_SENSOR, + ErrorCode.E920_AMBIENT_TEMP_SENSOR, + ] + + for error_code in temp_sensor_errors: + print(f" • E{error_code.value:03d} - {ERROR_CODE_TEXT[error_code]}") + + # Example 6: Demonstrate enum value comparison + print("\n\n6. Type-Safe Error Code Comparison") + print("-" * 70) + + # Simulate receiving error code as integer from device + raw_error_code = 799 + error = ErrorCode(raw_error_code) + + print(f"Received error code: {raw_error_code}") + print(f"Converted to enum: {error.name}") + print(f"Is water leak?: {error == ErrorCode.E799_WATER_LEAK}") + print(f"Is temperature sensor?: {error in temp_sensor_errors}") + + # Example 7: Error grouping + print("\n\n7. Error Code Grouping") + print("-" * 70) + + heating_errors = [ + ErrorCode.E096_UPPER_HEATER, + ErrorCode.E097_LOWER_HEATER, + ] + + compressor_errors = [ + ErrorCode.E907_COMPRESSOR_POWER, + ErrorCode.E908_COMPRESSOR, + ErrorCode.E911_DISCHARGE_TEMP_HIGH, + ErrorCode.E913_SUCTION_TEMP_LOW, + ] + + critical_errors = [ + ErrorCode.E799_WATER_LEAK, + ErrorCode.E326_DRY_FIRE, + ErrorCode.E901_ECO, + ] + + print("Heating Element Errors:") + for e in heating_errors: + print(f" • {e.name}") + + print("\nCompressor Errors:") + for e in compressor_errors: + print(f" • {e.name}") + + print("\nCritical Errors:") + for e in critical_errors: + print(f" • {e.name}") + + print("\n" + "=" * 70) + print("For complete error code reference, see docs/protocol/error_codes.rst") + print("=" * 70) + + +if __name__ == "__main__": + main() diff --git a/examples/event_emitter_demo.py b/examples/event_emitter_demo.py index b727f50..e936568 100644 --- a/examples/event_emitter_demo.py +++ b/examples/event_emitter_demo.py @@ -28,8 +28,13 @@ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) -from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient -from nwp500.models import DeviceStatus, CurrentOperationMode +from nwp500 import ( + NavienAPIClient, + NavienAuthClient, + NavienMqttClient, + CurrentOperationMode, +) +from nwp500.models import DeviceStatus # Example 1: Multiple listeners for the same event @@ -63,8 +68,10 @@ def optimize_on_mode_change( """Optimization handler.""" if new_mode == CurrentOperationMode.HEAT_PUMP_MODE: print("♻️ [Optimizer] Heat pump mode - maximum efficiency!") + elif new_mode == CurrentOperationMode.HYBRID_EFFICIENCY_MODE: + print("⚡ [Optimizer] Energy Saver mode - balanced performance!") elif new_mode == CurrentOperationMode.HYBRID_BOOST_MODE: - print("⚡ [Optimizer] High demand mode - fast recovery!") + print("⚡ [Optimizer] High Demand mode - fast recovery!") # Example 3: Power state handlers diff --git a/examples/mqtt_diagnostics_example.py b/examples/mqtt_diagnostics_example.py index a6e49d7..83106b1 100755 --- a/examples/mqtt_diagnostics_example.py +++ b/examples/mqtt_diagnostics_example.py @@ -52,19 +52,19 @@ def __init__(self): self.diagnostics = MqttDiagnosticsCollector( max_events_retained=1000, enable_verbose_logging=True ) - self.running = True + self.shutdown_event = asyncio.Event() self.output_dir = Path("mqtt_diagnostics_output") self.output_dir.mkdir(exist_ok=True) - def setup_signal_handlers(self) -> None: - """Setup signal handlers for graceful shutdown.""" + def handle_shutdown(self) -> None: + """Handle shutdown signal safely.""" + _logger.info("Shutting down gracefully...") + # Schedule the shutdown event to be set (thread-safe) + asyncio.create_task(self._set_shutdown()) - def handle_signal(signum, frame): - _logger.info(f"Received signal {signum}, shutting down...") - self.running = False - - signal.signal(signal.SIGINT, handle_signal) - signal.signal(signal.SIGTERM, handle_signal) + async def _set_shutdown(self) -> None: + """Set shutdown event (must be called from async context).""" + self.shutdown_event.set() async def export_diagnostics(self, interval: float = 300.0) -> None: """ @@ -73,11 +73,11 @@ async def export_diagnostics(self, interval: float = 300.0) -> None: Args: interval: Export interval in seconds (default: 5 minutes) """ - while self.running: + while not self.shutdown_event.is_set(): try: await asyncio.sleep(interval) - if not self.running: + if self.shutdown_event.is_set(): break # Export JSON @@ -105,7 +105,7 @@ async def monitor_connection_state(self, interval: float = 10.0) -> None: Args: interval: Update interval in seconds """ - while self.running: + while not self.shutdown_event.is_set(): try: await asyncio.sleep(interval) @@ -179,7 +179,10 @@ async def run_example( password: Navien account password duration_seconds: How long to run (default: 1 hour) """ - self.setup_signal_handlers() + # Setup signal handler for graceful shutdown + loop = asyncio.get_event_loop() + for sig in (signal.SIGINT, signal.SIGTERM): + loop.add_signal_handler(sig, self.handle_shutdown) _logger.info("=" * 70) _logger.info("MQTT DIAGNOSTICS COLLECTION EXAMPLE") @@ -244,14 +247,21 @@ async def run_example( "Press Ctrl+C to stop early." ) - await asyncio.sleep(duration_seconds) + # Sleep in small intervals to check shutdown flag + elapsed = 0.0 + interval = 1.0 + while ( + not self.shutdown_event.is_set() and elapsed < duration_seconds + ): + await asyncio.sleep(min(interval, duration_seconds - elapsed)) + elapsed += interval except asyncio.CancelledError: _logger.info("Example cancelled") finally: # Cleanup - self.running = False + self.shutdown_event.set() export_task.cancel() monitor_task.cancel() diff --git a/src/nwp500/__init__.py b/src/nwp500/__init__.py index 7faf309..0e95b00 100644 --- a/src/nwp500/__init__.py +++ b/src/nwp500/__init__.py @@ -40,6 +40,23 @@ encode_season_bitfield, encode_week_bitfield, ) +from nwp500.enums import ( + CommandCode, + CurrentOperationMode, + DhwOperationSetting, + DREvent, + ErrorCode, + FilterChange, + HeatSource, + OnOffFlag, + Operation, + RecirculationMode, + TemperatureType, + TempFormulaType, + TouRateType, + TouWeekType, + UnitType, +) from nwp500.events import ( EventEmitter, EventListener, @@ -66,12 +83,10 @@ ValidationError, ) from nwp500.models import ( - CurrentOperationMode, Device, DeviceFeature, DeviceInfo, DeviceStatus, - DhwOperationSetting, EnergyUsageDay, EnergyUsageResponse, EnergyUsageTotal, @@ -80,7 +95,6 @@ MonthlyEnergyData, MqttCommand, MqttRequest, - TemperatureUnit, TOUInfo, TOUSchedule, fahrenheit_to_half_celsius, @@ -108,15 +122,28 @@ "FirmwareInfo", "TOUSchedule", "TOUInfo", - "DhwOperationSetting", - "CurrentOperationMode", - "TemperatureUnit", "MqttRequest", "MqttCommand", "EnergyUsageTotal", "EnergyUsageDay", "MonthlyEnergyData", "EnergyUsageResponse", + # Enumerations + "CommandCode", + "CurrentOperationMode", + "DhwOperationSetting", + "DREvent", + "ErrorCode", + "FilterChange", + "HeatSource", + "OnOffFlag", + "Operation", + "RecirculationMode", + "TemperatureType", + "TempFormulaType", + "TouRateType", + "TouWeekType", + "UnitType", # Conversion utilities "fahrenheit_to_half_celsius", # Authentication diff --git a/src/nwp500/cli/commands.py b/src/nwp500/cli/commands.py index 3b23b98..4a4ea3d 100644 --- a/src/nwp500/cli/commands.py +++ b/src/nwp500/cli/commands.py @@ -51,13 +51,9 @@ async def handle_status_request(mqtt: NavienMqttClient, device: Device) -> None: def on_status(status: DeviceStatus) -> None: if not future.done(): - print( - json.dumps( - status.model_dump(), - indent=2, - default=_json_default_serializer, - ) - ) + from .output_formatters import format_json_output + + print(format_json_output(status.model_dump())) future.set_result(None) await mqtt.subscribe_device_status(device, on_status) @@ -125,13 +121,9 @@ async def handle_device_info_request( def on_device_info(info: Any) -> None: if not future.done(): - print( - json.dumps( - info.model_dump(), - indent=2, - default=_json_default_serializer, - ) - ) + from .output_formatters import format_json_output + + print(format_json_output(info.model_dump())) future.set_result(None) await mqtt.subscribe_device_feature(device, on_device_info) @@ -226,13 +218,9 @@ def on_status_response(status: DeviceStatus) -> None: if responses: status = responses[0] - print( - json.dumps( - status.model_dump(), - indent=2, - default=_json_default_serializer, - ) - ) + from .output_formatters import format_json_output + + print(format_json_output(status.model_dump())) _logger.info( f"Mode change successful. New mode: " f"{status.operation_mode.name}" @@ -301,13 +289,9 @@ def on_status_response(status: DeviceStatus) -> None: if responses: status = responses[0] - print( - json.dumps( - status.model_dump(), - indent=2, - default=_json_default_serializer, - ) - ) + from .output_formatters import format_json_output + + print(format_json_output(status.model_dump())) _logger.info( f"Temperature change successful. New target: " f"{status.dhw_target_temperature_setting}°F" @@ -601,13 +585,9 @@ def on_status_response(status: DeviceStatus) -> None: await asyncio.wait_for(future, timeout=10) if responses: status = responses[0] - print( - json.dumps( - status.model_dump(), - indent=2, - default=_json_default_serializer, - ) - ) + from .output_formatters import format_json_output + + print(format_json_output(status.model_dump())) _logger.info(f"TOU {action} successful.") else: _logger.warning("TOU command sent but no response received") diff --git a/src/nwp500/cli/output_formatters.py b/src/nwp500/cli/output_formatters.py index 4939e07..d002ca3 100644 --- a/src/nwp500/cli/output_formatters.py +++ b/src/nwp500/cli/output_formatters.py @@ -16,6 +16,10 @@ def _json_default_serializer(obj: Any) -> Any: """Serialize objects not serializable by default json code. + Note: Enums are handled by model.model_dump() which converts them to names. + This function handles any remaining non-JSON-serializable types that might + appear in raw MQTT messages. + Args: obj: Object to serialize @@ -25,10 +29,10 @@ def _json_default_serializer(obj: Any) -> Any: Raises: TypeError: If object cannot be serialized """ - if isinstance(obj, Enum): - return obj.name if isinstance(obj, datetime): return obj.isoformat() + if isinstance(obj, Enum): + return obj.name # Fallback for any enums not in model output raise TypeError(f"Type {type(obj)} not serializable") @@ -41,17 +45,12 @@ def write_status_to_csv(file_path: str, status: DeviceStatus) -> None: status: DeviceStatus object to write """ try: - # Convert the entire dataclass to a dictionary to capture all fields + # Convert status to dict (enums are already converted to names) status_dict = status.model_dump() # Add a timestamp to the beginning of the data status_dict["timestamp"] = datetime.now().isoformat() - # Convert Enum values to their names - for key, value in status_dict.items(): - if isinstance(value, Enum): - status_dict[key] = value.name - # Check if file exists to determine if we need to write the header file_exists = Path(file_path).exists() diff --git a/src/nwp500/constants.py b/src/nwp500/constants.py index f2497bd..411867a 100644 --- a/src/nwp500/constants.py +++ b/src/nwp500/constants.py @@ -1,59 +1,8 @@ -"""Constants and command codes for Navien device communication.""" - -from enum import IntEnum - - -class CommandCode(IntEnum): - """ - MQTT Command codes for Navien device control. - - These command codes are used for MQTT communication with Navien devices. - Commands are organized into two categories: - - - Query commands (16777xxx): Request device information - - Control commands (33554xxx): Change device settings - - All commands and their expected payloads are documented in - `docs/MQTT_MESSAGES.rst` under the "Control Messages" section. - - Examples: - >>> CommandCode.STATUS_REQUEST - - - >>> CommandCode.POWER_ON.value - 33554434 - - >>> CommandCode.POWER_ON.name - 'POWER_ON' - - >>> list(CommandCode)[:3] - [, ...] - """ - - # Query Commands (Information Retrieval) - DEVICE_INFO_REQUEST = 16777217 # Request device feature information - STATUS_REQUEST = 16777219 # Request current device status - RESERVATION_READ = 16777222 # Read current reservation schedule - ENERGY_USAGE_QUERY = 16777225 # Query energy usage history - RESERVATION_MANAGEMENT = 16777226 # Update/manage reservation schedules - - # Control Commands - Power - POWER_OFF = 33554433 # Turn device off - POWER_ON = 33554434 # Turn device on - - # Control Commands - DHW (Domestic Hot Water) - DHW_MODE = 33554437 # Change DHW operation mode - TOU_SETTINGS = 33554439 # Configure TOU schedule - DHW_TEMPERATURE = 33554464 # Set DHW temperature - - # Control Commands - Anti-Legionella - ANTI_LEGIONELLA_DISABLE = 33554471 # Disable anti-legionella cycle - ANTI_LEGIONELLA_ENABLE = 33554472 # Enable anti-legionella cycle - - # Control Commands - Time of Use (TOU) - TOU_DISABLE = 33554475 # Disable TOU optimization - TOU_ENABLE = 33554476 # Enable TOU optimization +"""Constants for Navien device communication. +Note: CommandCode has been moved to enums.py module. +Import from nwp500.enums instead of nwp500.constants. +""" # Note for maintainers: # Command codes and expected payload fields are defined in diff --git a/src/nwp500/enums.py b/src/nwp500/enums.py new file mode 100644 index 0000000..f66d42e --- /dev/null +++ b/src/nwp500/enums.py @@ -0,0 +1,501 @@ +"""Enumerations for Navien device protocol. + +This module contains enumerations for the Navien device protocol. These +enums define valid values for device control commands, status fields, and +capabilities. +""" + +from enum import IntEnum + +# ============================================================================ +# Status Value Enumerations +# ============================================================================ + + +class OnOffFlag(IntEnum): + """Generic on/off flag used throughout status fields. + + Used for: Power status, TOU status, recirculation status, vacation mode, + anti-legionella, and many other boolean device settings. + """ + + OFF = 1 + ON = 2 + + +class Operation(IntEnum): + """Device operation state.""" + + UNKNOWN = 0 + OPERATION = 1 + STOP = 2 + + +class DhwOperationSetting(IntEnum): + """DHW operation setting modes (user-configured heating preferences). + + This enum represents the user's configured mode preference - what + heating mode the device should use when it needs to heat water. These + values appear in the dhw_operation_setting field and are set via user + commands. + """ + + HEAT_PUMP = 1 # Heat Pump Only - most efficient, slowest recovery + ELECTRIC = 2 # Electric Only - least efficient, fastest recovery + ENERGY_SAVER = 3 # Hybrid: Efficiency - balanced, good default + HIGH_DEMAND = 4 # Hybrid: Boost - maximum heating capacity + VACATION = 5 # Vacation mode - suspends heating to save energy + POWER_OFF = 6 # Device powered off + + +class CurrentOperationMode(IntEnum): + """Current operation mode (real-time operational state). + + This enum represents the device's current actual operational state - what + the device is doing RIGHT NOW. These values appear in the operation_mode + field and change automatically based on heating demand. + """ + + STANDBY = 0 # Device is idle, not actively heating + HEAT_PUMP_MODE = 32 # Heat pump is actively running to heat water + HYBRID_EFFICIENCY_MODE = 64 # Device actively heating in Energy Saver mode + HYBRID_BOOST_MODE = 96 # Device actively heating in High Demand mode + + +class HeatSource(IntEnum): + """Currently active heat source (read-only status). + + This reflects what the device is currently using, not what mode + it's set to. In Hybrid mode, this field shows which source(s) + are active at any given moment. + """ + + UNKNOWN = 0 + HEATPUMP = 1 + HEATELEMENT = 2 + HEATPUMP_HEATELEMENT = 3 + + +class DREvent(IntEnum): + """Demand Response event status. + + Allows utilities to manage grid load by signaling water heaters + to reduce consumption (shed) or pre-heat (load up) before peak periods. + """ + + UNKNOWN = 0 # Not Applied + RUN_NORMAL = 1 # Normal operation during DR event + SHED = 2 # Load shedding - reduce power + LOADUP = 3 # Pre-heat before expected high demand + LOADUP_ADV = 4 # Advanced load-up strategy + CPE = 5 # Customer peak event / Grid emergency + + +class WaterLevel(IntEnum): + """Hot water level indicator (displayed as gauge in app). + + Note: IDs are non-sequential, likely represent bit positions + for multi-level displays. + """ + + LOW = 2 + LOW_MEDIUM = 8 + MEDIUM_HIGH = 16 + HIGH = 4 + + +class FilterChange(IntEnum): + """Air filter status for heat pump models.""" + + NORMAL = 0 + REPLACE_NEED = 1 + UNKNOWN = 2 + + +class RecirculationMode(IntEnum): + """Recirculation pump operation mode.""" + + UNKNOWN = 0 + ALWAYS = 1 # Pump always runs + BUTTON = 2 # Manual activation only + SCHEDULE = 3 # Runs on configured schedule + TEMPERATURE = 4 # Activates when pipe temp drops + + +# ============================================================================ +# Time of Use (TOU) Enumerations +# ============================================================================ + + +class TouWeekType(IntEnum): + """Day grouping for TOU schedules. + + TOU schedules can be configured separately for weekdays and weekends + to account for different electricity rates and usage patterns. + """ + + WEEK_DAY = 0 # Monday through Friday + WEEK_END = 1 # Saturday and Sunday + + +class TouRateType(IntEnum): + """Electricity rate period type. + + Device behavior during each rate period can be configured. + Typically, devices heat aggressively during off-peak, maintain + temperature during mid-peak, and avoid heating during on-peak + unless necessary. + """ + + UNKNOWN = 0 + OFF_PEAK = 1 # Lowest rates - heat aggressively + MID_PEAK = 2 # Medium rates - heat normally + ON_PEAK = 3 # Highest rates - minimize heating + + +# ============================================================================ +# Temperature and Unit Enumerations +# ============================================================================ + + +class TemperatureType(IntEnum): + """Temperature display unit preference.""" + + CELSIUS = 1 + FAHRENHEIT = 2 + + +class TempFormulaType(IntEnum): + """Temperature conversion formula type. + + Different device models use slightly different rounding algorithms + when converting internal Celsius values to Fahrenheit. This ensures + the mobile app matches the device's built-in display. + + Type 0: Asymmetric ceiling/floor rounding based on raw value remainder + Type 1: Standard rounding to nearest integer + """ + + ASYMMETRIC = 0 # Special rounding for remainder == 9 + STANDARD = 1 # Simple round to nearest + + +# ============================================================================ +# Heating System Enumerations +# ============================================================================ + + +class HeatControl(IntEnum): + """Heating control method (for combi-boilers).""" + + UNKNOWN = 0 + SUPPLY = 1 # Control based on supply temperature + RETURN = 2 # Control based on return temperature + OUTSIDE_CONTROL = 3 # Outdoor temperature compensation + + +# ============================================================================ +# Device Type Enumerations +# ============================================================================ + + +class UnitType(IntEnum): + """Navien device/unit model types.""" + + NO_DEVICE = 0 + NPE = 1 # Tankless water heater + NCB = 2 # Condensing boiler + NHB = 3 # High-efficiency boiler + CAS_NPE = 4 # Cascading NPE system + CAS_NHB = 5 # Cascading NHB system + NFB = 6 # Fire-tube boiler + CAS_NFB = 7 # Cascading NFB system + NFC = 8 # Condensing boiler + NPN = 9 # Condensing water heater (NPN/NHW700) + CAS_NPN = 10 # Cascading NPN system + NPE2 = 11 # Tankless water heater (2nd gen) + CAS_NPE2 = 12 # Cascading NPE2 system + NCB_H = 13 # High-efficiency NCB + NVW = 14 # Volume water heater + CAS_NVW = 15 # Cascading NVW system + NHB_H = 16 # High-efficiency NHB + CAS_NHB_H = 17 # Cascading NHB-H system + NFB_700 = 20 # Fire-tube boiler 700 series + CAS_NFB_700 = 21 # Cascading NFB700 system + TWC = 257 # Tankless water heater (commercial) + NPF = 513 # Heat pump water heater + + +class DeviceType(IntEnum): + """Communication device type.""" + + NAVILINK = 1 # Standard NaviLink WiFi module + NAVILINK_LIGHT = 2 # Light version NaviLink module + NPF700_MAIN = 50 # NPF700 main controller + NPF700_SUB = 51 # NPF700 sub-controller + NPF700_WIFI = 52 # NPF700 WiFi module + + +class CommandCode(IntEnum): + """MQTT Command codes for Navien device control. + + These command codes are used for MQTT communication with Navien devices. + Commands are organized into two categories: + + - Query commands (16777xxx): Request device information + - Control commands (33554xxx): Change device settings + + All commands and their expected payloads are documented in + docs/MQTT_MESSAGES.rst under the "Control Messages" section. + """ + + # Query Commands (Information Retrieval) + DEVICE_INFO_REQUEST = 16777217 # Request device feature information + STATUS_REQUEST = 16777219 # Request current device status + RESERVATION_READ = 16777222 # Read current reservation schedule + ENERGY_USAGE_QUERY = 16777225 # Query energy usage history + RESERVATION_MANAGEMENT = 16777226 # Update/manage reservation schedules + + # Control Commands - Power + POWER_OFF = 33554433 # Turn device off + POWER_ON = 33554434 # Turn device on + + # Control Commands - DHW (Domestic Hot Water) Operation + DHW_MODE = 33554437 # Change DHW operation mode + DHW_TEMPERATURE = 33554464 # Set DHW temperature + + # Control Commands - Scheduling + RESERVATION_WEEKLY = 33554438 # Configure weekly temperature schedule + TOU_RESERVATION = 33554439 # Configure Time-of-Use schedule + RECIR_RESERVATION = 33554440 # Configure recirculation schedule + RESERVATION_WATER_PROGRAM = 33554441 # Configure hot water program + + # Control Commands - Firmware/OTA + OTA_COMMIT = 33554442 # Commit OTA firmware update + OTA_CHECK = 33554443 # Check for OTA firmware updates + + # Control Commands - Recirculation + RECIR_HOT_BTN = 33554444 # Trigger recirculation hot button + RECIR_MODE = 33554445 # Set recirculation mode + + # Control Commands - WiFi + WIFI_RECONNECT = 33554446 # Reconnect WiFi + WIFI_RESET = 33554447 # Reset WiFi settings + + # Control Commands - Special Functions + FREZ_TEMP = 33554451 # Set freeze protection temperature + SMART_DIAGNOSTIC = 33554455 # Trigger smart diagnostics + + # Control Commands - Vacation/Away + GOOUT_DAY = 33554466 # Set vacation mode duration (days) + + # Control Commands - Intelligent/Adaptive Mode + RESERVATION_INTELLIGENT_OFF = 33554467 # Disable intelligent mode + RESERVATION_INTELLIGENT_ON = 33554468 # Enable intelligent mode + + # Control Commands - Demand Response + DR_OFF = 33554469 # Disable demand response + DR_ON = 33554470 # Enable demand response + + # Control Commands - Anti-Legionella + ANTI_LEGIONELLA_OFF = 33554471 # Disable anti-legionella cycle + ANTI_LEGIONELLA_ON = 33554472 # Enable anti-legionella cycle + + # Control Commands - Air Filter (Heat Pump Models) + AIR_FILTER_RESET = 33554473 # Reset air filter timer + AIR_FILTER_LIFE = 33554474 # Set air filter life span + + # Control Commands - Time of Use (TOU) + TOU_OFF = 33554475 # Disable TOU optimization + TOU_ON = 33554476 # Enable TOU optimization + + +class FirmwareType(IntEnum): + """Firmware component types.""" + + UNKNOWN = 0 + CONTROLLER = 1 # Main controller firmware + PANEL = 2 # Control panel firmware + ROOM_CON = 3 # Room controller firmware + COMMUNICATION_MODULE = 4 # WiFi/comm module firmware + VALVE_CONTROL = 5 # Valve controller firmware + SUB_ROOM_CON = 6 # Sub room controller firmware + + +# ============================================================================ +# Display Text Helpers +# ============================================================================ + + +DHW_OPERATION_SETTING_TEXT = { + DhwOperationSetting.HEAT_PUMP: "Heat Pump", + DhwOperationSetting.ELECTRIC: "Electric", + DhwOperationSetting.ENERGY_SAVER: "Energy Saver", + DhwOperationSetting.HIGH_DEMAND: "High Demand", + DhwOperationSetting.VACATION: "Vacation", + DhwOperationSetting.POWER_OFF: "Power Off", +} + +CURRENT_OPERATION_MODE_TEXT = { + CurrentOperationMode.STANDBY: "Standby", + CurrentOperationMode.HEAT_PUMP_MODE: "Heat Pump Active", + CurrentOperationMode.HYBRID_EFFICIENCY_MODE: "Hybrid Efficiency", + CurrentOperationMode.HYBRID_BOOST_MODE: "Hybrid Boost", +} + +HEAT_SOURCE_TEXT = { + HeatSource.UNKNOWN: "Unknown", + HeatSource.HEATPUMP: "Heat Pump", + HeatSource.HEATELEMENT: "Heat Element", + HeatSource.HEATPUMP_HEATELEMENT: "Heat Pump & Heat Element", +} + +DR_EVENT_TEXT = { + DREvent.UNKNOWN: "Not Applied", + DREvent.RUN_NORMAL: "Run Normal", + DREvent.SHED: "Shed", + DREvent.LOADUP: "Load Up", + DREvent.LOADUP_ADV: "Advance Load Up", + DREvent.CPE: "Customer Peak Event", +} + +RECIRC_MODE_TEXT = { + RecirculationMode.UNKNOWN: "Unknown", + RecirculationMode.ALWAYS: "Always", + RecirculationMode.BUTTON: "Button", + RecirculationMode.SCHEDULE: "Schedule", + RecirculationMode.TEMPERATURE: "Temperature", +} + +TOU_RATE_TEXT = { + TouRateType.UNKNOWN: "Unknown", + TouRateType.OFF_PEAK: "Off Peak", + TouRateType.MID_PEAK: "Mid Peak", + TouRateType.ON_PEAK: "On Peak", +} + +FILTER_STATUS_TEXT = { + FilterChange.NORMAL: "Normal Operation", + FilterChange.REPLACE_NEED: "Replacement Needed", + FilterChange.UNKNOWN: "Unknown", +} + + +# ============================================================================ +# Error Code Enumerations +# ============================================================================ + + +class ErrorCode(IntEnum): + """Device error codes. + + Error codes indicate specific faults detected by the device's + diagnostic system. Most errors are Level 1, allowing continued + operation with reduced functionality. + See docs/protocol/error_codes.rst for complete troubleshooting guide. + """ + + NO_ERROR = 0 + + # Heating element errors + E096_UPPER_HEATER = 96 + E097_LOWER_HEATER = 97 + + # Water system errors + E326_DRY_FIRE = 326 + + # Temperature sensor errors + E407_DHW_TEMP_SENSOR = 407 + E480_TANK_UPPER_TEMP_SENSOR = 480 + E481_TANK_LOWER_TEMP_SENSOR = 481 + E910_DISCHARGE_TEMP_SENSOR = 910 + E912_SUCTION_TEMP_SENSOR = 912 + E914_EVAPORATOR_TEMP_SENSOR = 914 + E920_AMBIENT_TEMP_SENSOR = 920 + + # Mixing valve errors + E445_MIXING_VALVE = 445 + + # Relay errors + E515_RELAY_FAULT = 515 + + # System errors + E517_DIP_SWITCH = 517 + E593_PANEL_KEY = 593 + E594_EEPROM = 594 + E595_POWER_METER = 595 + E596_WIFI = 596 + E598_RTC = 598 + + # Feedback/ADC errors + E615_FEEDBACK = 615 + + # Communication errors + E781_CTA2045 = 781 + + # Valve/leak errors + E798_SHUTOFF_VALVE = 798 + E799_WATER_LEAK = 799 + + # Safety errors + E901_ECO = 901 + + # Compressor errors + E907_COMPRESSOR_POWER = 907 + E908_COMPRESSOR = 908 + E909_EVAPORATOR_FAN = 909 + E911_DISCHARGE_TEMP_HIGH = 911 + E913_SUCTION_TEMP_LOW = 913 + E915_TEMP_DIFFERENCE = 915 + E916_EVAPORATOR_TEMP = 916 + + # Refrigerant system errors + E940_REFRIGERANT_BLOCKAGE = 940 + + # Condensate errors + E990_CONDENSATE_OVERFLOW = 990 + + +ERROR_CODE_TEXT = { + ErrorCode.NO_ERROR: "No Error", + ErrorCode.E096_UPPER_HEATER: "Abnormal Upper Electric Heater", + ErrorCode.E097_LOWER_HEATER: "Abnormal Lower Electric Heater", + ErrorCode.E326_DRY_FIRE: "Dry Fire", + ErrorCode.E407_DHW_TEMP_SENSOR: "Abnormal DHW Temperature Sensor", + ErrorCode.E445_MIXING_VALVE: "Abnormal Mixing Valve", + ErrorCode.E480_TANK_UPPER_TEMP_SENSOR: ( + "Abnormal Tank Upper Temperature Sensor" + ), + ErrorCode.E481_TANK_LOWER_TEMP_SENSOR: ( + "Abnormal Tank Lower Temperature Sensor" + ), + ErrorCode.E515_RELAY_FAULT: "Relay Fault", + ErrorCode.E517_DIP_SWITCH: "Abnormal DIP Switch", + ErrorCode.E593_PANEL_KEY: "Abnormal Panel Key", + ErrorCode.E594_EEPROM: "Abnormal EEPROM", + ErrorCode.E595_POWER_METER: "Abnormal Power Meter", + ErrorCode.E596_WIFI: "Abnormal WiFi Connection", + ErrorCode.E598_RTC: "Abnormal Real-Time Clock", + ErrorCode.E615_FEEDBACK: "Abnormal Feedback", + ErrorCode.E781_CTA2045: "Abnormal CTA-2045 Communication", + ErrorCode.E798_SHUTOFF_VALVE: "Abnormal Shut-off Valve", + ErrorCode.E799_WATER_LEAK: "Water Leak Detected", + ErrorCode.E901_ECO: "Abnormal ECO Operation", + ErrorCode.E907_COMPRESSOR_POWER: "Abnormal Compressor Power Line", + ErrorCode.E908_COMPRESSOR: "Abnormal Compressor Operation", + ErrorCode.E909_EVAPORATOR_FAN: "Abnormal Evaporator Fan", + ErrorCode.E910_DISCHARGE_TEMP_SENSOR: ( + "Abnormal Discharge Temperature Sensor" + ), + ErrorCode.E911_DISCHARGE_TEMP_HIGH: "Abnormally High Discharge Temperature", + ErrorCode.E912_SUCTION_TEMP_SENSOR: "Abnormal Suction Temperature Sensor", + ErrorCode.E913_SUCTION_TEMP_LOW: "Abnormally Low Suction Temperature", + ErrorCode.E914_EVAPORATOR_TEMP_SENSOR: ( + "Abnormal Evaporator Temperature Sensor" + ), + ErrorCode.E915_TEMP_DIFFERENCE: "Abnormal Temperature Difference", + ErrorCode.E916_EVAPORATOR_TEMP: "Abnormal Evaporator Temperature", + ErrorCode.E920_AMBIENT_TEMP_SENSOR: "Abnormal Ambient Temperature Sensor", + ErrorCode.E940_REFRIGERANT_BLOCKAGE: "Refrigerant Line Blockage", + ErrorCode.E990_CONDENSATE_OVERFLOW: "Condensate Overflow Detected", +} diff --git a/src/nwp500/models.py b/src/nwp500/models.py index 5a5ef7d..d6aacca 100644 --- a/src/nwp500/models.py +++ b/src/nwp500/models.py @@ -7,12 +7,22 @@ """ import logging -from enum import Enum from typing import Annotated, Any, Optional, Union from pydantic import BaseModel, BeforeValidator, ConfigDict, Field from pydantic.alias_generators import to_camel +from .enums import ( + CurrentOperationMode, + DeviceType, + DhwOperationSetting, + ErrorCode, + HeatSource, + TemperatureType, + TempFormulaType, + UnitType, +) + _logger = logging.getLogger(__name__) @@ -26,6 +36,14 @@ def _device_bool_validator(v: Any) -> bool: return bool(v == 2) +def _capability_flag_validator(v: Any) -> bool: + """Convert capability flag (2=True/supported, 1=False/not supported). + + Uses same pattern as OnOffFlag: 1=OFF/not supported, 2=ON/supported. + """ + return bool(v == 2) + + def _div_10_validator(v: Any) -> float: """Divide by 10.""" if isinstance(v, (int, float)): @@ -73,48 +91,101 @@ def _deci_celsius_to_fahrenheit(v: Any) -> float: return float(v) +def _tou_status_validator(v: Any) -> bool: + """Convert TOU status (0=False/disabled, 1=True/enabled).""" + return bool(v == 1) + + +def _tou_override_validator(v: Any) -> bool: + """Convert TOU override status (1=True/override active, 2=False/normal). + + Note: This field uses OnOffFlag pattern (1=OFF, 2=ON) but represents + whether TOU schedule operation is enabled, not whether override is active. + So: 2 (ON) = TOU operating normally = override NOT active = False + 1 (OFF) = TOU not operating = override IS active = True + """ + return bool(v == 1) + + # Reusable Annotated types for conversions DeviceBool = Annotated[bool, BeforeValidator(_device_bool_validator)] +CapabilityFlag = Annotated[bool, BeforeValidator(_capability_flag_validator)] Div10 = Annotated[float, BeforeValidator(_div_10_validator)] HalfCelsiusToF = Annotated[float, BeforeValidator(_half_celsius_to_fahrenheit)] DeciCelsiusToF = Annotated[float, BeforeValidator(_deci_celsius_to_fahrenheit)] +TouStatus = Annotated[bool, BeforeValidator(_tou_status_validator)] +TouOverride = Annotated[bool, BeforeValidator(_tou_override_validator)] class NavienBaseModel(BaseModel): - """Base model for all Navien models.""" + """Base model for all Navien models. + + Note: use_enum_values=False keeps enums as objects during validation. + Serialization to names happens in model_dump() method. + """ model_config = ConfigDict( alias_generator=to_camel, populate_by_name=True, extra="ignore", # Ignore unknown fields by default + use_enum_values=False, # Keep enums as objects during validation ) + def model_dump(self, **kwargs: Any) -> dict[str, Any]: + """Dump model to dict with enums as names by default.""" + # Default to 'name' mode for enums unless explicitly overridden + if "mode" not in kwargs: + kwargs["mode"] = "python" + result = super().model_dump(**kwargs) + # Convert enums to their names + converted: dict[str, Any] = self._convert_enums_to_names(result) + return converted -class DhwOperationSetting(Enum): - """DHW operation setting modes (user-configured heating preferences).""" + @staticmethod + def _convert_enums_to_names( + data: Any, visited: Optional[set[int]] = None + ) -> Any: + """Recursively convert Enum values to their names. - HEAT_PUMP = 1 - ELECTRIC = 2 - ENERGY_SAVER = 3 - HIGH_DEMAND = 4 - VACATION = 5 - POWER_OFF = 6 - - -class CurrentOperationMode(Enum): - """Current operation mode (real-time operational state).""" - - STANDBY = 0 - HEAT_PUMP_MODE = 32 - HYBRID_EFFICIENCY_MODE = 64 - HYBRID_BOOST_MODE = 96 - - -class TemperatureUnit(Enum): - """Temperature unit enumeration.""" + Args: + data: Data to convert + visited: Set of visited object ids to detect cycles - CELSIUS = 1 - FAHRENHEIT = 2 + Returns: + Data with enums converted to their names + """ + from enum import Enum + + if visited is None: + visited = set() + + if isinstance(data, Enum): + return data.name + elif isinstance(data, dict): + # Check for circular reference + data_id = id(data) + if data_id in visited: + return data + visited.add(data_id) + result: dict[Any, Any] = { + key: NavienBaseModel._convert_enums_to_names(value, visited) + for key, value in data.items() + } + visited.discard(data_id) + return result + elif isinstance(data, (list, tuple)): + # Check for circular reference + data_id = id(data) + if data_id in visited: + return data + visited.add(data_id) + converted = [ + NavienBaseModel._convert_enums_to_names(item, visited) + for item in data + ] + visited.discard(data_id) + return type(data)(converted) + return data class DeviceInfo(NavienBaseModel): @@ -123,7 +194,7 @@ class DeviceInfo(NavienBaseModel): home_seq: int = 0 mac_address: str = "" additional_value: str = "" - device_type: int = 52 + device_type: Union[DeviceType, int] = DeviceType.NPF700_WIFI device_name: str = "Unknown" connected: int = 0 install_type: Optional[str] = None @@ -152,7 +223,7 @@ class FirmwareInfo(NavienBaseModel): mac_address: str = "" additional_value: str = "" - device_type: int = 52 + device_type: Union[DeviceType, int] = DeviceType.NPF700_WIFI cur_sw_code: int = 0 cur_version: int = 0 downloaded_version: Optional[int] = None @@ -230,7 +301,10 @@ class DeviceStatus(NavienBaseModel): "(e.g., freeze protection, anti-seize operations)" ) ) - error_code: int = Field(description="Error code if any fault is detected") + error_code: ErrorCode = Field( + default=ErrorCode.NO_ERROR, + description="Error code if any fault is detected", + ) sub_error_code: int = Field( description="Sub error code providing additional error details" ) @@ -279,7 +353,7 @@ class DeviceStatus(NavienBaseModel): program_reservation_type: int = Field( description="Type of program reservation" ) - temp_formula_type: Union[int, str] = Field( + temp_formula_type: TempFormulaType = Field( description="Temperature formula type" ) current_statenum: int = Field(description="Current state number") @@ -332,10 +406,10 @@ class DeviceStatus(NavienBaseModel): ), json_schema_extra={"unit_of_measurement": "gal"}, ) - tou_status: int = Field( + tou_status: TouStatus = Field( description=( - "Time of Use (TOU) status - " - "indicates if TOU scheduled operation is active" + "Time of Use (TOU) scheduling enabled. " + "True = TOU is active/enabled, False = TOU is disabled" ) ) dr_override_status: int = Field( @@ -344,10 +418,11 @@ class DeviceStatus(NavienBaseModel): "User can override DR commands for up to 72 hours" ) ) - tou_override_status: int = Field( + tou_override_status: TouOverride = Field( description=( - "Time of Use override status. " - "User can temporarily override TOU schedule" + "TOU override status. " + "True = user has overridden TOU to force immediate heating, " + "False = device follows TOU schedule normally" ) ) total_energy_capacity: float = Field( @@ -475,7 +550,13 @@ class DeviceStatus(NavienBaseModel): error_buzzer_use: DeviceBool = Field( description="Whether the error buzzer is enabled" ) - current_heat_use: DeviceBool = Field(description="Current heat usage") + current_heat_use: HeatSource = Field( + description=( + "Currently active heat source. Indicates which heating " + "component(s) are actively running: 0=Unknown/not heating, " + "1=Heat Pump, 2=Electric Element, 3=Both simultaneously" + ) + ) heat_upper_use: DeviceBool = Field( description=( "Upper electric heating element usage status. " @@ -640,8 +721,8 @@ class DeviceStatus(NavienBaseModel): ) # Fields with scale division (raw / 10.0) - current_inlet_temperature: Div10 = Field( - description="Current inlet temperature", + current_inlet_temperature: HalfCelsiusToF = Field( + description="Cold water inlet temperature", json_schema_extra={ "unit_of_measurement": "°F", "device_class": "temperature", @@ -797,8 +878,8 @@ class DeviceStatus(NavienBaseModel): default=DhwOperationSetting.ENERGY_SAVER, description="User's configured DHW operation mode preference", ) - temperature_type: TemperatureUnit = Field( - default=TemperatureUnit.FAHRENHEIT, + temperature_type: TemperatureType = Field( + default=TemperatureType.FAHRENHEIT, description="Type of temperature unit", ) freeze_protection_temp_min: HalfCelsiusToF = Field( @@ -833,7 +914,7 @@ class DeviceFeature(NavienBaseModel): "(1=USA, complies with FCC Part 15 Class B)" ) ) - model_type_code: int = Field( + model_type_code: Union[UnitType, int] = Field( description="Model type identifier: NWP500 series model variant" ) control_type_code: int = Field( @@ -888,119 +969,118 @@ class DeviceFeature(NavienBaseModel): "for warranty and service identification" ) ) - power_use: int = Field( - description=( - "Power control capability (1=can be turned on/off via controls)" - ) + power_use: CapabilityFlag = Field( + description=("Power control capability (2=supported, 1=not supported)") ) - holiday_use: int = Field( + holiday_use: CapabilityFlag = Field( description=( - "Vacation mode support (1=supported) - " + "Vacation mode support (2=supported, 1=not supported) - " "energy-saving mode for 0-99 days" ) ) - program_reservation_use: int = Field( + program_reservation_use: CapabilityFlag = Field( description=( - "Scheduled operation support (1=supported) - " + "Scheduled operation support (2=supported, 1=not supported) - " "programmable heating schedules" ) ) - dhw_use: int = Field( + dhw_use: CapabilityFlag = Field( description=( - "Domestic hot water functionality (1=available) - " + "Domestic hot water functionality (2=supported, 1=not supported) - " "primary function of water heater" ) ) - dhw_temperature_setting_use: int = Field( + dhw_temperature_setting_use: CapabilityFlag = Field( description=( - "Temperature adjustment capability (1=supported) - " + "Temperature adjustment capability " + "(2=supported, 1=not supported) - " "user can modify target temperature" ) ) - smart_diagnostic_use: int = Field( + smart_diagnostic_use: CapabilityFlag = Field( description=( - "Self-diagnostic capability (1=available) - " + "Self-diagnostic capability (2=supported, 1=not supported) - " "10-minute startup diagnostic, error code system" ) ) - wifi_rssi_use: int = Field( + wifi_rssi_use: CapabilityFlag = Field( description=( - "WiFi signal monitoring (1=supported) - " + "WiFi signal monitoring (2=supported, 1=not supported) - " "reports signal strength in dBm" ) ) - temp_formula_type: int = Field( + temp_formula_type: TempFormulaType = Field( description=( "Temperature calculation method identifier " "for internal sensor calibration" ) ) - energy_usage_use: int = Field( + energy_usage_use: CapabilityFlag = Field( description=( "Energy monitoring support (1=available) - tracks kWh consumption" ) ) - freeze_protection_use: int = Field( + freeze_protection_use: CapabilityFlag = Field( description=( "Freeze protection capability (1=available) - " "automatic heating when tank drops below threshold" ) ) - mixing_value_use: int = Field( + mixing_value_use: CapabilityFlag = Field( description=( "Thermostatic mixing valve support (1=available) - " "for temperature limiting at point of use" ) ) - dr_setting_use: int = Field( + dr_setting_use: CapabilityFlag = Field( description=( "Demand Response support (1=available) - " "CTA-2045 compliance for utility load management" ) ) - anti_legionella_setting_use: int = Field( + anti_legionella_setting_use: CapabilityFlag = Field( description=( "Anti-Legionella function (1=available) - " "periodic heating to 140°F (60°C) to prevent bacteria" ) ) - hpwh_use: int = Field( + hpwh_use: CapabilityFlag = Field( description=( "Heat Pump Water Heater mode (1=supported) - " "primary efficient heating using refrigeration cycle" ) ) - dhw_refill_use: int = Field( + dhw_refill_use: CapabilityFlag = Field( description=( "Tank refill detection (1=supported) - " "monitors for dry fire conditions during refill" ) ) - eco_use: int = Field( + eco_use: CapabilityFlag = Field( description=( "ECO safety switch capability (1=available) - " "Energy Cut Off high-temperature limit protection" ) ) - electric_use: int = Field( + electric_use: CapabilityFlag = Field( description=( "Electric-only mode (1=supported) - " "heating element only for maximum recovery speed" ) ) - heatpump_use: int = Field( + heatpump_use: CapabilityFlag = Field( description=( "Heat pump only mode (1=supported) - " "most efficient operation using only refrigeration cycle" ) ) - energy_saver_use: int = Field( + energy_saver_use: CapabilityFlag = Field( description=( "Energy Saver mode (1=supported) - " "hybrid efficiency mode balancing speed and efficiency (default)" ) ) - high_demand_use: int = Field( + high_demand_use: CapabilityFlag = Field( description=( "High Demand mode (1=supported) - " "hybrid boost mode prioritizing fast recovery" @@ -1050,8 +1130,8 @@ class DeviceFeature(NavienBaseModel): ) # Enum field - temperature_type: TemperatureUnit = Field( - default=TemperatureUnit.FAHRENHEIT, + temperature_type: TemperatureType = Field( + default=TemperatureType.FAHRENHEIT, description=( "Default temperature unit preference - " "factory set to Fahrenheit for USA" @@ -1068,7 +1148,7 @@ class MqttRequest(NavienBaseModel): """MQTT command request payload.""" command: int - device_type: int + device_type: Union[DeviceType, int] mac_address: str additional_value: str = "..." mode: Optional[str] = None diff --git a/src/nwp500/mqtt_client.py b/src/nwp500/mqtt_client.py index eacd6ba..3073e77 100644 --- a/src/nwp500/mqtt_client.py +++ b/src/nwp500/mqtt_client.py @@ -898,7 +898,7 @@ async def set_dhw_mode( device: Device object mode_id: Mode ID (1=Heat Pump Only, 2=Electric Only, 3=Energy Saver, 4=High Demand, 5=Vacation) - vacation_days: Number of vacation days (required when mode_id == 5) + vacation_days: Number of vacation days (required for Vacation mode) Returns: Publish packet ID diff --git a/src/nwp500/mqtt_device_control.py b/src/nwp500/mqtt_device_control.py index 8dfc433..0dd007c 100644 --- a/src/nwp500/mqtt_device_control.py +++ b/src/nwp500/mqtt_device_control.py @@ -18,9 +18,9 @@ from datetime import datetime from typing import Any, Callable, Optional -from .constants import CommandCode +from .enums import CommandCode, DhwOperationSetting from .exceptions import ParameterValidationError, RangeValidationError -from .models import Device, DhwOperationSetting, fahrenheit_to_half_celsius +from .models import Device, fahrenheit_to_half_celsius __author__ = "Emmanuel Levijarvi" @@ -196,7 +196,7 @@ async def set_dhw_mode( device: Device object mode_id: Mode ID (1=Heat Pump Only, 2=Electric Only, 3=Energy Saver, 4=High Demand, 5=Vacation) - vacation_days: Number of vacation days (required when mode_id == 5) + vacation_days: Number of vacation days (required for Vacation mode) Returns: Publish packet ID @@ -300,7 +300,7 @@ async def enable_anti_legionella( command = self._build_command( device_type=device_type, device_id=device_id, - command=CommandCode.ANTI_LEGIONELLA_ENABLE, + command=CommandCode.ANTI_LEGIONELLA_ON, additional_value=additional_value, mode="anti-leg-on", param=[period_days], @@ -339,7 +339,7 @@ async def disable_anti_legionella(self, device: Device) -> int: command = self._build_command( device_type=device_type, device_id=device_id, - command=CommandCode.ANTI_LEGIONELLA_DISABLE, + command=CommandCode.ANTI_LEGIONELLA_OFF, additional_value=additional_value, mode="anti-leg-off", param=[], @@ -522,7 +522,7 @@ async def configure_tou_schedule( command = self._build_command( device_type=device_type, device_id=device_id, - command=CommandCode.TOU_SETTINGS, + command=CommandCode.TOU_RESERVATION, additional_value=additional_value, controllerSerialNumber=controller_serial_number, reservationUse=reservation_use, @@ -568,7 +568,7 @@ async def request_tou_settings( command = self._build_command( device_type=device_type, device_id=device_id, - command=CommandCode.TOU_SETTINGS, + command=CommandCode.TOU_RESERVATION, additional_value=additional_value, controllerSerialNumber=controller_serial_number, ) @@ -596,9 +596,7 @@ async def set_tou_enabled(self, device: Device, enabled: bool) -> int: device_topic = f"navilink-{device_id}" topic = f"cmd/{device_type}/{device_topic}/ctrl" - command_code = ( - CommandCode.TOU_ENABLE if enabled else CommandCode.TOU_DISABLE - ) + command_code = CommandCode.TOU_ON if enabled else CommandCode.TOU_OFF mode = "tou-on" if enabled else "tou-off" command = self._build_command( diff --git a/tests/test_models.py b/tests/test_models.py index 95521c6..7088a98 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -126,10 +126,11 @@ def test_device_status_deci_celsius_to_fahrenheit(default_status_data): def test_device_status_div10(default_status_data): - """Test Div10 conversion.""" - default_status_data["currentInletTemperature"] = 500 + """Test currentInletTemperature HalfCelsiusToF conversion.""" + # Raw value 100 = 50°C = (50 * 1.8) + 32 = 122°F + default_status_data["currentInletTemperature"] = 100 status = DeviceStatus.model_validate(default_status_data) - assert status.current_inlet_temperature == 50.0 + assert status.current_inlet_temperature == 122.0 def test_fahrenheit_to_half_celsius():