nwp500-python 9.2.0__tar.gz → 9.3.0__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-9.2.0 → nwp500_python-9.3.0}/.gitignore +2 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/CHANGELOG.rst +223 -0
- {nwp500_python-9.2.0/src/nwp500_python.egg-info → nwp500_python-9.3.0}/PKG-INFO +1 -1
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/explanation/index.rst +2 -0
- nwp500_python-9.3.0/docs/explanation/tank-energy.rst +439 -0
- nwp500_python-9.3.0/docs/explanation/unknown-values.rst +259 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/how-to/manage-units.rst +4 -4
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/how-to/track-energy.rst +44 -30
- nwp500_python-9.3.0/docs/project/authors.rst +2 -0
- nwp500_python-9.3.0/docs/project/changelog.rst +2 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/project/history.rst +1 -1
- nwp500_python-9.3.0/docs/project/license.rst +8 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/index.rst +1 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/installation.rst +1 -1
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/protocol/data_conversions.rst +18 -5
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/protocol/device_status.rst +5 -5
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/python_api/models.rst +11 -3
- nwp500_python-9.3.0/examples/.ruff.toml +31 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/air_filter_reset.py +3 -2
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/anti_legionella.py +4 -4
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/auto_recovery.py +6 -5
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/combined_callbacks.py +10 -3
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/demand_response.py +4 -3
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/device_capabilities.py +5 -7
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/device_status_debug.py +6 -8
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/energy_analytics.py +4 -3
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/firmware_payload_capture.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/mqtt_diagnostics.py +14 -11
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/power_control.py +4 -3
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/recirculation_control.py +5 -4
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/reconnection_demo.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/simple_auto_recovery.py +4 -3
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/token_restoration.py +7 -7
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/tou_schedule.py +2 -2
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/water_reservation.py +3 -2
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/beginner/01_authentication.py +3 -2
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/beginner/02_list_devices.py +17 -12
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/beginner/03_get_status.py +2 -3
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/beginner/04_set_temperature.py +3 -2
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/intermediate/advanced_auth_patterns.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/intermediate/command_queue.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/intermediate/device_status_callback.py +5 -7
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/intermediate/event_driven_control.py +6 -4
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/intermediate/mqtt_realtime_monitoring.py +6 -8
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/intermediate/periodic_requests.py +6 -5
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/intermediate/set_mode.py +3 -2
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/intermediate/vacation_mode.py +3 -2
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/mask.py +5 -2
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/testing/periodic_device_info.py +5 -4
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/testing/simple_periodic_info.py +2 -3
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/testing/test_api_client.py +8 -8
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/testing/test_mqtt_connection.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/testing/test_mqtt_messaging.py +16 -13
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/testing/test_periodic_minimal.py +13 -10
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/__init__.py +7 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/cli/presentation.py +25 -6
- nwp500_python-9.3.0/src/nwp500/converters.py +242 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/device_capabilities.py +2 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/encoding.py +2 -2
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/enums.py +6 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/models/__init__.py +2 -2
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/models/schedule.py +31 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/models/status.py +79 -16
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/models/tou.py +35 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/control.py +32 -2
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt_events.py +1 -1
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/reservations.py +77 -2
- nwp500_python-9.3.0/src/nwp500/tou_schedule.py +105 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/SOURCES.txt +6 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/scm_file_list.json +185 -179
- nwp500_python-9.3.0/src/nwp500_python.egg-info/scm_version.json +8 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/conftest.py +4 -2
- nwp500_python-9.3.0/tests/test_canonical_schedule.py +161 -0
- nwp500_python-9.3.0/tests/test_cli_tristate_presentation.py +126 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_device_capabilities.py +31 -1
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_model_converters.py +120 -57
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_models.py +161 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_protocol_correctness.py +126 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_reservations.py +132 -0
- nwp500_python-9.3.0/tests/test_tou_schedule.py +192 -0
- nwp500_python-9.2.0/docs/project/authors.rst +0 -2
- nwp500_python-9.2.0/docs/project/changelog.rst +0 -2
- nwp500_python-9.2.0/docs/project/license.rst +0 -7
- nwp500_python-9.2.0/examples/.ruff.toml +0 -5
- nwp500_python-9.2.0/src/nwp500/converters.py +0 -154
- nwp500_python-9.2.0/src/nwp500_python.egg-info/scm_version.json +0 -8
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/.coveragerc +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/.github/workflows/ci.yml +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/.readthedocs.yml +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/AUTHORS.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/LICENSE.txt +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/Makefile +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/README.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/RELEASE.md +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/Makefile +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/conf.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/explanation/advanced-features.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/how-to/maintenance.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/how-to/optimize-tou.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/how-to/schedule-operation.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/index.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/openapi.yaml +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/protocol/quick_reference.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/python_api/cli.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/python_api/events.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/reference/python_api/mqtt_client.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/requirements.txt +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/README.md +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/pyproject.toml +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/scripts/README.md +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/scripts/bump_version.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/scripts/format.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/scripts/lint.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/scripts/validate_version.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/setup.cfg +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/setup.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/cli/__main__.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/cli/handlers.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/cli/output_formatters.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/cli/rich_output.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/config.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/events.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/models/device.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/models/energy.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/models/feature.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/__init__.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/_control_commands.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/client.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/command_queue.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/connection.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/state_tracker.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/subscriptions.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/types.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/mqtt/utils.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_auth.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_cli_commands.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_command_queue.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_events.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_mqtt_events.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_mqtt_reliability.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_multi_device.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_openei.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_public_api.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_unit_system_process_wide.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tests/test_utils.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.3.0}/tox.ini +0 -0
|
@@ -5,6 +5,229 @@ Changelog
|
|
|
5
5
|
Unreleased
|
|
6
6
|
==========
|
|
7
7
|
|
|
8
|
+
Version 9.3.0 (2026-08-03)
|
|
9
|
+
==========================
|
|
10
|
+
|
|
11
|
+
**BREAKING CHANGES**: two independent corrections land together.
|
|
12
|
+
|
|
13
|
+
Tank energy values were wrong in two ways - a 2.5x unit-scale error and
|
|
14
|
+
two actively misleading field names - and both are corrected. Reported
|
|
15
|
+
tank energy is now 2.5x smaller and two public field names are removed.
|
|
16
|
+
|
|
17
|
+
Separately, eight status flags change type from ``bool`` to
|
|
18
|
+
``bool | None`` so the device's "unknown" state is no longer reported as
|
|
19
|
+
a definite OFF.
|
|
20
|
+
|
|
21
|
+
Changed
|
|
22
|
+
-------
|
|
23
|
+
- **Energy unit scale corrected.** ``totalEnergyCapacity`` and ``availableEnergyCapacity`` were
|
|
24
|
+
scaled by 10 on the assumption the device reported 10 Wh units. It does
|
|
25
|
+
not. Because ``totalEnergyCapacity`` is a whole-tank quantity, its slope
|
|
26
|
+
against the setpoint measures the quantum with no stratification
|
|
27
|
+
assumption: on a 65-gallon NWP500 that is 70.25 raw counts per Kelvin.
|
|
28
|
+
The field turns out to be bimodal - at a fixed setpoint it takes one of
|
|
29
|
+
two values exactly 2 degC apart - but both branches give the same slope
|
|
30
|
+
to within 0.2%, so the quantum is unaffected. Converting to Watt-hours needs a
|
|
31
|
+
water mass, and a "65 gallon" tank does not hold 65 gallons - so taking
|
|
32
|
+
the quantum to be round, as every other conversion in this protocol is,
|
|
33
|
+
4 Wh/count is the only candidate implying a water volume below the
|
|
34
|
+
nameplate (241.7 L). Two further checks agree: 183 individual heating
|
|
35
|
+
recoveries give 4.11 Wh/count by a noisier route, and integrating
|
|
36
|
+
``currentInstPower`` over them implies a heat-pump COP of 2.89 at the new
|
|
37
|
+
scale against 7.02 at the old, the latter being physically impossible.
|
|
38
|
+
**Reported tank energy is now 2.5x smaller.**
|
|
39
|
+
Historical series logged from earlier versions need rescaling by 0.4 to
|
|
40
|
+
be comparable.
|
|
41
|
+
|
|
42
|
+
- **Energy fields renamed.**
|
|
43
|
+
``availableEnergyCapacity`` is not available energy - it is the energy
|
|
44
|
+
still *needed* to reach the setpoint. It falls as the tank heats and
|
|
45
|
+
reaches zero when the tank is fully charged, so code treating it as
|
|
46
|
+
stored energy had the signal backwards (regression against mean tank
|
|
47
|
+
temperature: negative slope, R-squared 0.93, zero crossing at the
|
|
48
|
+
setpoint). Likewise ``totalEnergyCapacity`` is not a fixed tank size but
|
|
49
|
+
the cost of a full recovery to the *current setpoint*, measured from the
|
|
50
|
+
device's own minimum setpoint of 104.9 degF; it moves by about 140 Wh
|
|
51
|
+
per 0.5 degC of setpoint change.
|
|
52
|
+
|
|
53
|
+
.. code-block:: python
|
|
54
|
+
|
|
55
|
+
# OLD (removed)
|
|
56
|
+
status.total_energy_capacity # 15800.0
|
|
57
|
+
status.available_energy_capacity # 11660.0
|
|
58
|
+
|
|
59
|
+
# NEW
|
|
60
|
+
status.full_recovery_energy # 6320.0
|
|
61
|
+
status.energy_to_setpoint # 4664.0
|
|
62
|
+
|
|
63
|
+
The protocol field names on the wire are unchanged. CLI rows are
|
|
64
|
+
relabelled from "Total Capacity"/"Available Capacity" to
|
|
65
|
+
"Full Recovery"/"Energy to Setpoint".
|
|
66
|
+
|
|
67
|
+
- **Status flags now preserve the device's unknown state.** The protocol
|
|
68
|
+
encodes these flags as ``0 = unknown, 1 = OFF, 2 = ON``, and the library
|
|
69
|
+
was collapsing 0 to ``False`` - inventing an OFF the device never claimed.
|
|
70
|
+
Confirmed against Navien's own NaviLink app (2.03.00, versionCode 141),
|
|
71
|
+
which decodes exactly this set of fields through an enum declared
|
|
72
|
+
``UNKNOWN(0), OFF(1), ON(2)``; two sibling enums render their zero as
|
|
73
|
+
``"-"`` and ``"Not Applied"`` rather than as an off state.
|
|
74
|
+
|
|
75
|
+
Affected: ``operation_busy``, ``comp_use``, ``anti_legionella_use``,
|
|
76
|
+
``anti_legionella_operation_busy``, ``heat_upper_use``, ``heat_lower_use``,
|
|
77
|
+
``air_filter_alarm_use``, ``recirc_reservation_use``.
|
|
78
|
+
|
|
79
|
+
``None`` is falsy, so ``if status.comp_use:`` is unaffected. Code that
|
|
80
|
+
distinguishes ``is False`` from "not reported", or does arithmetic or
|
|
81
|
+
formatting on these fields, needs a ``None`` check. For Home Assistant
|
|
82
|
+
this is the wanted shape: ``None`` renders as "Unknown" instead of writing
|
|
83
|
+
a fabricated OFF into the recorder database.
|
|
84
|
+
|
|
85
|
+
- ``OnOffFlag`` gains the vendor's ``UNKNOWN = 0`` member. It previously
|
|
86
|
+
started at ``OFF = 1``, leaving the device's reserved value unrepresented.
|
|
87
|
+
|
|
88
|
+
- The CLI renders these flags as ``Unknown`` rather than ``No``. Both
|
|
89
|
+
affected rows are updated: "Busy" under OPERATION STATUS and
|
|
90
|
+
"Operation Busy" under ANTI-LEGIONELLA.
|
|
91
|
+
|
|
92
|
+
Added
|
|
93
|
+
-----
|
|
94
|
+
- **``DeviceStatus.usable_energy``**: drawable energy in Watt-hours,
|
|
95
|
+
computed as ``full_recovery_energy - energy_to_setpoint``. Both raw
|
|
96
|
+
fields are measured from the setpoint, so neither is a state of charge;
|
|
97
|
+
subtracting them cancels the setpoint and leaves the tank's heat above
|
|
98
|
+
the device's minimum operating temperature (104.9 degF), which is about
|
|
99
|
+
the lowest temperature usable for a shower. Robust despite
|
|
100
|
+
``full_recovery_energy`` being bimodal, since both fields shift
|
|
101
|
+
together: the implied tank temperature tracks the thermistor mean to a
|
|
102
|
+
standard deviation of 0.57 degF over 12275 samples. Rendered by the CLI
|
|
103
|
+
as "Usable Energy".
|
|
104
|
+
|
|
105
|
+
- ``converters.device_tristate_to_python`` and
|
|
106
|
+
``models.status.DeviceTriState`` for flags the device may decline to
|
|
107
|
+
report. ``converters.device_bool_to_python`` is unchanged and remains
|
|
108
|
+
correct for capability flags.
|
|
109
|
+
- New ``docs/explanation/unknown-values.rst`` recording which field families
|
|
110
|
+
use 0 as a sentinel and which do not, with the app evidence for each.
|
|
111
|
+
|
|
112
|
+
Removed
|
|
113
|
+
-------
|
|
114
|
+
- **Misnamed energy fields**: removed ``DeviceStatus.total_energy_capacity``
|
|
115
|
+
and ``DeviceStatus.available_energy_capacity`` outright rather than
|
|
116
|
+
aliasing them, so a missed rename fails with ``AttributeError`` instead
|
|
117
|
+
of silently returning a number 2.5x too large. Use
|
|
118
|
+
``full_recovery_energy`` and ``energy_to_setpoint``.
|
|
119
|
+
|
|
120
|
+
- **Incorrect converter**: removed ``converters.mul_10``, which existed
|
|
121
|
+
only to apply the wrong 10 Wh scale. Use
|
|
122
|
+
``converters.energy_count_to_wh`` and ``converters.WH_PER_ENERGY_COUNT``.
|
|
123
|
+
|
|
124
|
+
Fixed
|
|
125
|
+
-----
|
|
126
|
+
- **Documentation contradicted itself and the code on energy capacity.**
|
|
127
|
+
Three incompatible descriptions were published: Watt-hours with no
|
|
128
|
+
conversion (protocol reference), Watt-hours with a x10 scale (the code),
|
|
129
|
+
and a 0-100 percentage (``how-to/track-energy.rst``,
|
|
130
|
+
``reference/python_api/models.rst``, ``project/history.rst``). The
|
|
131
|
+
percentage claim was never true of any library version. All are now
|
|
132
|
+
consistent.
|
|
133
|
+
- ``how-to/track-energy.rst`` documented four fields that do not exist on
|
|
134
|
+
``DeviceStatus`` (``dhw_tank_upper_temp``, ``dhw_tank_lower_temp``,
|
|
135
|
+
``comp_temp``, ``dhw_heatex_out_temp``); replaced with the real names.
|
|
136
|
+
- ``dhwTemperature`` is documented as an outlet temperature but is
|
|
137
|
+
measured inside the tank: it tracks ``tankUpperTemperature`` to within
|
|
138
|
+
one 0.5 degC step, and the device has no sensor downstream of itself.
|
|
139
|
+
Navien's own app agrees, labelling it "DHW Temp." beside the tank
|
|
140
|
+
thermistors and showing ``dischargeTemperature`` separately.
|
|
141
|
+
- ``docs/explanation/tank-energy.rst`` tabulated ``totalEnergyCapacity``
|
|
142
|
+
and ``availableEnergyCapacity`` at ten times their raw wire values,
|
|
143
|
+
under column headings naming the raw protocol fields. The series had
|
|
144
|
+
been logged through the pre-fix library, which multiplied by 10. The
|
|
145
|
+
prose beside the tables ("70.25 raw counts per Kelvin", "35 counts")
|
|
146
|
+
was already in true raw counts, so the page contradicted itself by
|
|
147
|
+
exactly the factor it exists to correct. Tables are now in raw counts;
|
|
148
|
+
1580 counts at a 145.4 degF setpoint matches the test fixtures and the
|
|
149
|
+
6320 Wh the CLI reports. No numeric conclusion changes.
|
|
150
|
+
- ``docs/explanation/tank-energy.rst`` referred to a field
|
|
151
|
+
``hpUpperOnTemperatureSetting``; the protocol name is
|
|
152
|
+
``hpUpperOnTempSetting``.
|
|
153
|
+
- The setpoint-per-0.5-degC figure is 140 Wh, not 143 Wh, which is what
|
|
154
|
+
the documented slope of 70.25 counts/K gives at 4 Wh/count. Corrected
|
|
155
|
+
in the changelog, the model field description and the protocol
|
|
156
|
+
reference.
|
|
157
|
+
- New ``docs/explanation/tank-energy.rst`` records what the two fields
|
|
158
|
+
actually measure and the calibration evidence behind the scale change,
|
|
159
|
+
including that Navien's own NaviLink app (2.03.00, versionCode 141)
|
|
160
|
+
reads neither field, so no vendor-side corroboration exists.
|
|
161
|
+
|
|
162
|
+
- Documented that **temperature fields carry no sentinel at all**. The app
|
|
163
|
+
has no out-of-band constant (no ``0xFFFF``/``-999``/``-1``), no zero-guard
|
|
164
|
+
in any display path, and formats whatever arrives - so a temperature of
|
|
165
|
+
zero means zero. This closes a recurring source of bugs where zero-as-none
|
|
166
|
+
was applied to temperature converters and reported working sensors as
|
|
167
|
+
missing during cold-weather operation.
|
|
168
|
+
- Documented that capability flags are a distinct case: the app hides a
|
|
169
|
+
feature's entire UI when its DID ``Use`` flag reads 0, so 0 there means
|
|
170
|
+
"not fitted" and the existing ``bool`` mapping is correct.
|
|
171
|
+
|
|
172
|
+
Version 9.2.1 (2026-07-30)
|
|
173
|
+
==========================
|
|
174
|
+
|
|
175
|
+
Added
|
|
176
|
+
-----
|
|
177
|
+
- **Confirmed reservation/TOU write helpers and canonical schedule
|
|
178
|
+
comparison** (`#111 <https://github.com/eman/nwp500-python/issues/111>`_):
|
|
179
|
+
``update_reservations`` and ``configure_tou_schedule`` were
|
|
180
|
+
fire-and-forget, returning only an MQTT publish packet id with no way to
|
|
181
|
+
confirm the device applied the write. New
|
|
182
|
+
``nwp500.reservations.update_reservations_confirmed()`` and
|
|
183
|
+
``nwp500.tou_schedule.configure_tou_schedule_confirmed()`` (also exported
|
|
184
|
+
from the top-level ``nwp500`` package) send the write and await the
|
|
185
|
+
device's ``rsv/rd``/``tou/rd`` echo, returning the parsed schedule the
|
|
186
|
+
device now holds (or ``None`` on timeout). Because the protocol has no
|
|
187
|
+
request/response correlation id on these topics, a response is only
|
|
188
|
+
accepted once it matches the content that was just written, so a
|
|
189
|
+
stale/unrelated response from a concurrent read or a previous write
|
|
190
|
+
can't be mistaken for confirmation. Also added
|
|
191
|
+
``ReservationEntry.canonical_key()``/``ReservationSchedule.canonical()``
|
|
192
|
+
and the ``TOUPeriod``/``TOUReservationSchedule`` equivalents: a stable,
|
|
193
|
+
order-independent, hashable representation of the raw protocol fields so
|
|
194
|
+
a desired program can be compared against a device read-back with
|
|
195
|
+
``desired.canonical() == confirmed.canonical()`` instead of hand-diffing.
|
|
196
|
+
|
|
197
|
+
Fixed
|
|
198
|
+
-----
|
|
199
|
+
- **Freeze protection temperature now validated against device limits**
|
|
200
|
+
(`#112 <https://github.com/eman/nwp500-python/issues/112>`_):
|
|
201
|
+
``set_freeze_protection_temperature`` documented a 35-45°F valid range
|
|
202
|
+
but sent the value straight through with no validation. It now fetches
|
|
203
|
+
device features and validates against
|
|
204
|
+
``freeze_protection_temp_min``/``freeze_protection_temp_max`` (raising
|
|
205
|
+
``RangeValidationError`` if out of range), matching the pattern already
|
|
206
|
+
used by ``set_dhw_temperature``. The command is also now gated behind a
|
|
207
|
+
new ``freeze_protection_use`` capability (added to
|
|
208
|
+
``MqttDeviceCapabilityChecker._CAPABILITY_MAP`` and enforced with
|
|
209
|
+
``@requires_capability``), so it's no longer dispatched to devices that
|
|
210
|
+
don't support freeze protection at all.
|
|
211
|
+
- **Demand response commands gated on the dr_setting_use capability**
|
|
212
|
+
(`#114 <https://github.com/eman/nwp500-python/issues/114>`_):
|
|
213
|
+
``enable_demand_response``/``disable_demand_response`` were dispatched
|
|
214
|
+
unconditionally even though the ``dr_setting_use`` capability flag
|
|
215
|
+
exists on ``DeviceFeature``. Every other controllable feature follows
|
|
216
|
+
flag + ``_CAPABILITY_MAP`` entry + ``@requires_capability`` decorator;
|
|
217
|
+
DR was the one gap. Added the missing map entry and decorators so DR
|
|
218
|
+
now follows the same pattern.
|
|
219
|
+
|
|
220
|
+
Documentation
|
|
221
|
+
-------------
|
|
222
|
+
- **decode_reservation_hex docstring corrected** (`#113
|
|
223
|
+
<https://github.com/eman/nwp500-python/issues/113>`_): the ``param``
|
|
224
|
+
byte was documented as "temperature offset by 20°F", contradicting the
|
|
225
|
+
half-degrees-Celsius encoding used consistently everywhere else
|
|
226
|
+
(``models/schedule.py``, ``models/_converters.py``). Corrected to
|
|
227
|
+
"target temperature, half-degrees Celsius; °C = param/2", and fixed the
|
|
228
|
+
accompanying doctest-style example, which showed a ``minute`` key where
|
|
229
|
+
the function actually returns ``min``.
|
|
230
|
+
|
|
8
231
|
Version 9.2.0 (2026-07-06)
|
|
9
232
|
==========================
|
|
10
233
|
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
===========
|
|
2
|
+
Tank Energy
|
|
3
|
+
===========
|
|
4
|
+
|
|
5
|
+
The NWP500 reports two energy figures, ``totalEnergyCapacity`` and
|
|
6
|
+
``availableEnergyCapacity``. Both names mislead, and before v10.0 this
|
|
7
|
+
library also scaled them wrongly. This page explains what they
|
|
8
|
+
actually measure and shows the evidence for the correction.
|
|
9
|
+
|
|
10
|
+
.. contents::
|
|
11
|
+
:local:
|
|
12
|
+
:depth: 2
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
The short version
|
|
16
|
+
=================
|
|
17
|
+
|
|
18
|
+
.. list-table::
|
|
19
|
+
:header-rows: 1
|
|
20
|
+
:widths: 22 30 48
|
|
21
|
+
|
|
22
|
+
* - Protocol field
|
|
23
|
+
- What the name suggests
|
|
24
|
+
- What it actually is
|
|
25
|
+
* - ``availableEnergyCapacity``
|
|
26
|
+
- Energy available in the tank
|
|
27
|
+
- Energy still **needed** to reach the setpoint. It *falls* as the
|
|
28
|
+
tank heats and hits zero when the tank is fully charged - the
|
|
29
|
+
exact inverse of the name.
|
|
30
|
+
* - ``totalEnergyCapacity``
|
|
31
|
+
- Fixed tank capacity
|
|
32
|
+
- Cost of a full recovery to the **current setpoint**. It moves
|
|
33
|
+
whenever the setpoint moves.
|
|
34
|
+
|
|
35
|
+
Both are raw counts of 4 Wh each, not Watt-hours. Library versions before
|
|
36
|
+
10.0 multiplied by 10, overstating tank energy by 2.5x.
|
|
37
|
+
|
|
38
|
+
Both are also measured **from the setpoint**, so both describe potential
|
|
39
|
+
rather than content: move the setpoint and both change while the water in
|
|
40
|
+
the tank does not. Neither is a state of charge.
|
|
41
|
+
|
|
42
|
+
Their **difference** is a state of charge, and is exposed as
|
|
43
|
+
``DeviceStatus.usable_energy``:
|
|
44
|
+
|
|
45
|
+
.. code:: text
|
|
46
|
+
|
|
47
|
+
usable_energy = full_recovery_energy - energy_to_setpoint
|
|
48
|
+
= k * (tank_temperature - 104.9 degF)
|
|
49
|
+
|
|
50
|
+
The setpoint cancels. What remains is the tank's heat above the device's
|
|
51
|
+
minimum operating temperature - close enough to the lowest useful shower
|
|
52
|
+
temperature that it is a good estimate of what you can actually draw.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
How the fields behave
|
|
56
|
+
=====================
|
|
57
|
+
|
|
58
|
+
Both fields fit a single two-parameter model:
|
|
59
|
+
|
|
60
|
+
.. code:: text
|
|
61
|
+
|
|
62
|
+
energy_to_setpoint = k * (setpoint - tank_mean_temperature)
|
|
63
|
+
full_recovery_energy = k * (setpoint - reference_temperature)
|
|
64
|
+
|
|
65
|
+
where ``k`` is the tank's heat capacity and ``reference_temperature`` is
|
|
66
|
+
the device's own minimum setpoint, ``dhwTemperatureMin`` (40.5 degC /
|
|
67
|
+
104.9 degF) - though only about two thirds of the time, see
|
|
68
|
+
`Two branches`_.
|
|
69
|
+
|
|
70
|
+
Two consequences follow, and both matter:
|
|
71
|
+
|
|
72
|
+
* ``energy_to_setpoint`` is a **deficit**. Code that treats it as stored
|
|
73
|
+
energy has the signal backwards: it is largest when the tank is
|
|
74
|
+
coldest.
|
|
75
|
+
* ``full_recovery_energy`` is **not a constant**. Raising the setpoint by
|
|
76
|
+
0.5 degC raises it by 35 counts, about 140 Wh, on a 65-gallon tank. Seven months of
|
|
77
|
+
history on one device shows sixteen distinct values as the setpoint was
|
|
78
|
+
adjusted.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
The evidence
|
|
82
|
+
============
|
|
83
|
+
|
|
84
|
+
Deficit, not stored energy
|
|
85
|
+
--------------------------
|
|
86
|
+
|
|
87
|
+
During a heating recovery on a 65-gallon unit at a 140.9 degF setpoint,
|
|
88
|
+
with the tank warming and no draws. Each row is a mean over the samples
|
|
89
|
+
in that temperature bin, so the counts are not whole numbers:
|
|
90
|
+
|
|
91
|
+
.. list-table::
|
|
92
|
+
:header-rows: 1
|
|
93
|
+
|
|
94
|
+
* - Mean tank temp
|
|
95
|
+
- Setpoint minus tank
|
|
96
|
+
- mean ``availableEnergyCapacity``
|
|
97
|
+
- ``dhwChargePer``
|
|
98
|
+
* - 119.5 degF
|
|
99
|
+
- 21.4 degF
|
|
100
|
+
- 881.6
|
|
101
|
+
- 54.5 %
|
|
102
|
+
* - 127.4 degF
|
|
103
|
+
- 13.5 degF
|
|
104
|
+
- 530.0
|
|
105
|
+
- 69.5 %
|
|
106
|
+
* - 135.1 degF
|
|
107
|
+
- 5.8 degF
|
|
108
|
+
- 227.5
|
|
109
|
+
- 85.7 %
|
|
110
|
+
|
|
111
|
+
The field falls as the tank fills with heat. Regressed against mean tank
|
|
112
|
+
temperature over two weeks of five-minute samples, the slope is negative
|
|
113
|
+
with an R-squared of 0.93 and a zero crossing at the setpoint.
|
|
114
|
+
|
|
115
|
+
The last two rows also check out against the tank's heat capacity of
|
|
116
|
+
156 Wh/degF: 227.5 counts x 4 Wh = 910 Wh against 5.8 degF x 156 =
|
|
117
|
+
905 Wh, and 530.0 counts = 2120 Wh against 13.5 degF x 156 = 2106 Wh.
|
|
118
|
+
The coldest row runs about 6 % high, which is the stratification error
|
|
119
|
+
in using ``(upper + lower) / 2`` as the mean tank temperature - it is
|
|
120
|
+
worst when the tank is least mixed.
|
|
121
|
+
|
|
122
|
+
The 4 Wh quantum
|
|
123
|
+
----------------
|
|
124
|
+
|
|
125
|
+
``totalEnergyCapacity`` is a whole-tank quantity, so its slope against
|
|
126
|
+
the setpoint measures the quantum without needing any assumption about
|
|
127
|
+
how the tank stratifies.
|
|
128
|
+
|
|
129
|
+
The device does not report a single ``totalEnergyCapacity`` per setpoint -
|
|
130
|
+
see `Two branches`_ below - so the table lists the most common value at
|
|
131
|
+
each setpoint, which covers 68 % of samples. Values are raw counts as
|
|
132
|
+
they arrive on the wire:
|
|
133
|
+
|
|
134
|
+
.. list-table::
|
|
135
|
+
:header-rows: 1
|
|
136
|
+
|
|
137
|
+
* - Setpoint
|
|
138
|
+
- ``totalEnergyCapacity``
|
|
139
|
+
- Setpoint
|
|
140
|
+
- ``totalEnergyCapacity``
|
|
141
|
+
* - 140.0 degF
|
|
142
|
+
- 1369
|
|
143
|
+
- 144.5 degF
|
|
144
|
+
- 1545
|
|
145
|
+
* - 140.9 degF
|
|
146
|
+
- 1404
|
|
147
|
+
- 145.4 degF
|
|
148
|
+
- 1580
|
|
149
|
+
* - 141.8 degF
|
|
150
|
+
- 1439
|
|
151
|
+
- 146.3 degF
|
|
152
|
+
- 1615
|
|
153
|
+
* - 142.7 degF
|
|
154
|
+
- 1475
|
|
155
|
+
- 147.2 degF
|
|
156
|
+
- 1650
|
|
157
|
+
* - 143.6 degF
|
|
158
|
+
- 1510
|
|
159
|
+
- 148.1 degF
|
|
160
|
+
- 1685
|
|
161
|
+
|
|
162
|
+
An arithmetic sequence: least squares gives **R-squared 0.99999** and a
|
|
163
|
+
slope of **70.25 raw counts per Kelvin** of whole-tank temperature rise.
|
|
164
|
+
The endpoints alone give the same figure: (1685 - 1369) / 4.5 K = 70.2.
|
|
165
|
+
|
|
166
|
+
The slope is the robust part of this. The second branch, fitted
|
|
167
|
+
separately, gives 38.98 counts/degF against the primary's 39.06 - the
|
|
168
|
+
same figure to within 0.2 %. Two independent populations agreeing on the
|
|
169
|
+
slope is stronger evidence for the quantum than either alone.
|
|
170
|
+
|
|
171
|
+
Converting that to Watt-hours needs a water mass, and this is where care
|
|
172
|
+
is required: a "65 gallon" tank does not hold 65 gallons of water. The
|
|
173
|
+
nameplate is an upper bound - the vendor's own app hard-codes it, mapping
|
|
174
|
+
``volumeCode`` 1/2/3 to 189.2 L, **246.0 L** and 302.8 L (see `What the
|
|
175
|
+
vendor app does with these fields`_) - and the water actually in the tank
|
|
176
|
+
must come in under it. Rather than assume nominal volume and derive an
|
|
177
|
+
odd-looking quantum, assume the quantum is a round number - every other
|
|
178
|
+
conversion in this protocol is (half-degrees, tenths) - and see which one
|
|
179
|
+
implies a sensible volume:
|
|
180
|
+
|
|
181
|
+
.. list-table::
|
|
182
|
+
:header-rows: 1
|
|
183
|
+
|
|
184
|
+
* - Candidate quantum
|
|
185
|
+
- Implied water volume
|
|
186
|
+
- Plausible?
|
|
187
|
+
* - **4 Wh** (1/250 kWh)
|
|
188
|
+
- **241.7 L / 63.9 gal**
|
|
189
|
+
- Yes - slightly under nominal, as expected
|
|
190
|
+
* - 1/240 kWh (4.167 Wh)
|
|
191
|
+
- 251.8 L / 66.5 gal
|
|
192
|
+
- No - more than the nameplate
|
|
193
|
+
* - 10 kJ
|
|
194
|
+
- 167.8 L / 44.3 gal
|
|
195
|
+
- No
|
|
196
|
+
* - 15 kJ
|
|
197
|
+
- 251.7 L / 66.5 gal
|
|
198
|
+
- No
|
|
199
|
+
|
|
200
|
+
**4 Wh per count** is the only round candidate implying a volume below
|
|
201
|
+
the nameplate, which is the only physically sensible direction. The
|
|
202
|
+
library uses 4.0.
|
|
203
|
+
|
|
204
|
+
A second, noisier method agrees. Across 183 individual heating
|
|
205
|
+
recoveries, dividing each tank sensible-heat gain (from the two
|
|
206
|
+
thermistors and the nominal mass) by the device's reported change gives a
|
|
207
|
+
median of 4.11 Wh/count, p10 3.47 and p90 4.45. That route depends on
|
|
208
|
+
``(upper + lower) / 2`` approximating the true mean tank temperature, so
|
|
209
|
+
it is far less precise, but it is an independent confirmation and it does
|
|
210
|
+
not use electrical input at all.
|
|
211
|
+
|
|
212
|
+
The efficiency cross-check
|
|
213
|
+
--------------------------
|
|
214
|
+
|
|
215
|
+
A third check rules out the old scale on its own. Integrating
|
|
216
|
+
``currentInstPower`` over each recovery gives the electrical energy in,
|
|
217
|
+
and dividing the device's *reported* energy gain by it gives an implied
|
|
218
|
+
coefficient of performance:
|
|
219
|
+
|
|
220
|
+
.. list-table::
|
|
221
|
+
:header-rows: 1
|
|
222
|
+
|
|
223
|
+
* - Scale used
|
|
224
|
+
- Implied COP (median)
|
|
225
|
+
- Verdict
|
|
226
|
+
* - 4 Wh/count (corrected)
|
|
227
|
+
- 2.89
|
|
228
|
+
- Normal for a heat pump water heater
|
|
229
|
+
* - 10 Wh/count (pre-10.0)
|
|
230
|
+
- 7.02
|
|
231
|
+
- Physically impossible
|
|
232
|
+
|
|
233
|
+
A heat pump water heater in a 72 degF room runs at a COP of roughly 2 to
|
|
234
|
+
4. A COP of 7 would mean the device generated energy it never consumed.
|
|
235
|
+
|
|
236
|
+
This argument is worth stating separately because it needs no tank
|
|
237
|
+
volume, specific heat or stratification model - only the device's own
|
|
238
|
+
reported energy and its own reported power. It cannot tell you what the
|
|
239
|
+
quantum *is*, but it rules out the pre-10.0 value regardless of anything
|
|
240
|
+
assumed elsewhere on this page.
|
|
241
|
+
|
|
242
|
+
.. _two branches:
|
|
243
|
+
|
|
244
|
+
Two branches
|
|
245
|
+
------------
|
|
246
|
+
|
|
247
|
+
``totalEnergyCapacity`` is **not** a function of the setpoint alone. At a
|
|
248
|
+
fixed setpoint it takes one of two values, flipping between them several
|
|
249
|
+
times a day. Over four months at nine setpoints:
|
|
250
|
+
|
|
251
|
+
.. list-table::
|
|
252
|
+
:header-rows: 1
|
|
253
|
+
|
|
254
|
+
* - Branch
|
|
255
|
+
- Share
|
|
256
|
+
- Slope
|
|
257
|
+
- Zero crossing
|
|
258
|
+
* - Primary
|
|
259
|
+
- 68 %
|
|
260
|
+
- 39.06 counts/degF
|
|
261
|
+
- **104.95 degF**
|
|
262
|
+
* - Secondary
|
|
263
|
+
- 32 %
|
|
264
|
+
- 38.98 counts/degF
|
|
265
|
+
- **108.48 degF**
|
|
266
|
+
|
|
267
|
+
The two are parallel, separated by a constant 140-141 counts - exactly
|
|
268
|
+
**2 degC** of setpoint - at every setpoint measured.
|
|
269
|
+
|
|
270
|
+
The primary branch's zero crossing matches the device's
|
|
271
|
+
``dhwTemperatureMin`` of 104.9 degF to within a twentieth of a degree -
|
|
272
|
+
140.0 - 1369 / 39.06 = 104.95 degF - using only the device's own two
|
|
273
|
+
numbers: no tank mass, no specific heat, no thermistors, no assumption
|
|
274
|
+
about the quantum. On that branch,
|
|
275
|
+
|
|
276
|
+
.. code:: text
|
|
277
|
+
|
|
278
|
+
full_recovery_energy = k * (setpoint - dhwTemperatureMin)
|
|
279
|
+
|
|
280
|
+
The secondary branch behaves identically with a reference 2 degC higher,
|
|
281
|
+
and **what selects between them is unknown**. The device's
|
|
282
|
+
``hpUpperOnTempSetting`` correlates with the choice - 104.9 degF
|
|
283
|
+
when the primary is active, 143.4 degF when the secondary is - which
|
|
284
|
+
would fit the device computing recovery cost from its own turn-on
|
|
285
|
+
threshold, but only 22 paired samples were available and that is a lead
|
|
286
|
+
rather than a finding.
|
|
287
|
+
|
|
288
|
+
.. warning::
|
|
289
|
+
Because of this, do not derive the tank's heat capacity from a live
|
|
290
|
+
``full_recovery_energy`` reading: landing on the wrong branch gives an
|
|
291
|
+
error of about 9 %. Use the slope, which is stable across both
|
|
292
|
+
branches, or compute stored energy from the thermistors directly.
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
What ``dhwChargePer`` does
|
|
296
|
+
==========================
|
|
297
|
+
|
|
298
|
+
``dhwChargePer`` is a fourth signal and does not reconcile with the other
|
|
299
|
+
two. On one device, ``energy_to_setpoint / full_recovery_energy``
|
|
300
|
+
implies 30 % charged while ``dhwChargePer`` reads 59 %; over two weeks
|
|
301
|
+
the two differ by a mean of 48 points with 32 points of scatter.
|
|
302
|
+
|
|
303
|
+
It is nonetheless the number Navien shows its own users: the NaviLink app
|
|
304
|
+
prints it unmodified as a percentage labelled "DHW Charge", with no
|
|
305
|
+
client-side arithmetic of any kind. Whatever it means, it is computed on
|
|
306
|
+
the device, and a user comparing the app against this library will see
|
|
307
|
+
the app's figure and not the ratio above.
|
|
308
|
+
|
|
309
|
+
Treat it as an opaque vendor heuristic rather than a defined fraction of
|
|
310
|
+
anything. For a charge figure with defined meaning, use
|
|
311
|
+
``usable_energy``.
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
.. _what the vendor app does with these fields:
|
|
315
|
+
|
|
316
|
+
What the vendor app does with these fields
|
|
317
|
+
==========================================
|
|
318
|
+
|
|
319
|
+
Nothing. Navien's own NaviLink app never reads either field.
|
|
320
|
+
|
|
321
|
+
Decompiling the current release - version 2.03.00, versionCode 141,
|
|
322
|
+
published March 2026 - gives 8,101 Java sources, and neither
|
|
323
|
+
``totalEnergyCapacity`` nor ``availableEnergyCapacity`` appears in any of
|
|
324
|
+
them. Neither string appears in the raw dex string pool either, which
|
|
325
|
+
rules out the names having been lost to obfuscation. The app's status
|
|
326
|
+
model, ``KDResponseMgppStatus.Status``, declares about 140 fields -
|
|
327
|
+
including ``dhwChargePer``, ``tankUpperTemperature``,
|
|
328
|
+
``tankLowerTemperature``, ``currentInstPower`` and ``mixingRate`` - and
|
|
329
|
+
neither energy field is among them. The app requests no field subset, so
|
|
330
|
+
the device sends both and the app discards them on deserialization.
|
|
331
|
+
|
|
332
|
+
As a control, ``dhwChargePer`` and ``tankUpperTemperature`` *are* present
|
|
333
|
+
in the dex strings, so the absence of the other two is a real result and
|
|
334
|
+
not a broken search.
|
|
335
|
+
|
|
336
|
+
This matters for reading the rest of this page. There is no vendor label,
|
|
337
|
+
no vendor scale factor and no vendor formula to check the conclusions
|
|
338
|
+
above against - the evidence here is the only account of these two fields
|
|
339
|
+
that exists. It also explains why the protocol names are so misleading:
|
|
340
|
+
nothing Navien ships ever has to act on them.
|
|
341
|
+
|
|
342
|
+
The app does corroborate the surrounding facts this page leans on:
|
|
343
|
+
|
|
344
|
+
.. list-table::
|
|
345
|
+
:header-rows: 1
|
|
346
|
+
:widths: 40 60
|
|
347
|
+
|
|
348
|
+
* - What the app does
|
|
349
|
+
- What it confirms
|
|
350
|
+
* - ``MgppStatusFragment.getVolume()`` maps ``volumeCode`` 1/2/3 to
|
|
351
|
+
"50"/"65"/"80" gallons and 189.2/246.0/302.8 L, displayed under
|
|
352
|
+
the label "Volume"
|
|
353
|
+
- The nominal volume the quantum candidates are judged against is
|
|
354
|
+
the vendor's own figure, not an assumption of ours
|
|
355
|
+
* - ``MgppControlFragment.makeTempMap()`` reads
|
|
356
|
+
``dhwTemperatureMin / 2.0f`` as degC before converting
|
|
357
|
+
- ``dhwTemperatureMin`` is in half-degrees C, so 40.5 degC /
|
|
358
|
+
104.9 degF is the exact value the regression lands on. The app
|
|
359
|
+
rounds for display and shows 105 degF
|
|
360
|
+
* - The status screen labels ``dhwTemperature`` "DHW Temp." next to
|
|
361
|
+
"Upper Temp." and "Lower Temp.", and gives ``dischargeTemperature``
|
|
362
|
+
a separate "Discharge Temp." row
|
|
363
|
+
- The vendor UI does not treat ``dhwTemperature`` as an outlet
|
|
364
|
+
reading, and reserves a different field for what leaves the unit
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
Drawable energy
|
|
368
|
+
===============
|
|
369
|
+
|
|
370
|
+
``DeviceStatus.usable_energy`` is the difference of the two fields, and
|
|
371
|
+
is the one number here that describes the tank's state rather than its
|
|
372
|
+
distance from a target:
|
|
373
|
+
|
|
374
|
+
.. code:: text
|
|
375
|
+
|
|
376
|
+
usable_energy = full_recovery_energy - energy_to_setpoint
|
|
377
|
+
|
|
378
|
+
Raising the setpoint inflates both inputs equally, so the result does not
|
|
379
|
+
move - which is what makes it a state of charge and the two raw fields
|
|
380
|
+
not.
|
|
381
|
+
|
|
382
|
+
The implied reference is ``dhw_temperature_min``, 104.9 degF. A shower
|
|
383
|
+
runs around 105 degF, so heat below that reference is real but not
|
|
384
|
+
useful, and excluding it is the behaviour you want. Note that a mixing
|
|
385
|
+
valve does not change this floor: it caps how *hot* water can be
|
|
386
|
+
delivered, and once the tank falls below its setting it simply passes
|
|
387
|
+
through, so water stays usable down to the temperature you actually want
|
|
388
|
+
at the tap.
|
|
389
|
+
|
|
390
|
+
Despite ``full_recovery_energy`` being bimodal (see `Two branches`_),
|
|
391
|
+
the difference is robust, because both fields shift together. Checked
|
|
392
|
+
against the tank thermistors over 12275 samples, the tank temperature
|
|
393
|
+
implied by ``usable_energy`` agrees with the thermistor mean to a
|
|
394
|
+
standard deviation of **0.57 degF**, with 97.5 % of samples inside
|
|
395
|
+
2 degF.
|
|
396
|
+
|
|
397
|
+
If you need a different floor - a bath at 100 degF, or energy above the
|
|
398
|
+
cold inlet - compute it from the thermistors instead. On a 65-gallon tank
|
|
399
|
+
the heat capacity is 156 Wh per degF:
|
|
400
|
+
|
|
401
|
+
.. code:: text
|
|
402
|
+
|
|
403
|
+
drawable_Wh = 156 * (tank_mean_temperature - your_floor_degF)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
Migrating from before v10.0
|
|
407
|
+
===========================
|
|
408
|
+
|
|
409
|
+
.. list-table::
|
|
410
|
+
:header-rows: 1
|
|
411
|
+
:widths: 40 60
|
|
412
|
+
|
|
413
|
+
* - Old
|
|
414
|
+
- New
|
|
415
|
+
* - ``status.total_energy_capacity`` (removed)
|
|
416
|
+
- ``status.full_recovery_energy`` - and the value is 2.5x smaller
|
|
417
|
+
* - ``status.available_energy_capacity`` (removed)
|
|
418
|
+
- ``status.energy_to_setpoint`` - 2.5x smaller, and note it was
|
|
419
|
+
never "available"
|
|
420
|
+
* - Treating the value as available energy
|
|
421
|
+
- It is a deficit; invert the logic. ``full_recovery_energy -
|
|
422
|
+
energy_to_setpoint`` is the energy added above the device's
|
|
423
|
+
reference temperature
|
|
424
|
+
* - Treating the value as a percentage
|
|
425
|
+
- It never was one
|
|
426
|
+
|
|
427
|
+
The old attribute names are removed rather than aliased, so a
|
|
428
|
+
rename that is missed fails immediately with ``AttributeError`` instead
|
|
429
|
+
of silently returning a number 2.5x too large.
|
|
430
|
+
|
|
431
|
+
If you logged these values historically, the stored series needs
|
|
432
|
+
rescaling by 0.4 to be comparable with values from v10.0 onward.
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
See also
|
|
436
|
+
========
|
|
437
|
+
|
|
438
|
+
- :doc:`../how-to/track-energy` - Monitoring energy and power
|
|
439
|
+
- :doc:`../reference/protocol/data_conversions` - Protocol field conversions
|