Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/DEVICE_STATUS_FIELDS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
146 changes: 146 additions & 0 deletions docs/FIRMWARE_TRACKING.rst
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ Documentation
Device Feature Fields <DEVICE_FEATURE_FIELDS>
Error Codes <ERROR_CODES>
MQTT Messages <MQTT_MESSAGES>
Firmware Tracking <FIRMWARE_TRACKING>

.. toctree::
:maxdepth: 2
Expand Down
19 changes: 19 additions & 0 deletions src/nwp500/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
48 changes: 48 additions & 0 deletions src/nwp500/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from enum import Enum
from typing import Any, Optional, Union

from . import constants

_logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -303,6 +305,7 @@ class DeviceStatus:
heUpperOffDiffTempSetting: float
heLowerOnDiffTempSetting: float
heLowerOffDiffTempSetting: float
heatMinOpTemperature: float
drOverrideStatus: int
touOverrideStatus: int
totalEnergyCapacity: float
Expand All @@ -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(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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",
Expand All @@ -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)


Expand Down