From ce4753583ab0e85bf3babc48fe6704b3160492e1 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 08:56:59 -0800 Subject: [PATCH 01/21] Add comprehensive enumerations module for type-safe device control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGES: - Migrated command codes to DeviceControl enum - Renamed TemperatureUnit to TemperatureType - Updated capability flags to use CapabilityFlag type Added: - src/nwp500/enums.py with 12+ enumerations for device protocol - ErrorCode enum with complete error mappings - DeviceControl enum for all MQTT commands - Status enums: OnOffFlag, Operation, DhwOperationSetting, etc. - Human-readable text mappings for all enums - docs/enumerations.rst comprehensive reference - examples/error_code_demo.py showing enum usage Changed: - CLI output uses enum-based text mappings - Documentation updated across guides and protocol docs - Examples updated to use new enums - Command constants: ANTI_LEGIONELLA_ENABLE→ON, TOU_ENABLE→ON, etc. Fixed: - Temperature conversion test now uses correct HalfCelsiusToF formula --- CHANGELOG.rst | 66 +++- docs/enumerations.rst | 253 ++++++++++++++ docs/guides/time_of_use.rst | 6 +- docs/index.rst | 1 + docs/protocol/data_conversions.rst | 4 +- docs/protocol/device_features.rst | 49 +-- docs/protocol/mqtt_protocol.rst | 472 +++++++++++++++++++++++++-- docs/python_api/models.rst | 175 +++------- examples/anti_legionella_example.py | 6 +- examples/device_feature_callback.py | 43 +-- examples/error_code_demo.py | 155 +++++++++ examples/event_emitter_demo.py | 13 +- examples/mqtt_diagnostics_example.py | 25 +- src/nwp500/__init__.py | 41 ++- src/nwp500/cli/commands.py | 50 +-- src/nwp500/cli/output_formatters.py | 15 +- src/nwp500/constants.py | 50 ++- src/nwp500/enums.py | 427 ++++++++++++++++++++++++ src/nwp500/models.py | 202 +++++++----- src/nwp500/mqtt_device_control.py | 16 +- tests/test_models.py | 7 +- 21 files changed, 1706 insertions(+), 370 deletions(-) create mode 100644 docs/enumerations.rst create mode 100644 examples/error_code_demo.py create mode 100644 src/nwp500/enums.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 57b4a0a..9b4a5c7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,70 @@ Changelog ========= +Version 6.2.0 (2025-12-17) +========================== + +**BREAKING CHANGES**: Enumerations refactored for type safety and consistency + +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: ``DeviceControl`` (replaces individual constants) + - Error code enum: ``ErrorCode`` with complete error code mappings + - Human-readable text mappings for all enums (e.g., ``DHW_OPERATION_TEXT``, ``ERROR_CODE_TEXT``) + - Exported from main package: ``from nwp500 import OnOffFlag, ErrorCode, DeviceControl`` + - Comprehensive documentation in ``docs/enumerations.rst`` + - Example usage in ``examples/error_code_demo.py`` + +Changed +------- + +- **Command Code Constants**: Migrated from ``constants.py`` to ``DeviceControl`` enum in ``enums.py`` + + - ``CommandCode.ANTI_LEGIONELLA_ENABLE`` → ``DeviceControl.ANTI_LEGIONELLA_ON`` + - ``CommandCode.ANTI_LEGIONELLA_DISABLE`` → ``DeviceControl.ANTI_LEGIONELLA_OFF`` + - ``CommandCode.TOU_ENABLE`` → ``DeviceControl.TOU_ON`` + - ``CommandCode.TOU_DISABLE`` → ``DeviceControl.TOU_OFF`` + - ``CommandCode.TOU_SETTINGS`` → ``DeviceControl.TOU_RESERVATION`` + - All command constants now use consistent naming in ``DeviceControl`` enum + - Legacy ``CommandCode`` class retained in ``constants.py`` for backward compatibility + +- **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 + +- **CLI Output**: Enhanced status display with enum-based text mappings + + - Operation mode, heat source, DR event, and recirculation mode now use human-readable text + - Consistent formatting across all status fields + +- **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 ``DeviceControl`` 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 + +Fixed +----- + +- **Temperature Conversion Test**: Corrected ``test_device_status_div10`` to use ``HalfCelsiusToF`` conversion (100 → 122°F, not 50.0) + Version 6.1.1 (2025-12-08) ========================== @@ -238,7 +302,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..f9e80cf --- /dev/null +++ b/docs/enumerations.rst @@ -0,0 +1,253 @@ +Enumerations Reference +====================== + +This document provides a comprehensive reference for all enumerations used in +the Navien NWP500 protocol. + +Device Control Commands +----------------------- + +.. autoclass:: nwp500.enums.DeviceControl + :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_OPERATION_MODE`` +- **TOU**: ``TOU_ON``, ``TOU_OFF`` +- **Maintenance**: ``AIR_FILTER_RESET``, ``ANTI_LEGIONELLA_ON`` + +Example usage:: + + from nwp500 import DeviceControl + + # Send temperature command + command = DeviceControl.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. + +OperationMode +~~~~~~~~~~~~~ + +.. autoclass:: nwp500.enums.OperationMode + :members: + :undoc-members: + +DHW heating mode for heat pump water heaters. This determines which heat source(s) +the device will use: + +- **HEATPUMP**: Most efficient but slower heating +- **HYBRID**: Balance of efficiency and speed +- **ELECTRIC**: Fastest but uses most energy + +Example:: + + from nwp500 import OperationMode, OPERATION_MODE_TEXT + + mode = OperationMode.HYBRID + print(f"Current mode: {OPERATION_MODE_TEXT[mode]}") # "Hybrid" + +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 ( + OPERATION_MODE_TEXT, + HEAT_SOURCE_TEXT, + DR_EVENT_TEXT, + RECIRC_MODE_TEXT, + TOU_RATE_TEXT, + FILTER_STATUS_TEXT, + ) + + # Usage + mode = OperationMode.HYBRID + print(OPERATION_MODE_TEXT[mode]) # "Hybrid" + +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..647f2d5 100644 --- a/docs/python_api/models.rst +++ b/docs/python_api/models.rst @@ -19,131 +19,31 @@ 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: + +* **OperationMode** - DHW heating modes (Heat Pump/Hybrid/Electric) +* **HeatSource** - Currently active heat source +* **TemperatureType** - Temperature unit (Celsius/Fahrenheit) +* **DeviceControl** - All control command IDs +* **TouRateType** - Time-of-Use rate periods +* And many more protocol enumerations + +**Quick Reference:** + +.. code-block:: python + + from nwp500 import OperationMode, HeatSource, TemperatureType + + # Set operation mode + await mqtt.set_dhw_mode(device, OperationMode.HYBRID.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 +204,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`` (OperationMode) - Current operational state + * ``dhw_operation_setting`` (OperationMode) - User's mode preference + * ``current_heat_use`` (HeatSource) - Currently active heat source + * ``temperature_type`` (TemperatureType) - Temperature unit **Boolean Status Fields:** @@ -422,8 +323,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:** @@ -668,11 +569,11 @@ Best Practices .. code-block:: python # [OK] Type-safe - from nwp500 import DhwOperationSetting - await mqtt.set_dhw_mode(device, DhwOperationSetting.ENERGY_SAVER.value) + from nwp500 import OperationMode + await mqtt.set_dhw_mode(device, OperationMode.HYBRID.value) # ✗ Magic numbers - await mqtt.set_dhw_mode(device, 3) + await mqtt.set_dhw_mode(device, 2) 2. **Check feature support:** @@ -690,12 +591,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 HYBRID, 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..18face4 100644 --- a/examples/anti_legionella_example.py +++ b/examples/anti_legionella_example.py @@ -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/device_feature_callback.py b/examples/device_feature_callback.py index 44e4ae9..3a4726c 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.OFF 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.OFF 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..48c8a47 --- /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: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: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..b211b08 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,8 @@ 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_BOOST_MODE: - print("⚡ [Optimizer] High demand mode - fast recovery!") + elif new_mode == CurrentOperationMode.HYBRID_EFFICIENCY_MODE: + print("⚡ [Optimizer] Hybrid mode - balanced performance!") # Example 3: Power state handlers diff --git a/examples/mqtt_diagnostics_example.py b/examples/mqtt_diagnostics_example.py index a6e49d7..5f145c2 100755 --- a/examples/mqtt_diagnostics_example.py +++ b/examples/mqtt_diagnostics_example.py @@ -56,15 +56,10 @@ def __init__(self): 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_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) + def handle_shutdown(self) -> None: + """Handle shutdown signal.""" + _logger.info("Shutting down gracefully...") + self.running = False async def export_diagnostics(self, interval: float = 300.0) -> None: """ @@ -179,7 +174,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,7 +242,12 @@ async def run_example( "Press Ctrl+C to stop early." ) - await asyncio.sleep(duration_seconds) + # Sleep in small intervals to check running flag + elapsed = 0.0 + interval = 1.0 + while self.running and elapsed < duration_seconds: + await asyncio.sleep(min(interval, duration_seconds - elapsed)) + elapsed += interval except asyncio.CancelledError: _logger.info("Example cancelled") diff --git a/src/nwp500/__init__.py b/src/nwp500/__init__.py index 7faf309..1323601 100644 --- a/src/nwp500/__init__.py +++ b/src/nwp500/__init__.py @@ -30,6 +30,9 @@ authenticate, refresh_access_token, ) +from nwp500.constants import ( + CommandCode, +) from nwp500.encoding import ( build_reservation_entry, build_tou_period, @@ -40,6 +43,22 @@ encode_season_bitfield, encode_week_bitfield, ) +from nwp500.enums import ( + CurrentOperationMode, + DhwOperationSetting, + DREvent, + ErrorCode, + FilterChange, + HeatSource, + OnOffFlag, + Operation, + RecirculationMode, + TemperatureType, + TempFormulaType, + TouRateType, + TouWeekType, + UnitType, +) from nwp500.events import ( EventEmitter, EventListener, @@ -66,12 +85,10 @@ ValidationError, ) from nwp500.models import ( - CurrentOperationMode, Device, DeviceFeature, DeviceInfo, DeviceStatus, - DhwOperationSetting, EnergyUsageDay, EnergyUsageResponse, EnergyUsageTotal, @@ -80,7 +97,6 @@ MonthlyEnergyData, MqttCommand, MqttRequest, - TemperatureUnit, TOUInfo, TOUSchedule, fahrenheit_to_half_celsius, @@ -108,15 +124,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..6c48747 100644 --- a/src/nwp500/constants.py +++ b/src/nwp500/constants.py @@ -41,18 +41,54 @@ class CommandCode(IntEnum): 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 + # Control Commands - DHW (Domestic Hot Water) Operation + DHW_MODE = 33554437 # Change DHW operation mode (Heat Pump/Electric/Hybrid) 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_DISABLE = 33554471 # Disable anti-legionella cycle - ANTI_LEGIONELLA_ENABLE = 33554472 # Enable anti-legionella cycle + 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_DISABLE = 33554475 # Disable TOU optimization - TOU_ENABLE = 33554476 # Enable TOU optimization + TOU_OFF = 33554475 # Disable TOU optimization + TOU_ON = 33554476 # Enable TOU optimization # Note for maintainers: diff --git a/src/nwp500/enums.py b/src/nwp500/enums.py new file mode 100644 index 0000000..17296e6 --- /dev/null +++ b/src/nwp500/enums.py @@ -0,0 +1,427 @@ +"""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 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..d24f0ac 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,11 +91,30 @@ 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): @@ -87,34 +124,36 @@ class NavienBaseModel(BaseModel): alias_generator=to_camel, populate_by_name=True, extra="ignore", # Ignore unknown fields by default - ) - - -class DhwOperationSetting(Enum): - """DHW operation setting modes (user-configured heating preferences).""" - - 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.""" - - CELSIUS = 1 - FAHRENHEIT = 2 + use_enum_values=False, # Serialize enums as enum objects, not values + ) + + 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 + + @staticmethod + def _convert_enums_to_names(data: Any) -> Any: + """Recursively convert Enum values to their names.""" + from enum import Enum + + if isinstance(data, Enum): + return data.name + elif isinstance(data, dict): + return { + key: NavienBaseModel._convert_enums_to_names(value) + for key, value in data.items() + } + elif isinstance(data, (list, tuple)): + return type(data)( + NavienBaseModel._convert_enums_to_names(item) for item in data + ) + return data class DeviceInfo(NavienBaseModel): @@ -123,7 +162,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 +191,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 +269,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 +321,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 +374,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 +386,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 +518,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 +689,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 +846,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 +882,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 +937,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 +1098,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 +1116,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_device_control.py b/src/nwp500/mqtt_device_control.py index 8dfc433..3d49854 100644 --- a/src/nwp500/mqtt_device_control.py +++ b/src/nwp500/mqtt_device_control.py @@ -20,7 +20,7 @@ from .constants import CommandCode 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" @@ -214,7 +214,7 @@ async def set_dhw_mode( - 4: High Demand (maximum heating capacity) - 5: Vacation Mode (requires vacation_days parameter) """ - if mode_id == DhwOperationSetting.VACATION.value: + if mode_id == 5: # Vacation mode if vacation_days is None: raise ParameterValidationError( "Vacation mode requires vacation_days (1-30)", @@ -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(): From 276a9f5c35d9675b5f9d8f2999cf8ad59071a372 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 09:10:42 -0800 Subject: [PATCH 02/21] Fix changelog: clarify enum serialization happens in model not CLI --- CHANGELOG.rst | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9b4a5c7..32051ab 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -41,10 +41,11 @@ Changed - All capability flags (e.g., ``power_use``, ``dhw_use``) now use ``CapabilityFlag`` type - ``MqttRequest.device_type`` now accepts ``Union[DeviceType, int]`` for flexibility -- **CLI Output**: Enhanced status display with enum-based text mappings +- **Model Serialization**: Enums automatically serialize to human-readable names - - Operation mode, heat source, DR event, and recirculation mode now use human-readable text - - Consistent formatting across all status fields + - `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 @@ -61,6 +62,11 @@ Changed - ``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 ----- From 0e7c55babd016e3e322da1856707a8210db8ce1e Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 09:35:32 -0800 Subject: [PATCH 03/21] Fix documentation: replace non-existent OperationMode with correct enums - Replace OperationMode with DhwOperationSetting (user-configured mode) - Replace OperationMode with CurrentOperationMode (real-time state) - Update text mapping examples to use correct constants - Add both enums to enumerations.rst with clear explanations - Update models.rst type annotations and examples --- docs/enumerations.rst | 68 ++++++++++++++++++++++++++++++-------- docs/python_api/models.rst | 21 ++++++------ 2 files changed, 65 insertions(+), 24 deletions(-) diff --git a/docs/enumerations.rst b/docs/enumerations.rst index f9e80cf..1511341 100644 --- a/docs/enumerations.rst +++ b/docs/enumerations.rst @@ -52,26 +52,53 @@ Operation Device operation state indicating overall device activity. -OperationMode -~~~~~~~~~~~~~ +DhwOperationSetting +~~~~~~~~~~~~~~~~~~~ -.. autoclass:: nwp500.enums.OperationMode +.. autoclass:: nwp500.enums.DhwOperationSetting :members: :undoc-members: -DHW heating mode for heat pump water heaters. This determines which heat source(s) -the device will use: +User-configured DHW heating mode preference. This determines which heat source(s) +the device will use when heating is needed: -- **HEATPUMP**: Most efficient but slower heating -- **HYBRID**: Balance of efficiency and speed +- **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 OperationMode, OPERATION_MODE_TEXT + from nwp500 import DhwOperationSetting + from nwp500.enums import DHW_OPERATION_SETTING_TEXT - mode = OperationMode.HYBRID - print(f"Current mode: {OPERATION_MODE_TEXT[mode]}") # "Hybrid" + 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 ~~~~~~~~~~ @@ -231,17 +258,30 @@ user-friendly display text: .. code-block:: python from nwp500.enums import ( - OPERATION_MODE_TEXT, + 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 - mode = OperationMode.HYBRID - print(OPERATION_MODE_TEXT[mode]) # "Hybrid" + # 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 --------------------- diff --git a/docs/python_api/models.rst b/docs/python_api/models.rst index 647f2d5..3714552 100644 --- a/docs/python_api/models.rst +++ b/docs/python_api/models.rst @@ -21,7 +21,8 @@ Enumerations See :doc:`../enumerations` for the complete enumeration reference including: -* **OperationMode** - DHW heating modes (Heat Pump/Hybrid/Electric) +* **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) * **DeviceControl** - All control command IDs @@ -32,10 +33,10 @@ See :doc:`../enumerations` for the complete enumeration reference including: .. code-block:: python - from nwp500 import OperationMode, HeatSource, TemperatureType + from nwp500 import DhwOperationSetting, CurrentOperationMode, HeatSource, TemperatureType - # Set operation mode - await mqtt.set_dhw_mode(device, OperationMode.HYBRID.value) + # 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: @@ -204,8 +205,8 @@ Complete real-time device status with 100+ fields. **Operation Mode Fields:** - * ``operation_mode`` (OperationMode) - Current operational state - * ``dhw_operation_setting`` (OperationMode) - User's mode preference + * ``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 @@ -568,12 +569,12 @@ Best Practices .. code-block:: python - # [OK] Type-safe - from nwp500 import OperationMode - await mqtt.set_dhw_mode(device, OperationMode.HYBRID.value) + # ✓ Type-safe + from nwp500 import DhwOperationSetting + await mqtt.set_dhw_mode(device, DhwOperationSetting.ENERGY_SAVER.value) # ✗ Magic numbers - await mqtt.set_dhw_mode(device, 2) + await mqtt.set_dhw_mode(device, 3) 2. **Check feature support:** From 9416fb782e008984066b9766b6c77591e93f16e5 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 09:35:50 -0800 Subject: [PATCH 04/21] Update changelog with documentation fix --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 32051ab..75c69d6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -71,6 +71,7 @@ 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) ========================== From 237b03e0482ec0658dd718a74e78c9f7ddfc875a Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 09:36:50 -0800 Subject: [PATCH 05/21] Clarify use_enum_values comment: affects validation not serialization The use_enum_values=False parameter controls deserialization behavior (keeps enums as enum objects during validation), not serialization. Actual serialization to names is handled by the custom model_dump() method. --- src/nwp500/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nwp500/models.py b/src/nwp500/models.py index d24f0ac..7eb2908 100644 --- a/src/nwp500/models.py +++ b/src/nwp500/models.py @@ -124,7 +124,7 @@ class NavienBaseModel(BaseModel): alias_generator=to_camel, populate_by_name=True, extra="ignore", # Ignore unknown fields by default - use_enum_values=False, # Serialize enums as enum objects, not values + use_enum_values=False, # Keep enums as objects during validation (model_dump() converts to names) ) def model_dump(self, **kwargs: Any) -> dict[str, Any]: From c603e41b5ece9b22965947be0ddf39531fa64dbc Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 09:39:37 -0800 Subject: [PATCH 06/21] Fix comment: replace invalid HYBRID with ENERGY_SAVER enum value DhwOperationSetting does not have a HYBRID member. The hybrid modes are: - ENERGY_SAVER (value 3) - Hybrid: Efficiency mode - HIGH_DEMAND (value 4) - Hybrid: Boost mode --- docs/python_api/models.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/python_api/models.rst b/docs/python_api/models.rst index 3714552..c76e5b7 100644 --- a/docs/python_api/models.rst +++ b/docs/python_api/models.rst @@ -597,7 +597,7 @@ Best Practices active_source = status.current_heat_use # These can differ! - # User sets HYBRID, but device might only be using heat pump at the moment + # User sets ENERGY_SAVER (hybrid mode), but device might only be using heat pump at the moment Related Documentation ===================== From 0c85b448783abf0e2226b1718cdf02ee6d2d0eb5 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 09:42:01 -0800 Subject: [PATCH 07/21] Fix mode descriptions in event_emitter_demo for accuracy - Changed 'Hybrid mode' to 'Energy Saver mode' for HYBRID_EFFICIENCY_MODE - Added missing handler for HYBRID_BOOST_MODE (High Demand mode) - Ensures mode names match actual enum values and user-facing terminology --- examples/event_emitter_demo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/event_emitter_demo.py b/examples/event_emitter_demo.py index b211b08..e936568 100644 --- a/examples/event_emitter_demo.py +++ b/examples/event_emitter_demo.py @@ -69,7 +69,9 @@ def optimize_on_mode_change( if new_mode == CurrentOperationMode.HEAT_PUMP_MODE: print("♻️ [Optimizer] Heat pump mode - maximum efficiency!") elif new_mode == CurrentOperationMode.HYBRID_EFFICIENCY_MODE: - print("⚡ [Optimizer] Hybrid mode - balanced performance!") + print("⚡ [Optimizer] Energy Saver mode - balanced performance!") + elif new_mode == CurrentOperationMode.HYBRID_BOOST_MODE: + print("⚡ [Optimizer] High Demand mode - fast recovery!") # Example 3: Power state handlers From f4b8143692fc084c9fa99d7412786be018a76adc Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 09:43:37 -0800 Subject: [PATCH 08/21] Fix line length and move enum comment to docstring --- src/nwp500/models.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/nwp500/models.py b/src/nwp500/models.py index 7eb2908..4358ea5 100644 --- a/src/nwp500/models.py +++ b/src/nwp500/models.py @@ -118,13 +118,17 @@ def _tou_override_validator(v: Any) -> bool: 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 (model_dump() converts to names) + use_enum_values=False, # Keep enums as objects during validation ) def model_dump(self, **kwargs: Any) -> dict[str, Any]: From 2b3ee28827b346145fa05b17dd5563198adbd61d Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 09:45:56 -0800 Subject: [PATCH 09/21] Fix backwards OnOffFlag logic in device_feature_callback Changed two comparisons from checking OnOffFlag.OFF to OnOffFlag.ON: - smart_diagnostic_use: OFF (1) = not supported, ON (2) = supported - mixing_value_use: OFF (1) = not supported, ON (2) = supported The previous logic showed 'Yes' when the flag was OFF, which was incorrect. --- examples/device_feature_callback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/device_feature_callback.py b/examples/device_feature_callback.py index 3a4726c..3896e42 100644 --- a/examples/device_feature_callback.py +++ b/examples/device_feature_callback.py @@ -181,7 +181,7 @@ def on_device_feature(feature: DeviceFeature): f" Program Schedule: {'Yes' if feature.program_reservation_use == OnOffFlag.ON else 'No'}" ) print( - f" Smart Diagnostic: {'Yes' if feature.smart_diagnostic_use == OnOffFlag.OFF else 'No'}" + f" Smart Diagnostic: {'Yes' if feature.smart_diagnostic_use == OnOffFlag.ON else 'No'}" ) print( f" WiFi RSSI: {'Yes' if feature.wifi_rssi_use == OnOffFlag.ON else 'No'}" @@ -193,7 +193,7 @@ def on_device_feature(feature: DeviceFeature): f" Freeze Protection: {'Yes' if feature.freeze_protection_use == OnOffFlag.ON else 'No'}" ) print( - f" Mixing Valve: {'Yes' if feature.mixing_value_use == OnOffFlag.OFF else 'No'}" + f" Mixing Valve: {'Yes' if feature.mixing_value_use == OnOffFlag.ON else 'No'}" ) print( f" DR Settings: {'Yes' if feature.dr_setting_use == OnOffFlag.ON else 'No'}" From 6af1d7a792b555db503e0a6be6ad47993160172f Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 10:00:39 -0800 Subject: [PATCH 10/21] Fix regression: restore DhwOperationSetting enum usage Reverted magic number 5 back to DhwOperationSetting.VACATION.value. Added missing import from enums module. Updated docstring to avoid magic number reference. --- src/nwp500/mqtt_device_control.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/nwp500/mqtt_device_control.py b/src/nwp500/mqtt_device_control.py index 3d49854..9e71cca 100644 --- a/src/nwp500/mqtt_device_control.py +++ b/src/nwp500/mqtt_device_control.py @@ -19,6 +19,7 @@ from typing import Any, Callable, Optional from .constants import CommandCode +from .enums import DhwOperationSetting from .exceptions import ParameterValidationError, RangeValidationError from .models import Device, fahrenheit_to_half_celsius @@ -196,7 +197,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 @@ -214,7 +215,7 @@ async def set_dhw_mode( - 4: High Demand (maximum heating capacity) - 5: Vacation Mode (requires vacation_days parameter) """ - if mode_id == 5: # Vacation mode + if mode_id == DhwOperationSetting.VACATION.value: if vacation_days is None: raise ParameterValidationError( "Vacation mode requires vacation_days (1-30)", From 8933fe1af7ec7f698a8839ac04b751ea7834d786 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 10:04:15 -0800 Subject: [PATCH 11/21] Replace all remaining magic numbers with enum values Fixed magic number usage in: - examples/anti_legionella_example.py: 2 -> OnOffFlag.ON - examples/combined_callbacks.py: 2 -> OnOffFlag.ON - src/nwp500/mqtt_client.py: Updated docstring All enum values now use proper enum references for type safety. --- examples/anti_legionella_example.py | 5 +++-- examples/combined_callbacks.py | 7 +++++-- src/nwp500/mqtt_client.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/anti_legionella_example.py b/examples/anti_legionella_example.py index 18face4..3395793 100644 --- a/examples/anti_legionella_example.py +++ b/examples/anti_legionella_example.py @@ -18,14 +18,15 @@ from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient from nwp500.constants import CommandCode +from nwp500.enums import 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 "" 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/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 From 55bb1a6c157294b221e8b15bb06562dc44df34d2 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 10:12:20 -0800 Subject: [PATCH 12/21] Relocate CommandCode enum to enums.py module Moved CommandCode from constants.py to enums.py for better organization: - All enumerations now in a single module - constants.py now only contains firmware metadata - Backward compatibility maintained via re-export in constants.py - Updated imports in mqtt_device_control.py to use enums module - Updated __init__.py to export CommandCode from enums This consolidates all protocol enumerations in one place. --- src/nwp500/__init__.py | 4 +- src/nwp500/constants.py | 95 ++----------------------------- src/nwp500/enums.py | 74 ++++++++++++++++++++++++ src/nwp500/mqtt_device_control.py | 3 +- 4 files changed, 81 insertions(+), 95 deletions(-) diff --git a/src/nwp500/__init__.py b/src/nwp500/__init__.py index 1323601..0e95b00 100644 --- a/src/nwp500/__init__.py +++ b/src/nwp500/__init__.py @@ -30,9 +30,6 @@ authenticate, refresh_access_token, ) -from nwp500.constants import ( - CommandCode, -) from nwp500.encoding import ( build_reservation_entry, build_tou_period, @@ -44,6 +41,7 @@ encode_week_bitfield, ) from nwp500.enums import ( + CommandCode, CurrentOperationMode, DhwOperationSetting, DREvent, diff --git a/src/nwp500/constants.py b/src/nwp500/constants.py index 6c48747..1d87fe1 100644 --- a/src/nwp500/constants.py +++ b/src/nwp500/constants.py @@ -1,95 +1,10 @@ -"""Constants and command codes for Navien device communication.""" +"""Constants 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) Operation - DHW_MODE = 33554437 # Change DHW operation mode (Heat Pump/Electric/Hybrid) - 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 +Note: CommandCode has been moved to enums.py module. +For backward compatibility, CommandCode is re-exported from this module. +""" +from .enums import CommandCode # noqa: F401 # Note for maintainers: # Command codes and expected payload fields are defined in diff --git a/src/nwp500/enums.py b/src/nwp500/enums.py index 17296e6..f66d42e 100644 --- a/src/nwp500/enums.py +++ b/src/nwp500/enums.py @@ -236,6 +236,80 @@ class DeviceType(IntEnum): 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.""" diff --git a/src/nwp500/mqtt_device_control.py b/src/nwp500/mqtt_device_control.py index 9e71cca..0dd007c 100644 --- a/src/nwp500/mqtt_device_control.py +++ b/src/nwp500/mqtt_device_control.py @@ -18,8 +18,7 @@ from datetime import datetime from typing import Any, Callable, Optional -from .constants import CommandCode -from .enums import DhwOperationSetting +from .enums import CommandCode, DhwOperationSetting from .exceptions import ParameterValidationError, RangeValidationError from .models import Device, fahrenheit_to_half_celsius From 1c254db9f37b23d18921e34ea8ad80c9f8192193 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 10:14:01 -0800 Subject: [PATCH 13/21] Remove backward compatibility for CommandCode import Per project policy, no backward compatibility needed. - Removed re-export from constants.py - Updated constants.py to note new import location - Added breaking change to CHANGELOG with migration example Users must update: from nwp500.constants -> from nwp500.enums --- CHANGELOG.rst | 12 ++++++++++++ src/nwp500/constants.py | 4 +--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 75c69d6..4ff4a21 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,18 @@ 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 ----- diff --git a/src/nwp500/constants.py b/src/nwp500/constants.py index 1d87fe1..411867a 100644 --- a/src/nwp500/constants.py +++ b/src/nwp500/constants.py @@ -1,11 +1,9 @@ """Constants for Navien device communication. Note: CommandCode has been moved to enums.py module. -For backward compatibility, CommandCode is re-exported from this module. +Import from nwp500.enums instead of nwp500.constants. """ -from .enums import CommandCode # noqa: F401 - # Note for maintainers: # Command codes and expected payload fields are defined in # `docs/MQTT_MESSAGES.rst` under the "Control Messages" section and From 5011d52a7b3a878b088e3757984614157dcea377 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 10:17:36 -0800 Subject: [PATCH 14/21] Fix anti_legionella_example import after CommandCode relocation Updated import to use nwp500.enums instead of nwp500.constants --- examples/anti_legionella_example.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/anti_legionella_example.py b/examples/anti_legionella_example.py index 3395793..cd70a9c 100644 --- a/examples/anti_legionella_example.py +++ b/examples/anti_legionella_example.py @@ -17,8 +17,7 @@ from typing import Any from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient -from nwp500.constants import CommandCode -from nwp500.enums import OnOffFlag +from nwp500.enums import CommandCode, OnOffFlag def display_anti_legionella_status(status: dict[str, Any], label: str = "") -> None: From bdbc871c405dc763f62e50a240f56a33e78104ed Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 10:32:08 -0800 Subject: [PATCH 15/21] Fix documentation: replace DeviceControl with CommandCode The enum is named CommandCode, not DeviceControl. - Updated docs/enumerations.rst: DeviceControl -> CommandCode - Updated docs/python_api/models.rst: DeviceControl -> CommandCode - Updated CHANGELOG.rst: All DeviceControl references -> CommandCode This matches what developers actually import and use. --- CHANGELOG.rst | 23 +++++++++++------------ docs/enumerations.rst | 8 ++++---- docs/python_api/models.rst | 2 +- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4ff4a21..cf620ea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,25 +27,24 @@ Added - 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: ``DeviceControl`` (replaces individual constants) + - 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_TEXT``, ``ERROR_CODE_TEXT``) - - Exported from main package: ``from nwp500 import OnOffFlag, ErrorCode, DeviceControl`` + - 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 ``DeviceControl`` enum in ``enums.py`` +- **Command Code Constants**: Migrated from ``constants.py`` to ``CommandCode`` enum in ``enums.py`` - - ``CommandCode.ANTI_LEGIONELLA_ENABLE`` → ``DeviceControl.ANTI_LEGIONELLA_ON`` - - ``CommandCode.ANTI_LEGIONELLA_DISABLE`` → ``DeviceControl.ANTI_LEGIONELLA_OFF`` - - ``CommandCode.TOU_ENABLE`` → ``DeviceControl.TOU_ON`` - - ``CommandCode.TOU_DISABLE`` → ``DeviceControl.TOU_OFF`` - - ``CommandCode.TOU_SETTINGS`` → ``DeviceControl.TOU_RESERVATION`` - - All command constants now use consistent naming in ``DeviceControl`` enum - - Legacy ``CommandCode`` class retained in ``constants.py`` for backward compatibility + - ``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 @@ -69,7 +68,7 @@ Changed - **Examples**: Updated to use new enums for type-safe device control - - ``examples/anti_legionella_example.py``: Uses ``DeviceControl`` enum + - ``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 diff --git a/docs/enumerations.rst b/docs/enumerations.rst index 1511341..fd65e9b 100644 --- a/docs/enumerations.rst +++ b/docs/enumerations.rst @@ -7,7 +7,7 @@ the Navien NWP500 protocol. Device Control Commands ----------------------- -.. autoclass:: nwp500.enums.DeviceControl +.. autoclass:: nwp500.enums.CommandCode :members: :undoc-members: @@ -16,16 +16,16 @@ and trigger actions. The most commonly used commands include: - **Power Control**: ``POWER_ON``, ``POWER_OFF`` - **Temperature**: ``DHW_TEMPERATURE`` -- **Operation Mode**: ``DHW_OPERATION_MODE`` +- **Operation Mode**: ``DHW_MODE`` - **TOU**: ``TOU_ON``, ``TOU_OFF`` - **Maintenance**: ``AIR_FILTER_RESET``, ``ANTI_LEGIONELLA_ON`` Example usage:: - from nwp500 import DeviceControl + from nwp500 import CommandCode # Send temperature command - command = DeviceControl.DHW_TEMPERATURE + command = CommandCode.DHW_TEMPERATURE params = [120] # 60°C in half-degree units Status Value Enumerations diff --git a/docs/python_api/models.rst b/docs/python_api/models.rst index c76e5b7..9432cd0 100644 --- a/docs/python_api/models.rst +++ b/docs/python_api/models.rst @@ -25,7 +25,7 @@ See :doc:`../enumerations` for the complete enumeration reference including: * **CurrentOperationMode** - Real-time operational state (Standby/Heat Pump/Hybrid modes) * **HeatSource** - Currently active heat source * **TemperatureType** - Temperature unit (Celsius/Fahrenheit) -* **DeviceControl** - All control command IDs +* **CommandCode** - All control command IDs * **TouRateType** - Time-of-Use rate periods * And many more protocol enumerations From df04e50e16848654bcbc8e9a4081429851a29337 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 10:35:30 -0800 Subject: [PATCH 16/21] Add cycle detection to _convert_enums_to_names method Prevents potential infinite recursion on circular references: - Added 'visited' parameter to track processed objects - Returns early if a circular reference is detected - Uses set of object ids to identify already-processed items - Fixes potential stack overflow with circular data structures This is defensive programming for safety even though Pydantic models should not produce circular references in normal use. --- src/nwp500/models.py | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/nwp500/models.py b/src/nwp500/models.py index 4358ea5..59b0e0b 100644 --- a/src/nwp500/models.py +++ b/src/nwp500/models.py @@ -142,21 +142,50 @@ def model_dump(self, **kwargs: Any) -> dict[str, Any]: return converted @staticmethod - def _convert_enums_to_names(data: Any) -> Any: - """Recursively convert Enum values to their names.""" + def _convert_enums_to_names( + data: Any, visited: Optional[set[int]] = None + ) -> Any: + """Recursively convert Enum values to their names. + + Args: + data: Data to convert + visited: Set of visited object ids to detect cycles + + 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): - return { - key: NavienBaseModel._convert_enums_to_names(value) + # 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)): - return type(data)( - NavienBaseModel._convert_enums_to_names(item) for item in data - ) + # 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 + ] + result_val: Any = type(data)(converted) + visited.discard(data_id) + return result_val return data From ba14843bd51beb7ad5ab094813de820feb9e2f7e Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 10:37:47 -0800 Subject: [PATCH 17/21] Use asyncio.Event for thread-safe shutdown handling Signal handlers run in different execution contexts and can cause race conditions when modifying shared state. Fixed by: - Replaced self.running = False with asyncio.Event - Signal handler schedules shutdown via asyncio.create_task - Loop conditions check shutdown_event.is_set() instead of self.running - Eliminates race conditions between signal handler and main loop This is a safer pattern for async code with signal handlers. --- examples/mqtt_diagnostics_example.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/examples/mqtt_diagnostics_example.py b/examples/mqtt_diagnostics_example.py index 5f145c2..8b730ba 100755 --- a/examples/mqtt_diagnostics_example.py +++ b/examples/mqtt_diagnostics_example.py @@ -52,14 +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 handle_shutdown(self) -> None: - """Handle shutdown signal.""" + """Handle shutdown signal safely.""" _logger.info("Shutting down gracefully...") - self.running = False + # Schedule the shutdown event to be set (thread-safe) + asyncio.create_task(self._set_shutdown()) + + 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: """ @@ -68,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 @@ -100,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) From c2d92c7f7d4b068508975b50cd797200a1c0e65b Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 10:40:17 -0800 Subject: [PATCH 18/21] Use explicit enum.value in format strings Format strings with IntEnum values should use .value explicitly rather than relying on implicit conversion. This improves: - Clarity of intent (explicitly accessing the value) - Type checking compliance - Reduces error-prone implicit conversions Changed E{error_code:03d} to E{error_code.value:03d} in two places. --- examples/error_code_demo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/error_code_demo.py b/examples/error_code_demo.py index 48c8a47..ff0acdb 100644 --- a/examples/error_code_demo.py +++ b/examples/error_code_demo.py @@ -18,7 +18,7 @@ def diagnose_error(error_code: ErrorCode, sub_code: int = 0) -> None: # Type-safe error code comparison print(f"⚠ Error detected: {error_code.name}") - print(f" Code: E{error_code:03d} (Sub-code: {sub_code:02d})") + 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 @@ -97,7 +97,7 @@ def main(): ] for error_code in temp_sensor_errors: - print(f" • E{error_code:03d} - {ERROR_CODE_TEXT[error_code]}") + 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") From f0a109234872a3366123045e972f86fa131975e8 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 10:54:33 -0800 Subject: [PATCH 19/21] Fix remaining self.running references to use shutdown_event Updated two remaining references to self.running attribute that was removed in favor of asyncio.Event: - Line 253: while condition now uses 'not self.shutdown_event.is_set()' - Line 262: cleanup now calls 'self.shutdown_event.set()' Prevents AttributeError at runtime in the main example loop. --- examples/mqtt_diagnostics_example.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/mqtt_diagnostics_example.py b/examples/mqtt_diagnostics_example.py index 8b730ba..83106b1 100755 --- a/examples/mqtt_diagnostics_example.py +++ b/examples/mqtt_diagnostics_example.py @@ -247,10 +247,12 @@ async def run_example( "Press Ctrl+C to stop early." ) - # Sleep in small intervals to check running flag + # Sleep in small intervals to check shutdown flag elapsed = 0.0 interval = 1.0 - while self.running and elapsed < duration_seconds: + while ( + not self.shutdown_event.is_set() and elapsed < duration_seconds + ): await asyncio.sleep(min(interval, duration_seconds - elapsed)) elapsed += interval @@ -259,7 +261,7 @@ async def run_example( finally: # Cleanup - self.running = False + self.shutdown_event.set() export_task.cancel() monitor_task.cancel() From 6e2c35891cbf427f06bab58c8bec74cb68701fbd Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 10:56:28 -0800 Subject: [PATCH 20/21] Remove unnecessary temporary variable in _convert_enums_to_names Removed result_val temporary variable and return directly from type(data)(converted) for cleaner, more concise code without sacrificing readability. --- src/nwp500/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/nwp500/models.py b/src/nwp500/models.py index 59b0e0b..d6aacca 100644 --- a/src/nwp500/models.py +++ b/src/nwp500/models.py @@ -183,9 +183,8 @@ def _convert_enums_to_names( NavienBaseModel._convert_enums_to_names(item, visited) for item in data ] - result_val: Any = type(data)(converted) visited.discard(data_id) - return result_val + return type(data)(converted) return data From 571c4089f66a600b9e956e59fffb8af2994fbd87 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 17 Dec 2025 10:58:18 -0800 Subject: [PATCH 21/21] Fix RST escape sequence in documentation Changed from incorrect 'CAS\_' with escaped underscore to proper inline literal syntax using backticks: 'CAS_' renders correctly in reStructuredText without escape sequences. --- docs/enumerations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/enumerations.rst b/docs/enumerations.rst index fd65e9b..28ea228 100644 --- a/docs/enumerations.rst +++ b/docs/enumerations.rst @@ -227,7 +227,7 @@ Navien device/unit model types. Common values: - **NCB**: Condensing boiler - **NPN**: Condensing water heater -Values with "CAS\_" prefix indicate cascading systems where multiple units +Values with ``CAS_`` prefix indicate cascading systems where multiple units work together. DeviceType