nwp500-python 1.1.3__tar.gz → 1.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. {nwp500_python-1.1.3/src/nwp500_python.egg-info → nwp500_python-1.1.5}/PKG-INFO +1 -1
  2. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/DEVICE_STATUS_FIELDS.rst +6 -1
  3. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/ENERGY_MONITORING.rst +39 -0
  4. nwp500_python-1.1.5/docs/FIRMWARE_TRACKING.rst +146 -0
  5. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/MQTT_CLIENT.rst +214 -4
  6. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/index.rst +1 -0
  7. nwp500_python-1.1.5/src/nwp500/constants.py +31 -0
  8. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/models.py +49 -1
  9. {nwp500_python-1.1.3 → nwp500_python-1.1.5/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  10. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500_python.egg-info/SOURCES.txt +1 -0
  11. nwp500_python-1.1.3/src/nwp500/constants.py +0 -12
  12. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.coveragerc +0 -0
  13. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.github/copilot-instructions.md +0 -0
  14. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.github/workflows/ci.yml +0 -0
  15. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.github/workflows/release.yml +0 -0
  16. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.gitignore +0 -0
  17. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.pre-commit-config.yaml +0 -0
  18. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.readthedocs.yml +0 -0
  19. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/AUTHORS.rst +0 -0
  20. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/CHANGELOG.rst +0 -0
  21. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/CONTRIBUTING.rst +0 -0
  22. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/LICENSE.txt +0 -0
  23. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/Makefile +0 -0
  24. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/README.rst +0 -0
  25. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/RELEASE.md +0 -0
  26. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/API_CLIENT.rst +0 -0
  27. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/API_REFERENCE.rst +0 -0
  28. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/AUTHENTICATION.rst +0 -0
  29. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/COMMAND_QUEUE.rst +0 -0
  30. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/DEVELOPMENT.rst +0 -0
  31. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/DEVICE_FEATURE_FIELDS.rst +0 -0
  32. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/ERROR_CODES.rst +0 -0
  33. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/EVENT_EMITTER.rst +0 -0
  34. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/MQTT_MESSAGES.rst +0 -0
  35. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/Makefile +0 -0
  36. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/_static/.gitignore +0 -0
  37. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/authors.rst +0 -0
  38. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/changelog.rst +0 -0
  39. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/conf.py +0 -0
  40. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/contributing.rst +0 -0
  41. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/license.rst +0 -0
  42. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/openapi.yaml +0 -0
  43. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/readme.rst +0 -0
  44. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/requirements.txt +0 -0
  45. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/.ruff.toml +0 -0
  46. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/README.md +0 -0
  47. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/api_client_example.py +0 -0
  48. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/auth_constructor_example.py +0 -0
  49. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/authenticate.py +0 -0
  50. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/combined_callbacks.py +0 -0
  51. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/command_queue_demo.py +0 -0
  52. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/device_feature_callback.py +0 -0
  53. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/device_status_callback.py +0 -0
  54. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/device_status_callback_debug.py +0 -0
  55. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/energy_usage_example.py +0 -0
  56. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/event_emitter_demo.py +0 -0
  57. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/improved_auth_pattern.py +0 -0
  58. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/mask.py +0 -0
  59. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/mqtt_client_example.py +0 -0
  60. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/periodic_device_info.py +0 -0
  61. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/periodic_requests.py +0 -0
  62. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/power_control_example.py +0 -0
  63. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/reconnection_demo.py +0 -0
  64. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/set_dhw_temperature_example.py +0 -0
  65. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/set_mode_example.py +0 -0
  66. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/simple_periodic_info.py +0 -0
  67. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/simple_periodic_status.py +0 -0
  68. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/test_api_client.py +0 -0
  69. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/test_mqtt_connection.py +0 -0
  70. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/test_mqtt_messaging.py +0 -0
  71. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/test_periodic_minimal.py +0 -0
  72. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/pyproject.toml +0 -0
  73. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/scripts/format.py +0 -0
  74. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/scripts/lint.py +0 -0
  75. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/scripts/setup-dev.py +0 -0
  76. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/setup.cfg +0 -0
  77. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/setup.py +0 -0
  78. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/__init__.py +0 -0
  79. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/api_client.py +0 -0
  80. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/auth.py +0 -0
  81. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/cli.py +0 -0
  82. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/config.py +0 -0
  83. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/events.py +0 -0
  84. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/mqtt_client.py +0 -0
  85. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  86. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  87. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  88. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500_python.egg-info/requires.txt +0 -0
  89. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500_python.egg-info/top_level.txt +0 -0
  90. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/tests/conftest.py +0 -0
  91. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/tests/test_command_queue.py +0 -0
  92. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/tests/test_events.py +0 -0
  93. {nwp500_python-1.1.3 → nwp500_python-1.1.5}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 1.1.3
3
+ Version: 1.1.5
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: Emmanuel Levijarvi
@@ -147,7 +147,7 @@ This document lists the fields found in the ``status`` object of device status m
147
147
  - integer
148
148
  - °F
149
149
  - Target superheat value - the desired temperature difference ensuring complete refrigerant vaporization.
150
- - ``raw / 10.0``
150
+ - ``(raw / 10) * 9/5 + 32`` (decicelsius to Fahrenheit)
151
151
  * - ``compUse``
152
152
  - bool
153
153
  - None
@@ -428,6 +428,11 @@ This document lists the fields found in the ``status`` object of device status m
428
428
  - °F
429
429
  - Heater element lower off differential temperature setting.
430
430
  - ``raw / 10.0``
431
+ * - ``heatMinOpTemperature``
432
+ - float
433
+ - °F
434
+ - Minimum operating temperature for the heating element. This sets the lower threshold at which the heating element can operate.
435
+ - ``raw + 20``
431
436
  * - ``drOverrideStatus``
432
437
  - integer
433
438
  - None
@@ -79,6 +79,45 @@ Total upper electric heater runtime in minutes -
79
79
  ``heater2RunningMinuteTotal`` (int): Total lower electric heater runtime
80
80
  in minutes
81
81
 
82
+ Historical Energy Usage
83
+ -----------------------
84
+
85
+ Request detailed daily energy usage data for specific months:
86
+
87
+ .. code:: python
88
+
89
+ from nwp500 import NavienMqttClient, EnergyUsageResponse
90
+
91
+ def on_energy_usage(energy: EnergyUsageResponse):
92
+ print(f"Total Usage: {energy.total.total_usage} Wh")
93
+ print(f"Heat Pump: {energy.total.heat_pump_percentage:.1f}%")
94
+ print(f"Electric: {energy.total.heat_element_percentage:.1f}%")
95
+
96
+ # Daily breakdown
97
+ for day in energy.daily:
98
+ print(f"Day {day.day}: {day.total_usage} Wh")
99
+
100
+ # Subscribe to energy usage responses
101
+ await mqtt_client.subscribe_energy_usage(device, on_energy_usage)
102
+
103
+ # Request energy usage for September 2025
104
+ await mqtt_client.request_energy_usage(device, year=2025, months=[9])
105
+
106
+ # Request multiple months
107
+ await mqtt_client.request_energy_usage(device, year=2025, months=[7, 8, 9])
108
+
109
+ **Key Methods:**
110
+
111
+ - ``request_energy_usage(device, year, months)``: Request historical data
112
+ - ``subscribe_energy_usage(device, callback)``: Subscribe to energy usage responses
113
+
114
+ **Response Fields:**
115
+
116
+ - ``total.total_usage`` (int): Total energy consumption in Wh
117
+ - ``total.heat_pump_percentage`` (float): Percentage from heat pump
118
+ - ``total.heat_element_percentage`` (float): Percentage from electric heaters
119
+ - ``daily`` (list): Daily breakdown of usage per day
120
+
82
121
  Energy Capacity
83
122
  ---------------
84
123
 
@@ -0,0 +1,146 @@
1
+
2
+ Firmware Version Tracking
3
+ =========================
4
+
5
+ This document tracks firmware versions and the device status fields they introduce or modify.
6
+
7
+ Purpose
8
+ -------
9
+
10
+ The Navien NWP500 water heater receives firmware updates that may introduce new status fields or modify existing behavior. This tracking system helps:
11
+
12
+ 1. **Graceful Degradation**: The library can handle unknown fields from newer firmware versions without crashing
13
+ 2. **User Reporting**: Users can report firmware versions when encountering new fields
14
+ 3. **Library Updates**: Maintainers can prioritize adding support for new fields based on firmware adoption
15
+ 4. **Documentation**: Track when fields were introduced for better device compatibility documentation
16
+
17
+ How It Works
18
+ ------------
19
+
20
+ When the library encounters unknown fields in device status messages:
21
+
22
+ 1. It checks if the field is documented in ``constants.KNOWN_FIRMWARE_FIELD_CHANGES``
23
+ 2. If the field is known but not implemented, it logs an INFO message
24
+ 3. If the field is completely unknown, it logs a WARNING message asking users to report their firmware version
25
+ 4. The unknown field is safely ignored, and the library continues to function
26
+
27
+ Known Firmware Field Changes
28
+ -----------------------------
29
+
30
+ The following table tracks known fields that have been introduced in firmware updates:
31
+
32
+ .. list-table::
33
+ :header-rows: 1
34
+ :widths: 20 15 15 50
35
+
36
+ * - Field Name
37
+ - First Observed
38
+ - Conversion
39
+ - Description
40
+ * - ``heatMinOpTemperature``
41
+ - Controller: 184614912, WiFi: 34013184
42
+ - ``raw + 20``
43
+ - Minimum operating temperature for the heating element. Sets the lower threshold at which the heating element can operate.
44
+
45
+ Reporting New Fields
46
+ --------------------
47
+
48
+ If you see a warning message about unknown fields, please help us improve the library by reporting:
49
+
50
+ 1. **The unknown field name(s)** from the warning message
51
+ 2. **Your device firmware versions**:
52
+
53
+ - Controller SW Version (``controllerSwVersion``)
54
+ - Panel SW Version (``panelSwVersion``)
55
+ - WiFi SW Version (``wifiSwVersion``)
56
+
57
+ 3. **Sample raw values** for the unknown field (if possible)
58
+ 4. **Your device model** (e.g., NWP500)
59
+
60
+ You can get your firmware versions by running:
61
+
62
+ .. code-block:: python
63
+
64
+ from nwp500.mqtt_client import NavienMQTTClient
65
+ from nwp500.auth import NavienAuthClient
66
+ from nwp500.api_client import NavienAPIClient
67
+ import asyncio
68
+ import os
69
+
70
+ async def get_firmware():
71
+ async with NavienAuthClient(
72
+ os.getenv("NAVIEN_EMAIL"),
73
+ os.getenv("NAVIEN_PASSWORD")
74
+ ) as auth:
75
+ api = NavienAPIClient(auth)
76
+ devices = await api.get_devices()
77
+ device = devices[0]
78
+
79
+ mqtt = NavienMQTTClient(auth, device.mac_address, device.device_type)
80
+ await mqtt.connect()
81
+
82
+ def feature_callback(feature):
83
+ print(f"Controller SW: {feature.controllerSwVersion}")
84
+ print(f"Panel SW: {feature.panelSwVersion}")
85
+ print(f"WiFi SW: {feature.wifiSwVersion}")
86
+
87
+ await mqtt.request_device_info(feature_callback)
88
+ await asyncio.sleep(2)
89
+ await mqtt.disconnect()
90
+
91
+ asyncio.run(get_firmware())
92
+
93
+ Or using the CLI (if implemented):
94
+
95
+ .. code-block:: bash
96
+
97
+ nwp-cli --device-info
98
+
99
+ Please report issues at: https://github.com/eman/nwp500-python/issues
100
+
101
+ Adding New Fields
102
+ -----------------
103
+
104
+ When adding support for a newly discovered field:
105
+
106
+ 1. Add the field to ``DeviceStatus`` dataclass in ``models.py``
107
+ 2. Add appropriate conversion logic in ``DeviceStatus.from_dict()``
108
+ 3. Document the field in ``DEVICE_STATUS_FIELDS.rst``
109
+ 4. Update ``constants.KNOWN_FIRMWARE_FIELD_CHANGES`` with field metadata
110
+ 5. Update this tracking document with firmware version information
111
+ 6. Remove the field from ``KNOWN_FIRMWARE_FIELD_CHANGES`` once implemented
112
+
113
+ Example entry in ``constants.py``:
114
+
115
+ .. code-block:: python
116
+
117
+ KNOWN_FIRMWARE_FIELD_CHANGES = {
118
+ "newFieldName": {
119
+ "introduced_in": "controller: 123, panel: 456, wifi: 789",
120
+ "description": "What this field represents",
121
+ "conversion": "raw + 20", # or "raw / 10.0", "bool (1=OFF, 2=ON)", etc.
122
+ },
123
+ }
124
+
125
+ Firmware Version History
126
+ ------------------------
127
+
128
+ This section tracks observed firmware versions and their associated changes.
129
+
130
+ **Latest Known Versions** (as of 2025-10-15):
131
+
132
+ - Controller SW Version: 184614912
133
+ - Panel SW Version: 0 (not used on NWP500 devices)
134
+ - WiFi SW Version: 34013184
135
+
136
+ **Observed Features:**
137
+
138
+ - These versions include support for ``heatMinOpTemperature`` field
139
+ - Recirculation pump fields (``recirc*``) are present but not yet documented
140
+
141
+ *Note: This tracking system was implemented on 2025-10-15. Historical firmware information is not available.*
142
+
143
+ Contributing
144
+ ------------
145
+
146
+ 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.
@@ -396,6 +396,41 @@ Publish a message to an MQTT topic.
396
396
  Device Command Methods
397
397
  ^^^^^^^^^^^^^^^^^^^^^^
398
398
 
399
+ Complete MQTT API Reference
400
+ ''''''''''''''''''''''''''''
401
+
402
+ This section provides a comprehensive reference of all available MQTT client methods for requesting data and controlling devices.
403
+
404
+ **Request Methods & Corresponding Subscriptions**
405
+
406
+ +------------------------------------+---------------------------------------+----------------------------------------+
407
+ | Request Method | Subscribe Method | Response Type |
408
+ +====================================+=======================================+========================================+
409
+ | ``request_device_status()`` | ``subscribe_device_status()`` | ``DeviceStatus`` object |
410
+ +------------------------------------+---------------------------------------+----------------------------------------+
411
+ | ``request_device_info()`` | ``subscribe_device_feature()`` | ``DeviceFeature`` object |
412
+ +------------------------------------+---------------------------------------+----------------------------------------+
413
+ | ``request_energy_usage()`` | ``subscribe_energy_usage()`` | ``EnergyUsageResponse`` object |
414
+ +------------------------------------+---------------------------------------+----------------------------------------+
415
+ | ``set_power()`` | ``subscribe_device_status()`` | Updated ``DeviceStatus`` |
416
+ +------------------------------------+---------------------------------------+----------------------------------------+
417
+ | ``set_dhw_mode()`` | ``subscribe_device_status()`` | Updated ``DeviceStatus`` |
418
+ +------------------------------------+---------------------------------------+----------------------------------------+
419
+ | ``set_dhw_temperature()`` | ``subscribe_device_status()`` | Updated ``DeviceStatus`` |
420
+ +------------------------------------+---------------------------------------+----------------------------------------+
421
+ | ``set_dhw_temperature_display()`` | ``subscribe_device_status()`` | Updated ``DeviceStatus`` |
422
+ +------------------------------------+---------------------------------------+----------------------------------------+
423
+
424
+ **Generic Subscriptions**
425
+
426
+ +------------------------------------+---------------------------------------+----------------------------------------+
427
+ | Method | Purpose | Response Type |
428
+ +====================================+=======================================+========================================+
429
+ | ``subscribe_device()`` | Subscribe to all device messages | Raw ``dict`` (all message types) |
430
+ +------------------------------------+---------------------------------------+----------------------------------------+
431
+ | ``subscribe()`` | Subscribe to any MQTT topic | Raw ``dict`` |
432
+ +------------------------------------+---------------------------------------+----------------------------------------+
433
+
399
434
  request_device_status()
400
435
  '''''''''''''''''''''''
401
436
 
@@ -403,12 +438,26 @@ request_device_status()
403
438
 
404
439
  await mqtt_client.request_device_status(device: Device) -> int
405
440
 
406
- Request current device status.
441
+ Request current device status including temperatures, operation mode, power consumption, and error codes.
407
442
 
408
443
  **Command:** ``16777219``
409
444
 
410
445
  **Topic:** ``cmd/{device_type}/navilink-{device_id}/st``
411
446
 
447
+ **Response:** Subscribe with ``subscribe_device_status()`` to receive ``DeviceStatus`` objects
448
+
449
+ **Example:**
450
+
451
+ .. code:: python
452
+
453
+ def on_status(status: DeviceStatus):
454
+ print(f"Water Temp: {status.dhwTemperature}°F")
455
+ print(f"Mode: {status.operationMode}")
456
+ print(f"Power: {status.currentInstPower}W")
457
+
458
+ await mqtt_client.subscribe_device_status(device, on_status)
459
+ await mqtt_client.request_device_status(device)
460
+
412
461
  request_device_info()
413
462
  '''''''''''''''''''''
414
463
 
@@ -416,12 +465,59 @@ request_device_info()
416
465
 
417
466
  await mqtt_client.request_device_info(device: Device) -> int
418
467
 
419
- Request device information.
468
+ Request device information including firmware version, serial number, temperature limits, and capabilities.
420
469
 
421
470
  **Command:** ``16777217``
422
471
 
423
472
  **Topic:** ``cmd/{device_type}/navilink-{device_id}/st/did``
424
473
 
474
+ **Response:** Subscribe with ``subscribe_device_feature()`` to receive ``DeviceFeature`` objects
475
+
476
+ **Example:**
477
+
478
+ .. code:: python
479
+
480
+ def on_feature(feature: DeviceFeature):
481
+ print(f"Firmware: {feature.controllerSwVersion}")
482
+ print(f"Serial: {feature.controllerSerialNumber}")
483
+ print(f"Temp Range: {feature.dhwTemperatureMin}-{feature.dhwTemperatureMax}°F")
484
+
485
+ await mqtt_client.subscribe_device_feature(device, on_feature)
486
+ await mqtt_client.request_device_info(device)
487
+
488
+ request_energy_usage()
489
+ ''''''''''''''''''''''
490
+
491
+ .. code:: python
492
+
493
+ await mqtt_client.request_energy_usage(device: Device, year: int, months: list[int]) -> int
494
+
495
+ Request historical daily energy usage data for specified month(s). Returns heat pump and electric heating element consumption with daily breakdown.
496
+
497
+ **Command:** ``16777225``
498
+
499
+ **Topic:** ``cmd/{device_type}/navilink-{device_id}/st/energy-usage-daily-query/rd``
500
+
501
+ **Response:** Subscribe with ``subscribe_energy_usage()`` to receive ``EnergyUsageResponse`` objects
502
+
503
+ **Parameters:**
504
+
505
+ - ``year``: Year to query (e.g., 2025)
506
+ - ``months``: List of months to query (1-12). Can request multiple months.
507
+
508
+ **Example:**
509
+
510
+ .. code:: python
511
+
512
+ def on_energy(energy: EnergyUsageResponse):
513
+ print(f"Total Usage: {energy.total.total_usage} Wh")
514
+ print(f"Heat Pump: {energy.total.heat_pump_percentage:.1f}%")
515
+ for day in energy.daily:
516
+ print(f"Day {day.day}: {day.total_usage} Wh")
517
+
518
+ await mqtt_client.subscribe_energy_usage(device, on_energy)
519
+ await mqtt_client.request_energy_usage(device, year=2025, months=[9])
520
+
425
521
  set_power()
426
522
  '''''''''''
427
523
 
@@ -435,6 +531,8 @@ Turn device on or off.
435
531
 
436
532
  **Mode:** ``power-on`` or ``power-off``
437
533
 
534
+ **Response:** Device status is updated; subscribe with ``subscribe_device_status()`` to see changes
535
+
438
536
  set_dhw_mode()
439
537
  ''''''''''''''
440
538
 
@@ -456,6 +554,8 @@ Set DHW (Domestic Hot Water) operation mode. This sets the ``dhwOperationSetting
456
554
  * ``4``: High Demand (faster recovery - Hybrid: Boost)
457
555
  * ``5``: Vacation (suspend heating for 0-99 days)
458
556
 
557
+ **Response:** Device status is updated; subscribe with ``subscribe_device_status()`` to see changes
558
+
459
559
  **Important:** Setting the mode updates ``dhwOperationSetting`` but does not immediately change ``operationMode``. The ``operationMode`` field reflects the device's current operational state and changes automatically when the device starts/stops heating. See :doc:`DEVICE_STATUS_FIELDS` for details on the relationship between these fields.
460
560
 
461
561
  set_dhw_temperature()
@@ -465,13 +565,44 @@ set_dhw_temperature()
465
565
 
466
566
  await mqtt_client.set_dhw_temperature(device: Device, temperature: int) -> int
467
567
 
468
- Set DHW target temperature.
568
+ Set DHW target temperature using the **MESSAGE value** (20°F lower than display).
469
569
 
470
570
  **Command:** ``33554433``
471
571
 
472
572
  **Mode:** ``dhw-temperature``
473
573
 
474
- **Parameters:** - ``temperature``: Target temperature in Fahrenheit
574
+ **Parameters:**
575
+
576
+ - ``temperature``: Target temperature in Fahrenheit (message value, not display value)
577
+
578
+ **Response:** Device status is updated; subscribe with ``subscribe_device_status()`` to see changes
579
+
580
+ **Important:** The temperature in the message is 20°F lower than what displays on the device/app:
581
+
582
+ - Message value 120°F → Display shows 140°F
583
+ - Message value 130°F → Display shows 150°F
584
+
585
+ set_dhw_temperature_display()
586
+ ''''''''''''''''''''''''''''''
587
+
588
+ .. code:: python
589
+
590
+ await mqtt_client.set_dhw_temperature_display(device: Device, display_temperature: int) -> int
591
+
592
+ Set DHW target temperature using the **DISPLAY value** (what you see on device/app). This is a convenience method that automatically converts display temperature to message value.
593
+
594
+ **Parameters:**
595
+
596
+ - ``display_temperature``: Target temperature as shown on display/app (Fahrenheit)
597
+
598
+ **Response:** Device status is updated; subscribe with ``subscribe_device_status()`` to see changes
599
+
600
+ **Example:**
601
+
602
+ .. code:: python
603
+
604
+ # Set display temperature to 140°F (sends 120°F in message)
605
+ await mqtt_client.set_dhw_temperature_display(device, 140)
475
606
 
476
607
  signal_app_connection()
477
608
  '''''''''''''''''''''''
@@ -484,6 +615,85 @@ Signal that the app has connected.
484
615
 
485
616
  **Topic:** ``evt/{device_type}/navilink-{device_id}/app-connection``
486
617
 
618
+ Subscription Methods
619
+ ''''''''''''''''''''
620
+
621
+ subscribe_device_status()
622
+ .........................
623
+
624
+ .. code:: python
625
+
626
+ await mqtt_client.subscribe_device_status(
627
+ device: Device,
628
+ callback: Callable[[DeviceStatus], None]
629
+ ) -> int
630
+
631
+ Subscribe to device status messages with automatic parsing into ``DeviceStatus`` objects. Use this after calling ``request_device_status()`` or any control commands to receive updates.
632
+
633
+ **Emits Events:**
634
+
635
+ - ``status_received``: Every status update (DeviceStatus)
636
+ - ``temperature_changed``: Temperature changed (old_temp, new_temp)
637
+ - ``mode_changed``: Operation mode changed (old_mode, new_mode)
638
+ - ``power_changed``: Power consumption changed (old_power, new_power)
639
+ - ``heating_started``: Device started heating (status)
640
+ - ``heating_stopped``: Device stopped heating (status)
641
+ - ``error_detected``: Error code detected (error_code, status)
642
+ - ``error_cleared``: Error code cleared (error_code)
643
+
644
+ subscribe_device_feature()
645
+ ..........................
646
+
647
+ .. code:: python
648
+
649
+ await mqtt_client.subscribe_device_feature(
650
+ device: Device,
651
+ callback: Callable[[DeviceFeature], None]
652
+ ) -> int
653
+
654
+ Subscribe to device feature/info messages with automatic parsing into ``DeviceFeature`` objects. Use this after calling ``request_device_info()`` to receive device capabilities and firmware info.
655
+
656
+ **Emits Events:**
657
+
658
+ - ``feature_received``: Feature/info received (DeviceFeature)
659
+
660
+ subscribe_energy_usage()
661
+ ........................
662
+
663
+ .. code:: python
664
+
665
+ await mqtt_client.subscribe_energy_usage(
666
+ device: Device,
667
+ callback: Callable[[EnergyUsageResponse], None]
668
+ ) -> int
669
+
670
+ Subscribe to energy usage query responses with automatic parsing into ``EnergyUsageResponse`` objects. Use this after calling ``request_energy_usage()`` to receive historical energy data.
671
+
672
+ subscribe_device()
673
+ ..................
674
+
675
+ .. code:: python
676
+
677
+ await mqtt_client.subscribe_device(
678
+ device: Device,
679
+ callback: Callable[[str, dict], None]
680
+ ) -> int
681
+
682
+ Subscribe to all messages from a device (no parsing). Receives all message types as raw dictionaries. Use the specific subscription methods above for automatic parsing.
683
+
684
+ subscribe()
685
+ ...........
686
+
687
+ .. code:: python
688
+
689
+ await mqtt_client.subscribe(
690
+ topic: str,
691
+ callback: Callable[[str, dict], None],
692
+ qos: mqtt.QoS = mqtt.QoS.AT_LEAST_ONCE
693
+ ) -> int
694
+
695
+ Subscribe to any MQTT topic. Supports wildcards (``#``, ``+``). Receives raw dictionary messages.
696
+
487
697
  Periodic Request Methods (Optional)
488
698
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
489
699
 
@@ -325,6 +325,7 @@ Documentation
325
325
  Device Feature Fields <DEVICE_FEATURE_FIELDS>
326
326
  Error Codes <ERROR_CODES>
327
327
  MQTT Messages <MQTT_MESSAGES>
328
+ Firmware Tracking <FIRMWARE_TRACKING>
328
329
 
329
330
  .. toctree::
330
331
  :maxdepth: 2
@@ -0,0 +1,31 @@
1
+ """
2
+ This module defines constants for the Navien API.
3
+ """
4
+
5
+ # MQTT Command Codes
6
+ CMD_STATUS_REQUEST = 16777219
7
+ CMD_DEVICE_INFO_REQUEST = 16777217
8
+ CMD_POWER_ON = 33554434
9
+ CMD_POWER_OFF = 33554433
10
+ CMD_DHW_MODE = 33554437
11
+ CMD_DHW_TEMPERATURE = 33554464
12
+ CMD_ENERGY_USAGE_QUERY = 16777225
13
+
14
+ # Known Firmware Versions and Field Changes
15
+ # Track firmware versions where new fields were introduced to help with debugging
16
+ KNOWN_FIRMWARE_FIELD_CHANGES = {
17
+ # Format: "field_name": {"introduced_in": "version", "description": "what it does"}
18
+ "heatMinOpTemperature": {
19
+ "introduced_in": "Controller: 184614912, WiFi: 34013184",
20
+ "description": "Minimum operating temperature for heating element",
21
+ "conversion": "raw + 20",
22
+ },
23
+ }
24
+
25
+ # Latest known firmware versions (as of 2025-10-15)
26
+ # These versions have been observed with heatMinOpTemperature field
27
+ LATEST_KNOWN_FIRMWARE = {
28
+ "controllerSwVersion": 184614912, # Observed on NWP500 device
29
+ "panelSwVersion": 0, # Panel SW version not used on this device
30
+ "wifiSwVersion": 34013184, # Observed on NWP500 device
31
+ }
@@ -10,6 +10,8 @@ from dataclasses import dataclass, field
10
10
  from enum import Enum
11
11
  from typing import Any, Optional, Union
12
12
 
13
+ from . import constants
14
+
13
15
  _logger = logging.getLogger(__name__)
14
16
 
15
17
 
@@ -303,6 +305,7 @@ class DeviceStatus:
303
305
  heUpperOffDiffTempSetting: float
304
306
  heLowerOnDiffTempSetting: float
305
307
  heLowerOffDiffTempSetting: float
308
+ heatMinOpTemperature: float
306
309
  drOverrideStatus: int
307
310
  touOverrideStatus: int
308
311
  totalEnergyCapacity: float
@@ -317,6 +320,9 @@ class DeviceStatus:
317
320
  # Copy data to avoid modifying the original dictionary
318
321
  converted_data = data.copy()
319
322
 
323
+ # Get valid field names for this class
324
+ valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
325
+
320
326
  # Handle key typo from documentation/API
321
327
  if "heLowerOnTDiffempSetting" in converted_data:
322
328
  converted_data["heLowerOnDiffTempSetting"] = converted_data.pop(
@@ -373,6 +379,7 @@ class DeviceStatus:
373
379
  "heUpperOffTempSetting",
374
380
  "heLowerOnTempSetting",
375
381
  "heLowerOffTempSetting",
382
+ "heatMinOpTemperature",
376
383
  ]
377
384
  for field_name in add_20_fields:
378
385
  if field_name in converted_data:
@@ -380,7 +387,6 @@ class DeviceStatus:
380
387
 
381
388
  # Convert fields with 'raw / 10.0' formula (non-temperature fields)
382
389
  div_10_fields = [
383
- "targetSuperHeat",
384
390
  "currentInletTemperature",
385
391
  "currentDhwFlowRate",
386
392
  "hpUpperOnDiffTempSetting",
@@ -414,6 +420,7 @@ class DeviceStatus:
414
420
  "evaporatorTemperature",
415
421
  "ambientTemperature",
416
422
  "currentSuperHeat",
423
+ "targetSuperHeat",
417
424
  ]
418
425
  for field_name in heat_pump_temp_fields:
419
426
  if field_name in converted_data:
@@ -457,6 +464,34 @@ class DeviceStatus:
457
464
  # Default to FAHRENHEIT for unknown temperature types
458
465
  converted_data["temperatureType"] = TemperatureUnit.FAHRENHEIT
459
466
 
467
+ # Filter out any unknown fields not defined in the dataclass
468
+ # This handles new fields added by firmware updates gracefully
469
+ unknown_fields = set(converted_data.keys()) - valid_fields
470
+ if unknown_fields:
471
+ # Check if any unknown fields are documented in constants
472
+ known_firmware_fields = set(constants.KNOWN_FIRMWARE_FIELD_CHANGES.keys())
473
+ known_new_fields = unknown_fields & known_firmware_fields
474
+ truly_unknown = unknown_fields - known_firmware_fields
475
+
476
+ if known_new_fields:
477
+ _logger.info(
478
+ "Ignoring known new fields from recent firmware: %s. "
479
+ "These fields are documented but not yet implemented in DeviceStatus. "
480
+ "Please report this with your firmware version to help us track field changes.",
481
+ known_new_fields,
482
+ )
483
+
484
+ if truly_unknown:
485
+ _logger.warning(
486
+ "Discovered new unknown fields from device status: %s. "
487
+ "This may indicate a firmware update. Please report this issue with your "
488
+ "device firmware version (controllerSwVersion, panelSwVersion, wifiSwVersion) "
489
+ "so we can update the library. See constants.KNOWN_FIRMWARE_FIELD_CHANGES.",
490
+ truly_unknown,
491
+ )
492
+
493
+ converted_data = {k: v for k, v in converted_data.items() if k in valid_fields}
494
+
460
495
  return cls(**converted_data)
461
496
 
462
497
 
@@ -518,6 +553,9 @@ class DeviceFeature:
518
553
  # Copy data to avoid modifying the original dictionary
519
554
  converted_data = data.copy()
520
555
 
556
+ # Get valid field names for this class
557
+ valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
558
+
521
559
  # Convert temperature fields with 'raw + 20' formula (same as DeviceStatus)
522
560
  temp_add_20_fields = [
523
561
  "dhwTemperatureMin",
@@ -543,6 +581,16 @@ class DeviceFeature:
543
581
  # Default to FAHRENHEIT for unknown temperature types
544
582
  converted_data["temperatureType"] = TemperatureUnit.FAHRENHEIT
545
583
 
584
+ # Filter out any unknown fields (similar to DeviceStatus)
585
+ unknown_fields = set(converted_data.keys()) - valid_fields
586
+ if unknown_fields:
587
+ _logger.info(
588
+ "Ignoring unknown fields from device feature: %s. "
589
+ "This may indicate new device capabilities from a firmware update.",
590
+ unknown_fields,
591
+ )
592
+ converted_data = {k: v for k, v in converted_data.items() if k in valid_fields}
593
+
546
594
  return cls(**converted_data)
547
595
 
548
596
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 1.1.3
3
+ Version: 1.1.5
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: Emmanuel Levijarvi
@@ -26,6 +26,7 @@ docs/DEVICE_STATUS_FIELDS.rst
26
26
  docs/ENERGY_MONITORING.rst
27
27
  docs/ERROR_CODES.rst
28
28
  docs/EVENT_EMITTER.rst
29
+ docs/FIRMWARE_TRACKING.rst
29
30
  docs/MQTT_CLIENT.rst
30
31
  docs/MQTT_MESSAGES.rst
31
32
  docs/Makefile
@@ -1,12 +0,0 @@
1
- """
2
- This module defines constants for the Navien API.
3
- """
4
-
5
- # MQTT Command Codes
6
- CMD_STATUS_REQUEST = 16777219
7
- CMD_DEVICE_INFO_REQUEST = 16777217
8
- CMD_POWER_ON = 33554434
9
- CMD_POWER_OFF = 33554433
10
- CMD_DHW_MODE = 33554437
11
- CMD_DHW_TEMPERATURE = 33554464
12
- CMD_ENERGY_USAGE_QUERY = 16777225
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes