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..afdf35d --- /dev/null +++ b/docs/FIRMWARE_TRACKING.rst @@ -0,0 +1,146 @@ + +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`` + - Controller: 184614912, WiFi: 34013184 + - ``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 tracks observed firmware versions and their associated changes. + +**Latest Known Versions** (as of 2025-10-15): + +- 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.* + +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..9cfdf29 100644 --- a/src/nwp500/constants.py +++ b/src/nwp500/constants.py @@ -10,3 +10,22 @@ 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": "Controller: 184614912, WiFi: 34013184", + "description": "Minimum operating temperature for heating element", + "conversion": "raw + 20", + }, +} + +# Latest known firmware versions (as of 2025-10-15) +# These versions have been observed with heatMinOpTemperature field +LATEST_KNOWN_FIRMWARE = { + "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 a0e49b0..1913560 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,34 @@ 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_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( + "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) @@ -518,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", @@ -543,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)