From 780e362a7eaf4b174a3d2b036ed28587e1c21f3b Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 15 Oct 2025 09:20:14 -0700 Subject: [PATCH 1/3] Add heatMinOpTemperature field and firmware version tracking system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add heatMinOpTemperature field to DeviceStatus model - Raw value converted using 'raw + 20' formula (70 -> 90°F) - Represents minimum operating temperature for heat pump activation - Implement firmware version tracking system - Add KNOWN_FIRMWARE_FIELD_CHANGES to constants.py for tracking new fields - Enhanced logging to differentiate known vs unknown new fields - Graceful handling of fields from firmware updates - Documentation updates - Add heatMinOpTemperature to DEVICE_STATUS_FIELDS.rst - Create new FIRMWARE_TRACKING.rst guide - Add firmware tracking to documentation index - Improve future maintainability - Users can report firmware versions when new fields appear - Library continues to function with unknown fields - Clear path for adding support for new firmware features This change ensures compatibility with current firmware while preparing for future device updates. --- docs/DEVICE_STATUS_FIELDS.rst | 5 ++ docs/FIRMWARE_TRACKING.rst | 141 ++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + src/nwp500/constants.py | 18 +++++ src/nwp500/models.py | 34 ++++++++ 5 files changed, 199 insertions(+) create mode 100644 docs/FIRMWARE_TRACKING.rst diff --git a/docs/DEVICE_STATUS_FIELDS.rst b/docs/DEVICE_STATUS_FIELDS.rst index 92f9c5a..abe2a00 100644 --- a/docs/DEVICE_STATUS_FIELDS.rst +++ b/docs/DEVICE_STATUS_FIELDS.rst @@ -428,6 +428,11 @@ This document lists the fields found in the ``status`` object of device status m - °F - Heater element lower off differential temperature setting. - ``raw / 10.0`` + * - ``heatMinOpTemperature`` + - float + - °F + - Minimum operating temperature for the heating element. This sets the lower threshold at which the heating element can operate. + - ``raw + 20`` * - ``drOverrideStatus`` - integer - None diff --git a/docs/FIRMWARE_TRACKING.rst b/docs/FIRMWARE_TRACKING.rst new file mode 100644 index 0000000..5aca1ff --- /dev/null +++ b/docs/FIRMWARE_TRACKING.rst @@ -0,0 +1,141 @@ + +Firmware Version Tracking +========================= + +This document tracks firmware versions and the device status fields they introduce or modify. + +Purpose +------- + +The Navien NWP500 water heater receives firmware updates that may introduce new status fields or modify existing behavior. This tracking system helps: + +1. **Graceful Degradation**: The library can handle unknown fields from newer firmware versions without crashing +2. **User Reporting**: Users can report firmware versions when encountering new fields +3. **Library Updates**: Maintainers can prioritize adding support for new fields based on firmware adoption +4. **Documentation**: Track when fields were introduced for better device compatibility documentation + +How It Works +------------ + +When the library encounters unknown fields in device status messages: + +1. It checks if the field is documented in ``constants.KNOWN_FIRMWARE_FIELD_CHANGES`` +2. If the field is known but not implemented, it logs an INFO message +3. If the field is completely unknown, it logs a WARNING message asking users to report their firmware version +4. The unknown field is safely ignored, and the library continues to function + +Known Firmware Field Changes +----------------------------- + +The following table tracks known fields that have been introduced in firmware updates: + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Field Name + - First Observed + - Conversion + - Description + * - ``heatMinOpTemperature`` + - Unknown version + - ``raw + 20`` + - Minimum operating temperature for the heating element. Sets the lower threshold at which the heating element can operate. + +Reporting New Fields +-------------------- + +If you see a warning message about unknown fields, please help us improve the library by reporting: + +1. **The unknown field name(s)** from the warning message +2. **Your device firmware versions**: + + - Controller SW Version (``controllerSwVersion``) + - Panel SW Version (``panelSwVersion``) + - WiFi SW Version (``wifiSwVersion``) + +3. **Sample raw values** for the unknown field (if possible) +4. **Your device model** (e.g., NWP500) + +You can get your firmware versions by running: + +.. code-block:: python + + from nwp500.mqtt_client import NavienMQTTClient + from nwp500.auth import NavienAuthClient + from nwp500.api_client import NavienAPIClient + import asyncio + import os + + async def get_firmware(): + async with NavienAuthClient( + os.getenv("NAVIEN_EMAIL"), + os.getenv("NAVIEN_PASSWORD") + ) as auth: + api = NavienAPIClient(auth) + devices = await api.get_devices() + device = devices[0] + + mqtt = NavienMQTTClient(auth, device.mac_address, device.device_type) + await mqtt.connect() + + def feature_callback(feature): + print(f"Controller SW: {feature.controllerSwVersion}") + print(f"Panel SW: {feature.panelSwVersion}") + print(f"WiFi SW: {feature.wifiSwVersion}") + + await mqtt.request_device_info(feature_callback) + await asyncio.sleep(2) + await mqtt.disconnect() + + asyncio.run(get_firmware()) + +Or using the CLI (if implemented): + +.. code-block:: bash + + nwp-cli --device-info + +Please report issues at: https://github.com/eman/nwp500-python/issues + +Adding New Fields +----------------- + +When adding support for a newly discovered field: + +1. Add the field to ``DeviceStatus`` dataclass in ``models.py`` +2. Add appropriate conversion logic in ``DeviceStatus.from_dict()`` +3. Document the field in ``DEVICE_STATUS_FIELDS.rst`` +4. Update ``constants.KNOWN_FIRMWARE_FIELD_CHANGES`` with field metadata +5. Update this tracking document with firmware version information +6. Remove the field from ``KNOWN_FIRMWARE_FIELD_CHANGES`` once implemented + +Example entry in ``constants.py``: + +.. code-block:: python + + KNOWN_FIRMWARE_FIELD_CHANGES = { + "newFieldName": { + "introduced_in": "controller: 123, panel: 456, wifi: 789", + "description": "What this field represents", + "conversion": "raw + 20", # or "raw / 10.0", "bool (1=OFF, 2=ON)", etc. + }, + } + +Firmware Version History +------------------------ + +This section will be updated as we learn about firmware versions and their changes. + +**Latest Known Versions** (as of last update): + +- Controller SW Version: TBD +- Panel SW Version: TBD +- WiFi SW Version: TBD + +*Note: This tracking system was implemented on 2025-10-15. Historical firmware information is not available.* + +Contributing +------------ + +If you have information about different firmware versions or field changes, please submit a pull request or open an issue. Your contributions help make this library more robust and compatible with different device configurations. diff --git a/docs/index.rst b/docs/index.rst index 8c86397..4cd2411 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -325,6 +325,7 @@ Documentation Device Feature Fields Error Codes MQTT Messages + Firmware Tracking .. toctree:: :maxdepth: 2 diff --git a/src/nwp500/constants.py b/src/nwp500/constants.py index 30a8d8b..be01b84 100644 --- a/src/nwp500/constants.py +++ b/src/nwp500/constants.py @@ -10,3 +10,21 @@ CMD_DHW_MODE = 33554437 CMD_DHW_TEMPERATURE = 33554464 CMD_ENERGY_USAGE_QUERY = 16777225 + +# Known Firmware Versions and Field Changes +# Track firmware versions where new fields were introduced to help with debugging +KNOWN_FIRMWARE_FIELD_CHANGES = { + # Format: "field_name": {"introduced_in": "version", "description": "what it does"} + "heatMinOpTemperature": { + "introduced_in": "unknown", # First observed in production devices + "description": "Minimum operating temperature for heating element", + "conversion": "raw + 20", + }, +} + +# Latest known firmware versions (update as new versions are discovered) +LATEST_KNOWN_FIRMWARE = { + "controllerSwVersion": None, # Update when firmware version is observed + "panelSwVersion": None, + "wifiSwVersion": None, +} diff --git a/src/nwp500/models.py b/src/nwp500/models.py index a0e49b0..7a96e8d 100644 --- a/src/nwp500/models.py +++ b/src/nwp500/models.py @@ -10,6 +10,8 @@ from enum import Enum from typing import Any, Optional, Union +from . import constants + _logger = logging.getLogger(__name__) @@ -303,6 +305,7 @@ class DeviceStatus: heUpperOffDiffTempSetting: float heLowerOnDiffTempSetting: float heLowerOffDiffTempSetting: float + heatMinOpTemperature: float drOverrideStatus: int touOverrideStatus: int totalEnergyCapacity: float @@ -317,6 +320,9 @@ def from_dict(cls, data: dict): # Copy data to avoid modifying the original dictionary converted_data = data.copy() + # Get valid field names for this class + valid_fields = {f.name for f in cls.__dataclass_fields__.values()} + # Handle key typo from documentation/API if "heLowerOnTDiffempSetting" in converted_data: converted_data["heLowerOnDiffTempSetting"] = converted_data.pop( @@ -373,6 +379,7 @@ def from_dict(cls, data: dict): "heUpperOffTempSetting", "heLowerOnTempSetting", "heLowerOffTempSetting", + "heatMinOpTemperature", ] for field_name in add_20_fields: if field_name in converted_data: @@ -457,6 +464,33 @@ def from_dict(cls, data: dict): # Default to FAHRENHEIT for unknown temperature types converted_data["temperatureType"] = TemperatureUnit.FAHRENHEIT + # Filter out any unknown fields not defined in the dataclass + # This handles new fields added by firmware updates gracefully + unknown_fields = set(converted_data.keys()) - valid_fields + if unknown_fields: + # Check if any unknown fields are documented in constants + known_new_fields = unknown_fields & set(constants.KNOWN_FIRMWARE_FIELD_CHANGES.keys()) + truly_unknown = unknown_fields - set(constants.KNOWN_FIRMWARE_FIELD_CHANGES.keys()) + + if known_new_fields: + _logger.info( + "Ignoring known new fields from recent firmware: %s. " + "These fields are documented but not yet implemented in DeviceStatus. " + "Please report this with your firmware version to help us track field changes.", + known_new_fields, + ) + + if truly_unknown: + _logger.warning( + "Discovered new unknown fields from device status: %s. " + "This may indicate a firmware update. Please report this issue with your " + "device firmware version (controllerSwVersion, panelSwVersion, wifiSwVersion) " + "so we can update the library. See constants.KNOWN_FIRMWARE_FIELD_CHANGES.", + truly_unknown, + ) + + converted_data = {k: v for k, v in converted_data.items() if k in valid_fields} + return cls(**converted_data) From 2b486f9d306414f7e127b683b7017d9a42765910 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 15 Oct 2025 09:25:09 -0700 Subject: [PATCH 2/3] Update src/nwp500/models.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/nwp500/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/nwp500/models.py b/src/nwp500/models.py index 7a96e8d..029b297 100644 --- a/src/nwp500/models.py +++ b/src/nwp500/models.py @@ -469,8 +469,9 @@ def from_dict(cls, data: dict): unknown_fields = set(converted_data.keys()) - valid_fields if unknown_fields: # Check if any unknown fields are documented in constants - known_new_fields = unknown_fields & set(constants.KNOWN_FIRMWARE_FIELD_CHANGES.keys()) - truly_unknown = unknown_fields - set(constants.KNOWN_FIRMWARE_FIELD_CHANGES.keys()) + known_firmware_fields = set(constants.KNOWN_FIRMWARE_FIELD_CHANGES.keys()) + known_new_fields = unknown_fields & known_firmware_fields + truly_unknown = unknown_fields - known_firmware_fields if known_new_fields: _logger.info( From 5a1bbcbc2bf5aec0af51b771a67002de9d2a92a7 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Wed, 15 Oct 2025 09:29:48 -0700 Subject: [PATCH 3/3] Update firmware versions with live device data and add DeviceFeature unknown field handling - Query live device to populate LATEST_KNOWN_FIRMWARE with actual versions: - Controller SW: 184614912 - Panel SW: 0 (not used on NWP500) - WiFi SW: 34013184 - Add unknown field filtering to DeviceFeature.from_dict() - Prevents crashes when new feature fields appear (e.g., recirculationUse) - Consistent with DeviceStatus handling - Logs info message about ignored fields - Update FIRMWARE_TRACKING.rst with observed versions and features - Document heatMinOpTemperature was observed with these versions - Note presence of recirc* fields for future implementation All firmware data collected from live NWP500 device on 2025-10-15. --- docs/FIRMWARE_TRACKING.rst | 17 +++++++++++------ src/nwp500/constants.py | 11 ++++++----- src/nwp500/models.py | 13 +++++++++++++ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/docs/FIRMWARE_TRACKING.rst b/docs/FIRMWARE_TRACKING.rst index 5aca1ff..afdf35d 100644 --- a/docs/FIRMWARE_TRACKING.rst +++ b/docs/FIRMWARE_TRACKING.rst @@ -38,7 +38,7 @@ The following table tracks known fields that have been introduced in firmware up - Conversion - Description * - ``heatMinOpTemperature`` - - Unknown version + - Controller: 184614912, WiFi: 34013184 - ``raw + 20`` - Minimum operating temperature for the heating element. Sets the lower threshold at which the heating element can operate. @@ -125,13 +125,18 @@ Example entry in ``constants.py``: Firmware Version History ------------------------ -This section will be updated as we learn about firmware versions and their changes. +This section tracks observed firmware versions and their associated changes. -**Latest Known Versions** (as of last update): +**Latest Known Versions** (as of 2025-10-15): -- Controller SW Version: TBD -- Panel SW Version: TBD -- WiFi SW Version: TBD +- Controller SW Version: 184614912 +- Panel SW Version: 0 (not used on NWP500 devices) +- WiFi SW Version: 34013184 + +**Observed Features:** + +- These versions include support for ``heatMinOpTemperature`` field +- Recirculation pump fields (``recirc*``) are present but not yet documented *Note: This tracking system was implemented on 2025-10-15. Historical firmware information is not available.* diff --git a/src/nwp500/constants.py b/src/nwp500/constants.py index be01b84..9cfdf29 100644 --- a/src/nwp500/constants.py +++ b/src/nwp500/constants.py @@ -16,15 +16,16 @@ KNOWN_FIRMWARE_FIELD_CHANGES = { # Format: "field_name": {"introduced_in": "version", "description": "what it does"} "heatMinOpTemperature": { - "introduced_in": "unknown", # First observed in production devices + "introduced_in": "Controller: 184614912, WiFi: 34013184", "description": "Minimum operating temperature for heating element", "conversion": "raw + 20", }, } -# Latest known firmware versions (update as new versions are discovered) +# Latest known firmware versions (as of 2025-10-15) +# These versions have been observed with heatMinOpTemperature field LATEST_KNOWN_FIRMWARE = { - "controllerSwVersion": None, # Update when firmware version is observed - "panelSwVersion": None, - "wifiSwVersion": None, + "controllerSwVersion": 184614912, # Observed on NWP500 device + "panelSwVersion": 0, # Panel SW version not used on this device + "wifiSwVersion": 34013184, # Observed on NWP500 device } diff --git a/src/nwp500/models.py b/src/nwp500/models.py index 029b297..1913560 100644 --- a/src/nwp500/models.py +++ b/src/nwp500/models.py @@ -553,6 +553,9 @@ def from_dict(cls, data: dict): # Copy data to avoid modifying the original dictionary converted_data = data.copy() + # Get valid field names for this class + valid_fields = {f.name for f in cls.__dataclass_fields__.values()} + # Convert temperature fields with 'raw + 20' formula (same as DeviceStatus) temp_add_20_fields = [ "dhwTemperatureMin", @@ -578,6 +581,16 @@ def from_dict(cls, data: dict): # Default to FAHRENHEIT for unknown temperature types converted_data["temperatureType"] = TemperatureUnit.FAHRENHEIT + # Filter out any unknown fields (similar to DeviceStatus) + unknown_fields = set(converted_data.keys()) - valid_fields + if unknown_fields: + _logger.info( + "Ignoring unknown fields from device feature: %s. " + "This may indicate new device capabilities from a firmware update.", + unknown_fields, + ) + converted_data = {k: v for k, v in converted_data.items() if k in valid_fields} + return cls(**converted_data)