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.
- {nwp500_python-1.1.3/src/nwp500_python.egg-info → nwp500_python-1.1.5}/PKG-INFO +1 -1
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/DEVICE_STATUS_FIELDS.rst +6 -1
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/ENERGY_MONITORING.rst +39 -0
- nwp500_python-1.1.5/docs/FIRMWARE_TRACKING.rst +146 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/MQTT_CLIENT.rst +214 -4
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/index.rst +1 -0
- nwp500_python-1.1.5/src/nwp500/constants.py +31 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/models.py +49 -1
- {nwp500_python-1.1.3 → nwp500_python-1.1.5/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500_python.egg-info/SOURCES.txt +1 -0
- nwp500_python-1.1.3/src/nwp500/constants.py +0 -12
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.coveragerc +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.github/copilot-instructions.md +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.github/workflows/ci.yml +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.github/workflows/release.yml +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.gitignore +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.pre-commit-config.yaml +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/.readthedocs.yml +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/AUTHORS.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/CHANGELOG.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/CONTRIBUTING.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/LICENSE.txt +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/Makefile +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/README.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/RELEASE.md +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/API_CLIENT.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/API_REFERENCE.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/AUTHENTICATION.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/COMMAND_QUEUE.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/DEVELOPMENT.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/DEVICE_FEATURE_FIELDS.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/ERROR_CODES.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/EVENT_EMITTER.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/MQTT_MESSAGES.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/Makefile +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/_static/.gitignore +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/authors.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/changelog.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/conf.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/contributing.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/license.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/openapi.yaml +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/readme.rst +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/docs/requirements.txt +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/.ruff.toml +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/README.md +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/api_client_example.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/auth_constructor_example.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/authenticate.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/combined_callbacks.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/command_queue_demo.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/device_feature_callback.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/device_status_callback.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/device_status_callback_debug.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/energy_usage_example.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/event_emitter_demo.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/improved_auth_pattern.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/mask.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/mqtt_client_example.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/periodic_device_info.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/periodic_requests.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/power_control_example.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/reconnection_demo.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/set_dhw_temperature_example.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/set_mode_example.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/simple_periodic_info.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/simple_periodic_status.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/test_api_client.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/test_mqtt_connection.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/test_mqtt_messaging.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/examples/test_periodic_minimal.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/pyproject.toml +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/scripts/format.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/scripts/lint.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/scripts/setup-dev.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/setup.cfg +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/setup.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/__init__.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/api_client.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/auth.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/cli.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/config.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/events.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500/mqtt_client.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/tests/conftest.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/tests/test_command_queue.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/tests/test_events.py +0 -0
- {nwp500_python-1.1.3 → nwp500_python-1.1.5}/tox.ini +0 -0
|
@@ -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
|
|
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:**
|
|
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
|
|
|
@@ -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,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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|