nwp500-python 9.3.0__tar.gz → 9.3.1__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.3.0 → nwp500_python-9.3.1}/CHANGELOG.rst +99 -0
- {nwp500_python-9.3.0/src/nwp500_python.egg-info → nwp500_python-9.3.1}/PKG-INFO +1 -1
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/how-to/optimize-tou.rst +72 -57
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/openapi.yaml +31 -1
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/python_api/models.rst +52 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/python_api/mqtt_client.rst +4 -11
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/firmware_payload_capture.py +18 -14
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/tou_schedule.py +4 -4
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/__init__.py +4 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/cli/__main__.py +74 -15
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/cli/handlers.py +9 -5
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/models/__init__.py +4 -0
- nwp500_python-9.3.1/src/nwp500/models/device.py +101 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/models/feature.py +6 -1
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/models/tou.py +6 -5
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/_control_commands.py +0 -8
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/_device_subscriptions.py +6 -5
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/control.py +0 -32
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/subscriptions.py +7 -6
- {nwp500_python-9.3.0 → nwp500_python-9.3.1/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500_python.egg-info/SOURCES.txt +1 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500_python.egg-info/scm_file_list.json +175 -174
- nwp500_python-9.3.1/src/nwp500_python.egg-info/scm_version.json +8 -0
- nwp500_python-9.3.1/tests/test_cli_commands.py +441 -0
- nwp500_python-9.3.1/tests/test_device_rest_models.py +109 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_models.py +49 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_public_api.py +13 -0
- nwp500_python-9.3.0/src/nwp500/models/device.py +0 -57
- nwp500_python-9.3.0/src/nwp500_python.egg-info/scm_version.json +0 -8
- nwp500_python-9.3.0/tests/test_cli_commands.py +0 -220
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/.coveragerc +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/.github/workflows/ci.yml +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/.gitignore +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/.readthedocs.yml +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/AUTHORS.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/LICENSE.txt +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/Makefile +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/README.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/RELEASE.md +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/Makefile +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/conf.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/explanation/advanced-features.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/explanation/index.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/explanation/tank-energy.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/explanation/unknown-values.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/how-to/maintenance.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/how-to/manage-units.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/how-to/schedule-operation.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/how-to/track-energy.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/index.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/project/authors.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/project/changelog.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/project/history.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/project/license.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/index.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/installation.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/protocol/data_conversions.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/protocol/device_status.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/protocol/quick_reference.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/python_api/cli.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/python_api/events.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/requirements.txt +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/.ruff.toml +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/README.md +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/air_filter_reset.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/auto_recovery.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/combined_callbacks.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/demand_response.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/device_capabilities.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/device_status_debug.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/energy_analytics.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/mqtt_diagnostics.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/power_control.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/recirculation_control.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/reconnection_demo.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/simple_auto_recovery.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/token_restoration.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/advanced/water_reservation.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/beginner/01_authentication.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/beginner/02_list_devices.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/beginner/04_set_temperature.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/intermediate/advanced_auth_patterns.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/intermediate/command_queue.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/intermediate/device_status_callback.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/intermediate/event_driven_control.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/intermediate/periodic_requests.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/intermediate/set_mode.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/intermediate/vacation_mode.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/mask.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/testing/periodic_device_info.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/testing/test_api_client.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/testing/test_mqtt_connection.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/testing/test_mqtt_messaging.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/examples/testing/test_periodic_minimal.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/pyproject.toml +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/scripts/README.md +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/scripts/bump_version.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/scripts/format.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/scripts/lint.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/scripts/validate_version.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/setup.cfg +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/setup.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/cli/output_formatters.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/cli/presentation.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/cli/rich_output.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/config.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/converters.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/device_capabilities.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/encoding.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/enums.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/events.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/models/energy.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/models/schedule.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/models/status.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/__init__.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/client.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/command_queue.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/connection.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/state_tracker.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/types.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt/utils.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/mqtt_events.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/reservations.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/tou_schedule.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/conftest.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_auth.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_canonical_schedule.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_cli_tristate_presentation.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_command_queue.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_device_capabilities.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_events.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_model_converters.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_mqtt_events.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_mqtt_reliability.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_multi_device.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_openei.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_protocol_correctness.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_reservations.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_tou_schedule.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_unit_system_process_wide.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tests/test_utils.py +0 -0
- {nwp500_python-9.3.0 → nwp500_python-9.3.1}/tox.ini +0 -0
|
@@ -5,6 +5,105 @@ Changelog
|
|
|
5
5
|
Unreleased
|
|
6
6
|
==========
|
|
7
7
|
|
|
8
|
+
Version 9.3.1 (2026-08-29)
|
|
9
|
+
==========================
|
|
10
|
+
|
|
11
|
+
``NavienMqttClient.request_tou_settings()`` is removed. Not a breaking
|
|
12
|
+
change in practice: the device never answered it, so no caller can have
|
|
13
|
+
been getting a schedule out of it. Code that still names the method will
|
|
14
|
+
now fail with ``AttributeError`` rather than time out. See Removed below.
|
|
15
|
+
|
|
16
|
+
Added
|
|
17
|
+
-----
|
|
18
|
+
- **REST fields the cloud added since the models were written.**
|
|
19
|
+
``/device/list`` now returns an ``error`` block (``errorCode``,
|
|
20
|
+
``errorOccuredTime``) and the ``descaling`` block previously seen only
|
|
21
|
+
on ``/device/info``, and ``deviceInfo`` gained ``modelTypeCode`` and
|
|
22
|
+
``installerId``. None of these were modelled, and ``NavienBaseModel``
|
|
23
|
+
ignores unknown keys, so all of them were silently discarded.
|
|
24
|
+
:class:`~nwp500.models.Device` gains optional ``error``
|
|
25
|
+
(:class:`~nwp500.models.DeviceErrorSummary`) and ``descaling``
|
|
26
|
+
(:class:`~nwp500.models.DescalingInfo`) sections, and
|
|
27
|
+
:class:`~nwp500.models.DeviceInfo` gains ``model_type_code`` and
|
|
28
|
+
``installer_id``. ``error`` makes the device's last recorded fault
|
|
29
|
+
readable without an MQTT connection, including while the device is
|
|
30
|
+
offline. Every new field is optional, so responses that omit them - such
|
|
31
|
+
as ``/device/info``, which carries no ``error`` block - parse unchanged.
|
|
32
|
+
``error_code`` is typed ``ErrorCode | int`` (validated left to right, so a
|
|
33
|
+
known code becomes an ``ErrorCode`` member and only an unknown one stays a
|
|
34
|
+
plain int) so an unrecognised code cannot make a whole listing unparseable. ``docs/openapi.yaml`` is
|
|
35
|
+
updated to match.
|
|
36
|
+
|
|
37
|
+
Removed
|
|
38
|
+
-------
|
|
39
|
+
- **``request_tou_settings()`` removed - the device has no MQTT read for its
|
|
40
|
+
TOU schedule.** The method published a ``CommandCode.TOU_RESERVATION``
|
|
41
|
+
message to ``ctrl/tou/rd`` carrying only ``controllerSerialNumber``, then
|
|
42
|
+
waited for a reply on ``res/tou/rd``. No reply ever comes: a live device
|
|
43
|
+
with TOU provisioned (``program_reservation_use`` true, a valid controller
|
|
44
|
+
serial, and a plan the REST API returns in full) stayed silent for 45
|
|
45
|
+
seconds. ``ctrl/tou/rd`` with that command code is the *write* - it is what
|
|
46
|
+
:meth:`~nwp500.NavienMqttClient.configure_tou_schedule` publishes and what
|
|
47
|
+
the vendor app publishes from its TOU editor
|
|
48
|
+
(``TouScheduleViewmodel.setPublishMgppControlTou``) - and the device answers
|
|
49
|
+
on ``res/tou/rd`` only to confirm such a write. The vendor app reads TOU
|
|
50
|
+
over REST, which is the only TOU read the protocol has.
|
|
51
|
+
|
|
52
|
+
Beyond returning nothing, the call published a write-shaped command with no
|
|
53
|
+
schedule attached. This device ignored it - ``touStatus`` was unchanged
|
|
54
|
+
across repeated calls - but a firmware that took it at face value could read
|
|
55
|
+
it as "store an empty TOU schedule".
|
|
56
|
+
|
|
57
|
+
**Migration**: use :meth:`~nwp500.NavienAPIClient.get_tou_info`, which
|
|
58
|
+
returns the stored plan - rate name, utility, ZIP code and the seasonal
|
|
59
|
+
pricing intervals. The read itself is pure REST; it is keyed by the
|
|
60
|
+
controller serial number, which only the MQTT device-info response
|
|
61
|
+
publishes, so fetch that once and cache it.
|
|
62
|
+
:meth:`~nwp500.NavienMqttClient.subscribe_tou_response` is unaffected and
|
|
63
|
+
still delivers write confirmations. Enabling and disabling TOU
|
|
64
|
+
(:meth:`~nwp500.NavienMqttClient.set_tou_enabled`, command codes
|
|
65
|
+
``TOU_ON``/``TOU_OFF``) is a separate path and is unaffected.
|
|
66
|
+
|
|
67
|
+
Fixed
|
|
68
|
+
-----
|
|
69
|
+
- **CLI ``energy --months`` no longer duplicates ``--month`` output.**
|
|
70
|
+
``handle_get_energy_request`` picked the view from the length of the
|
|
71
|
+
month list, so ``--months 5`` took the single-month branch and printed
|
|
72
|
+
the same daily breakdown as ``--month 5`` instead of the monthly
|
|
73
|
+
summary. The handler now takes an explicit ``daily`` flag set by the
|
|
74
|
+
option the user actually passed. ``energy`` also rejects ``--month``
|
|
75
|
+
and ``--months`` together rather than silently preferring ``--month``,
|
|
76
|
+
and validates the ``--months`` range and format the way ``--month``
|
|
77
|
+
already did. All of that validation now happens while Click parses the
|
|
78
|
+
arguments, so a bad invocation is a usage error (exit code 2) before any
|
|
79
|
+
authentication or MQTT connection is attempted - previously the command
|
|
80
|
+
body raised ``ClickException`` after connecting, where ``async_command``'s
|
|
81
|
+
catch-all reported it as an "Unexpected Error" with a traceback, and
|
|
82
|
+
anyone without working credentials saw an authentication failure instead
|
|
83
|
+
of the usage error. That catch-all now re-raises ``ClickException`` so any
|
|
84
|
+
command can report a usage error as itself.
|
|
85
|
+
- **``DeviceFeature.mixing_valve_use`` now reads the device's actual key.**
|
|
86
|
+
The field was aliased to ``mixingValveUse``, but the device sends
|
|
87
|
+
``mixingValueUse`` - "Value", not "Valve" - and the base model ignores
|
|
88
|
+
unknown keys, so the field silently reported its ``False`` default on
|
|
89
|
+
every device regardless of whether a thermostatic mixing valve was
|
|
90
|
+
supported. The CLI's "Mixing Valve" capability row was wrong for the
|
|
91
|
+
same reason. The misspelling is Navien's own: it appears in the vendor
|
|
92
|
+
app's ``KDResponseMgppDid$Feature`` class and in
|
|
93
|
+
``docs/reference/protocol/device_features.rst``, which documented the
|
|
94
|
+
wire spelling correctly all along.
|
|
95
|
+
|
|
96
|
+
- **``firmware_payload_capture.py`` captured almost nothing.** Its wildcards
|
|
97
|
+
covered ``cmd/{type}/{client_id}/res/#`` and the event topic, but the device
|
|
98
|
+
acknowledges control and query commands on
|
|
99
|
+
``cmd/{type}/navilink-{mac}/{client_id}/res`` - which neither pattern
|
|
100
|
+
matches - so the device status and device info responses were missed, along
|
|
101
|
+
with every request published on the device path. A capture run recorded 2
|
|
102
|
+
payloads where it should have recorded 11. It now also subscribes to
|
|
103
|
+
``cmd/{type}/navilink-{mac}/#``, which additionally picks up traffic from
|
|
104
|
+
other clients on the same device. Query results the device routes back to a
|
|
105
|
+
client-keyed topic, such as reservations and energy usage, were already
|
|
106
|
+
covered by the existing response wildcard.
|
|
8
107
|
Version 9.3.0 (2026-08-03)
|
|
9
108
|
==========================
|
|
10
109
|
|
|
@@ -339,28 +339,38 @@ Enables or disables TOU operation without changing the schedule.
|
|
|
339
339
|
* ``device``: Device object
|
|
340
340
|
* ``enabled``: ``True`` to enable TOU, ``False`` to disable
|
|
341
341
|
|
|
342
|
-
|
|
343
|
-
|
|
342
|
+
REST: Read the Current TOU Schedule
|
|
343
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
344
344
|
|
|
345
345
|
.. code-block:: python
|
|
346
346
|
|
|
347
|
-
async def
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
347
|
+
async def get_tou_info(
|
|
348
|
+
mac_address: str,
|
|
349
|
+
additional_value: str,
|
|
350
|
+
controller_id: str,
|
|
351
|
+
user_type: str = "O",
|
|
352
|
+
) -> TOUInfo
|
|
351
353
|
|
|
352
|
-
|
|
354
|
+
Returns the stored TOU plan: rate name, utility, ZIP code, and the seasonal
|
|
355
|
+
schedule with its pricing intervals.
|
|
353
356
|
|
|
354
357
|
**Parameters:**
|
|
355
358
|
|
|
356
|
-
* ``
|
|
357
|
-
* ``
|
|
359
|
+
* ``mac_address``: Device MAC address
|
|
360
|
+
* ``additional_value``: Additional device identifier
|
|
361
|
+
* ``controller_id``: Controller serial number. Only the MQTT device-info
|
|
362
|
+
response publishes it (``DeviceFeature.controller_serial_number``); it is a
|
|
363
|
+
hardware property, so fetch it once and cache it. See Example 3.
|
|
364
|
+
* ``user_type``: User type (default ``"O"``)
|
|
358
365
|
|
|
359
|
-
|
|
366
|
+
.. note::
|
|
360
367
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
368
|
+
There is no MQTT read for the TOU schedule. ``ctrl/tou/rd`` with
|
|
369
|
+
``CommandCode.TOU_RESERVATION`` is the *write* - it is what
|
|
370
|
+
:meth:`~nwp500.NavienMqttClient.configure_tou_schedule` publishes, and what
|
|
371
|
+
the vendor app publishes from its TOU editor. The device replies on
|
|
372
|
+
``cmd/{deviceType}/{clientId}/res/tou/rd`` to confirm such a write; it does
|
|
373
|
+
not answer a request that carries no schedule. Read the plan over REST.
|
|
364
374
|
|
|
365
375
|
Building TOU Periods
|
|
366
376
|
--------------------
|
|
@@ -527,7 +537,7 @@ Configure two rate periods - off-peak and peak pricing:
|
|
|
527
537
|
await mqtt_client.subscribe_device_feature(device, capture_feature)
|
|
528
538
|
await mqtt_client.request_device_info(device)
|
|
529
539
|
feature = await asyncio.wait_for(feature_future, timeout=15)
|
|
530
|
-
controller_serial = feature.
|
|
540
|
+
controller_serial = feature.controller_serial_number
|
|
531
541
|
|
|
532
542
|
# Define off-peak period (midnight to 2 PM, weekdays)
|
|
533
543
|
off_peak = build_tou_period(
|
|
@@ -651,58 +661,63 @@ Configure different rates for summer and winter:
|
|
|
651
661
|
Example 3: Retrieve Current TOU Settings
|
|
652
662
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
653
663
|
|
|
654
|
-
|
|
664
|
+
The stored TOU plan is read over the REST API. The read itself needs no MQTT
|
|
665
|
+
connection, but it is keyed by the controller serial number, and the only
|
|
666
|
+
place that is published is the MQTT device-info response. The serial is a
|
|
667
|
+
hardware property that never changes, so fetch it once, keep it, and every
|
|
668
|
+
later read is pure REST:
|
|
655
669
|
|
|
656
670
|
.. code-block:: python
|
|
657
671
|
|
|
658
|
-
|
|
672
|
+
import asyncio
|
|
673
|
+
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
674
|
+
from nwp500.encoding import decode_price, decode_week_bitfield
|
|
675
|
+
|
|
676
|
+
async def get_controller_serial(auth_client, device) -> str:
|
|
677
|
+
"""One-time lookup: the controller serial is only published over MQTT."""
|
|
678
|
+
mqtt_client = NavienMqttClient(auth_client)
|
|
679
|
+
await mqtt_client.connect()
|
|
680
|
+
try:
|
|
681
|
+
feature_future = asyncio.Future()
|
|
682
|
+
|
|
683
|
+
def capture_feature(feature):
|
|
684
|
+
if not feature_future.done():
|
|
685
|
+
feature_future.set_result(feature)
|
|
686
|
+
|
|
687
|
+
await mqtt_client.subscribe_device_feature(device, capture_feature)
|
|
688
|
+
await mqtt_client.request_device_info(device)
|
|
689
|
+
feature = await asyncio.wait_for(feature_future, timeout=15)
|
|
690
|
+
return feature.controller_serial_number
|
|
691
|
+
finally:
|
|
692
|
+
await mqtt_client.disconnect()
|
|
659
693
|
|
|
660
|
-
async def check_tou_settings():
|
|
694
|
+
async def check_tou_settings(controller_serial: str | None = None):
|
|
661
695
|
async with NavienAuthClient("user@example.com", "password") as auth_client:
|
|
662
696
|
api_client = NavienAPIClient(auth_client=auth_client)
|
|
663
697
|
device = await api_client.get_first_device()
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
for i, period in enumerate(periods, 1):
|
|
682
|
-
days = decode_week_bitfield(period.get("week", 0))
|
|
683
|
-
price_min = decode_price(
|
|
684
|
-
period.get("priceMin", 0),
|
|
685
|
-
period.get("decimalPoint", 0)
|
|
686
|
-
)
|
|
687
|
-
price_max = decode_price(
|
|
688
|
-
period.get("priceMax", 0),
|
|
689
|
-
period.get("decimalPoint", 0)
|
|
690
|
-
)
|
|
691
|
-
|
|
698
|
+
|
|
699
|
+
if controller_serial is None:
|
|
700
|
+
controller_serial = await get_controller_serial(auth_client, device)
|
|
701
|
+
|
|
702
|
+
tou_info = await api_client.get_tou_info(
|
|
703
|
+
mac_address=device.device_info.mac_address,
|
|
704
|
+
additional_value=device.device_info.additional_value,
|
|
705
|
+
controller_id=controller_serial,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
print(f"Plan: {tou_info.name} ({tou_info.utility})")
|
|
709
|
+
|
|
710
|
+
# TOUSchedule.intervals holds the raw interval dicts
|
|
711
|
+
for season in tou_info.schedule:
|
|
712
|
+
for i, interval in enumerate(season.intervals, 1):
|
|
713
|
+
days = decode_week_bitfield(interval["week"])
|
|
714
|
+
dp = interval["decimalPoint"]
|
|
692
715
|
print(f"\nPeriod {i}:")
|
|
693
716
|
print(f" Days: {', '.join(days)}")
|
|
694
|
-
print(f" Time: {
|
|
695
|
-
f"- {
|
|
696
|
-
print(f" Price: ${
|
|
697
|
-
|
|
698
|
-
await mqtt_client.subscribe(response_topic, on_tou_response)
|
|
699
|
-
|
|
700
|
-
# Request current settings
|
|
701
|
-
await mqtt_client.request_tou_settings(device, controller_serial)
|
|
702
|
-
|
|
703
|
-
# Wait for response
|
|
704
|
-
await asyncio.sleep(5)
|
|
705
|
-
await mqtt_client.disconnect()
|
|
717
|
+
print(f" Time: {interval['startHour']:02d}:{interval['startMinute']:02d} "
|
|
718
|
+
f"- {interval['endHour']:02d}:{interval['endMinute']:02d}")
|
|
719
|
+
print(f" Price: ${decode_price(interval['priceMin'], dp):.5f} "
|
|
720
|
+
f"- ${decode_price(interval['priceMax'], dp):.5f}/kWh")
|
|
706
721
|
|
|
707
722
|
asyncio.run(check_tou_settings())
|
|
708
723
|
|
|
@@ -195,6 +195,14 @@ paths:
|
|
|
195
195
|
connected:
|
|
196
196
|
type: integer
|
|
197
197
|
example: 2
|
|
198
|
+
modelTypeCode:
|
|
199
|
+
type: integer
|
|
200
|
+
nullable: true
|
|
201
|
+
example: null
|
|
202
|
+
installerId:
|
|
203
|
+
type: string
|
|
204
|
+
nullable: true
|
|
205
|
+
example: null
|
|
198
206
|
location:
|
|
199
207
|
type: object
|
|
200
208
|
properties:
|
|
@@ -206,7 +214,29 @@ paths:
|
|
|
206
214
|
example: "Anytown"
|
|
207
215
|
address:
|
|
208
216
|
type: string
|
|
209
|
-
example: "123 Main Street"
|
|
217
|
+
example: "123 Main Street"
|
|
218
|
+
error:
|
|
219
|
+
type: object
|
|
220
|
+
description: Last recorded device fault.
|
|
221
|
+
properties:
|
|
222
|
+
errorCode:
|
|
223
|
+
type: integer
|
|
224
|
+
example: 0
|
|
225
|
+
errorOccuredTime:
|
|
226
|
+
type: string
|
|
227
|
+
nullable: true
|
|
228
|
+
example: "2025-12-07T11:58:02"
|
|
229
|
+
descaling:
|
|
230
|
+
type: object
|
|
231
|
+
properties:
|
|
232
|
+
descalingStartTime:
|
|
233
|
+
type: string
|
|
234
|
+
nullable: true
|
|
235
|
+
example: null
|
|
236
|
+
descalingEndTime:
|
|
237
|
+
type: string
|
|
238
|
+
nullable: true
|
|
239
|
+
example: null
|
|
210
240
|
/device/info:
|
|
211
241
|
post:
|
|
212
242
|
summary: Device Info
|
|
@@ -60,6 +60,10 @@ Complete device representation with info and location.
|
|
|
60
60
|
|
|
61
61
|
* ``device_info`` (DeviceInfo) - Device identification and status
|
|
62
62
|
* ``location`` (Location) - Physical location information
|
|
63
|
+
* ``error`` (DeviceErrorSummary, optional) - Last recorded fault. Returned by
|
|
64
|
+
``/device/list`` only; ``None`` on a device fetched through ``/device/info``.
|
|
65
|
+
* ``descaling`` (DescalingInfo, optional) - Descaling window, if one is
|
|
66
|
+
scheduled or recorded
|
|
63
67
|
|
|
64
68
|
**Example:**
|
|
65
69
|
|
|
@@ -80,6 +84,52 @@ Complete device representation with info and location.
|
|
|
80
84
|
print(f"Location: {loc.city}, {loc.state}")
|
|
81
85
|
print(f"Coords: {loc.latitude}, {loc.longitude}")
|
|
82
86
|
|
|
87
|
+
DeviceErrorSummary
|
|
88
|
+
------------------
|
|
89
|
+
|
|
90
|
+
The device's last recorded fault, as reported by the REST API. Unlike
|
|
91
|
+
``DeviceStatus.error_code`` this is readable without an MQTT connection, and
|
|
92
|
+
remains readable while the device is offline.
|
|
93
|
+
|
|
94
|
+
.. py:class:: DeviceErrorSummary
|
|
95
|
+
|
|
96
|
+
**Fields:**
|
|
97
|
+
|
|
98
|
+
* ``error_code`` (ErrorCode | int) - ``ErrorCode.NO_ERROR`` when there is no
|
|
99
|
+
recorded fault. A code the enum does not know is kept as a plain int rather
|
|
100
|
+
than failing the whole response.
|
|
101
|
+
* ``error_occurred_time`` (str, optional) - When the fault was recorded, as an
|
|
102
|
+
ISO-8601 string. Sent by the API under the misspelled key ``errorOccuredTime``.
|
|
103
|
+
|
|
104
|
+
**Example:**
|
|
105
|
+
|
|
106
|
+
.. code-block:: python
|
|
107
|
+
|
|
108
|
+
devices = await api.list_devices()
|
|
109
|
+
|
|
110
|
+
for device in devices:
|
|
111
|
+
if device.error and device.error.error_code != ErrorCode.NO_ERROR:
|
|
112
|
+
code = device.error.error_code
|
|
113
|
+
# A code the enum knows arrives as an ErrorCode; anything else
|
|
114
|
+
# falls back to a plain int, which has no .name.
|
|
115
|
+
label = code.name if isinstance(code, ErrorCode) else f"code {code}"
|
|
116
|
+
print(f"{device.device_info.device_name}: {label}"
|
|
117
|
+
f" at {device.error.error_occurred_time}")
|
|
118
|
+
|
|
119
|
+
DescalingInfo
|
|
120
|
+
-------------
|
|
121
|
+
|
|
122
|
+
Descaling window reported by the REST API.
|
|
123
|
+
|
|
124
|
+
.. py:class:: DescalingInfo
|
|
125
|
+
|
|
126
|
+
**Fields:**
|
|
127
|
+
|
|
128
|
+
* ``descaling_start_time`` (str, optional) - Start of the descaling window
|
|
129
|
+
* ``descaling_end_time`` (str, optional) - End of the descaling window
|
|
130
|
+
|
|
131
|
+
Both are ``None`` on a device with no descaling scheduled or recorded.
|
|
132
|
+
|
|
83
133
|
DeviceInfo
|
|
84
134
|
----------
|
|
85
135
|
|
|
@@ -99,6 +149,8 @@ Device identification and connection information.
|
|
|
99
149
|
* ``device_name`` (str) - User-assigned device name
|
|
100
150
|
* ``connected`` (int) - Connection status (2 = online, 0 = offline)
|
|
101
151
|
* ``install_type`` (str, optional) - Installation type
|
|
152
|
+
* ``model_type_code`` (int, optional) - Model type code
|
|
153
|
+
* ``installer_id`` (str, optional) - Installer identifier
|
|
102
154
|
|
|
103
155
|
**Example:**
|
|
104
156
|
|
|
@@ -582,24 +582,17 @@ configure_tou_schedule()
|
|
|
582
582
|
|
|
583
583
|
**Capability Required:** ``program_reservation_use``
|
|
584
584
|
|
|
585
|
-
request_tou_settings()
|
|
586
|
-
^^^^^^^^^^^^^^^^^^^^^^
|
|
587
|
-
|
|
588
|
-
.. py:method:: request_tou_settings(device, controller_serial_number)
|
|
589
|
-
|
|
590
|
-
Request the current TOU schedule.
|
|
591
|
-
|
|
592
585
|
subscribe_tou_response()
|
|
593
586
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
594
587
|
|
|
595
588
|
.. py:method:: subscribe_tou_response(device, callback)
|
|
596
589
|
|
|
597
|
-
Subscribe to parsed TOU schedule
|
|
590
|
+
Subscribe to parsed TOU schedule write confirmations.
|
|
598
591
|
|
|
599
592
|
The callback is invoked with a :class:`~nwp500.models.TOUReservationSchedule`
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
593
|
+
when the device confirms a :meth:`configure_tou_schedule` write on the
|
|
594
|
+
``tou/rd`` response topic. The device has no MQTT read for its TOU schedule;
|
|
595
|
+
to read the stored plan, use :meth:`~nwp500.NavienAPIClient.get_tou_info`.
|
|
603
596
|
|
|
604
597
|
:param callback: Called with the parsed TOU schedule on each response.
|
|
605
598
|
:type callback: Callable[[TOUReservationSchedule], None]
|
|
@@ -8,7 +8,6 @@ updates by diffing captures taken before and after an update.
|
|
|
8
8
|
|
|
9
9
|
Specifically captures:
|
|
10
10
|
- Weekly reservations (rsv/rd)
|
|
11
|
-
- Time-of-Use schedule (tou/rd)
|
|
12
11
|
- Device info (firmware versions, capabilities)
|
|
13
12
|
- Device status (current operating state)
|
|
14
13
|
- All other response/event topics (via wildcards)
|
|
@@ -37,7 +36,6 @@ from pathlib import Path
|
|
|
37
36
|
from typing import Any
|
|
38
37
|
|
|
39
38
|
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
40
|
-
from nwp500.exceptions import Nwp500Error
|
|
41
39
|
from nwp500.models import DeviceFeature
|
|
42
40
|
from nwp500.mqtt.utils import redact, redact_topic
|
|
43
41
|
from nwp500.topic_builder import MqttTopicBuilder
|
|
@@ -113,17 +111,30 @@ async def main() -> None:
|
|
|
113
111
|
|
|
114
112
|
# --- Wildcard subscriptions to catch everything ---
|
|
115
113
|
|
|
116
|
-
#
|
|
114
|
+
# Everything under the device's own command path. The device
|
|
115
|
+
# acknowledges control and query commands on
|
|
116
|
+
# cmd/{type}/navilink-{mac}/{client_id}/res, which is where the status
|
|
117
|
+
# and device-info responses arrive, so a capture without this
|
|
118
|
+
# subscription misses them entirely. It also picks up the published
|
|
119
|
+
# requests themselves and the traffic of other clients on the same
|
|
120
|
+
# device (a Home Assistant integration, the vendor app), which is
|
|
121
|
+
# exactly what a capture wants. Query results that the device routes
|
|
122
|
+
# back to a client-keyed topic - reservations, energy usage - arrive
|
|
123
|
+
# under res_wildcard below instead.
|
|
124
|
+
cmd_wildcard = MqttTopicBuilder.command_topic(device_type, mac, "#")
|
|
125
|
+
# Query results routed back to this client specifically
|
|
117
126
|
res_wildcard = MqttTopicBuilder.response_topic(device_type, client_id, "#")
|
|
118
127
|
# All event messages pushed by the device
|
|
119
128
|
evt_wildcard = MqttTopicBuilder.event_topic(device_type, mac, "#")
|
|
120
129
|
|
|
121
130
|
print(
|
|
122
|
-
f"\nSubscribing to:\n {redact_topic(
|
|
131
|
+
f"\nSubscribing to:\n {redact_topic(cmd_wildcard)}\n"
|
|
132
|
+
f" {redact_topic(res_wildcard)}\n"
|
|
123
133
|
f" {redact_topic(evt_wildcard)}\n"
|
|
124
134
|
)
|
|
125
135
|
print("Captured topics:")
|
|
126
136
|
|
|
137
|
+
await mqtt_client.subscribe(cmd_wildcard, capture.record)
|
|
127
138
|
await mqtt_client.subscribe(res_wildcard, capture.record)
|
|
128
139
|
await mqtt_client.subscribe(evt_wildcard, capture.record)
|
|
129
140
|
|
|
@@ -156,16 +167,9 @@ async def main() -> None:
|
|
|
156
167
|
await mqtt_client.request_reservations(device)
|
|
157
168
|
await asyncio.sleep(5)
|
|
158
169
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if serial:
|
|
163
|
-
print("Requesting TOU schedule...")
|
|
164
|
-
try:
|
|
165
|
-
await mqtt_client.request_tou_settings(device, serial)
|
|
166
|
-
await asyncio.sleep(5)
|
|
167
|
-
except Nwp500Error as exc:
|
|
168
|
-
print(f" TOU request failed: {exc}")
|
|
170
|
+
# No TOU step: the device has no MQTT read for its TOU schedule.
|
|
171
|
+
# ctrl/tou/rd carries the *write*, which a capture tool must not send.
|
|
172
|
+
# Read the schedule over REST instead (api_client.get_tou_info).
|
|
169
173
|
|
|
170
174
|
# --- Step 5: wait a bit more to catch any late-arriving messages ---
|
|
171
175
|
print("\nWaiting for any remaining messages...")
|
|
@@ -119,10 +119,10 @@ async def main() -> None:
|
|
|
119
119
|
enabled=True,
|
|
120
120
|
)
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
print("Waiting up to 15 seconds for TOU
|
|
122
|
+
# The device has no MQTT read for its TOU schedule; the write above
|
|
123
|
+
# is confirmed on the same tou/rd topic. To read the stored schedule
|
|
124
|
+
# back, use the REST API: api_client.get_tou_info(...).
|
|
125
|
+
print("Waiting up to 15 seconds for the TOU write confirmation...")
|
|
126
126
|
await asyncio.sleep(15)
|
|
127
127
|
|
|
128
128
|
print("Toggling TOU off for quick test...")
|
|
@@ -75,7 +75,9 @@ from nwp500.factory import (
|
|
|
75
75
|
)
|
|
76
76
|
from nwp500.models import (
|
|
77
77
|
ConvertedTOUPlan,
|
|
78
|
+
DescalingInfo,
|
|
78
79
|
Device,
|
|
80
|
+
DeviceErrorSummary,
|
|
79
81
|
DeviceFeature,
|
|
80
82
|
DeviceInfo,
|
|
81
83
|
DeviceStatus,
|
|
@@ -147,6 +149,8 @@ __all__ = [
|
|
|
147
149
|
"DeviceInfo",
|
|
148
150
|
"Location",
|
|
149
151
|
"Device",
|
|
152
|
+
"DeviceErrorSummary",
|
|
153
|
+
"DescalingInfo",
|
|
150
154
|
"FirmwareInfo",
|
|
151
155
|
"ReservationEntry",
|
|
152
156
|
"ReservationSchedule",
|
|
@@ -143,6 +143,11 @@ def async_command(f: Any) -> Any:
|
|
|
143
143
|
await mqtt.disconnect()
|
|
144
144
|
return 0
|
|
145
145
|
|
|
146
|
+
except click.ClickException:
|
|
147
|
+
# A usage/config error a command raised deliberately. Let
|
|
148
|
+
# Click report it as itself; the catch-all below would
|
|
149
|
+
# otherwise log it as an "Unexpected Error" with a traceback.
|
|
150
|
+
raise
|
|
146
151
|
except (
|
|
147
152
|
InvalidCredentialsError,
|
|
148
153
|
AuthenticationError,
|
|
@@ -639,14 +644,69 @@ async def tou_apply(
|
|
|
639
644
|
_logger.error("API client not available")
|
|
640
645
|
|
|
641
646
|
|
|
642
|
-
|
|
647
|
+
def _parse_months(
|
|
648
|
+
ctx: click.Context, param: click.Parameter, value: str | None
|
|
649
|
+
) -> list[int] | None:
|
|
650
|
+
"""Parse ``--months`` at parse time, so bad input is a usage error."""
|
|
651
|
+
if value is None:
|
|
652
|
+
return None
|
|
653
|
+
months = []
|
|
654
|
+
for raw in value.split(","):
|
|
655
|
+
try:
|
|
656
|
+
month = int(raw.strip())
|
|
657
|
+
except ValueError:
|
|
658
|
+
raise click.BadParameter(
|
|
659
|
+
f"{raw.strip()!r} is not a month number; expected a "
|
|
660
|
+
"comma-separated list like 1,2,3"
|
|
661
|
+
) from None
|
|
662
|
+
if not 1 <= month <= 12:
|
|
663
|
+
raise click.BadParameter(f"{month} is not in the range 1-12")
|
|
664
|
+
months.append(month)
|
|
665
|
+
if not months:
|
|
666
|
+
raise click.BadParameter("at least one month is required")
|
|
667
|
+
return months
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
class _EnergySelection(click.Command):
|
|
671
|
+
"""Rejects an unusable --month/--months combination during parsing.
|
|
672
|
+
|
|
673
|
+
Cross-option checks have no natural home in a per-option callback, and
|
|
674
|
+
the command body is the wrong place: it runs inside ``async_command``,
|
|
675
|
+
after authentication and the MQTT connect, so a usage error there would
|
|
676
|
+
surface only to users whose credentials already work - and then as an
|
|
677
|
+
"Unexpected Error" traceback, because the wrapper's catch-all swallows
|
|
678
|
+
``ClickException``.
|
|
679
|
+
"""
|
|
680
|
+
|
|
681
|
+
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
|
|
682
|
+
rest = super().parse_args(ctx, args)
|
|
683
|
+
month, months = ctx.params.get("month"), ctx.params.get("months")
|
|
684
|
+
if month is not None and months is not None:
|
|
685
|
+
raise click.UsageError(
|
|
686
|
+
"Use either --month (daily breakdown) or --months "
|
|
687
|
+
"(monthly summary), not both",
|
|
688
|
+
ctx=ctx,
|
|
689
|
+
)
|
|
690
|
+
if month is None and months is None:
|
|
691
|
+
raise click.UsageError(
|
|
692
|
+
"Either --months (for monthly summary) or --month "
|
|
693
|
+
"(for daily breakdown) is required",
|
|
694
|
+
ctx=ctx,
|
|
695
|
+
)
|
|
696
|
+
return rest
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
@cli.command(cls=_EnergySelection) # type: ignore[attr-defined]
|
|
643
700
|
@click.option("--year", type=int, required=True, help="Year to query")
|
|
644
701
|
@click.option(
|
|
645
|
-
"--months",
|
|
702
|
+
"--months",
|
|
703
|
+
required=False,
|
|
704
|
+
callback=_parse_months,
|
|
705
|
+
help="Comma-separated months for a monthly summary (e.g. 1,2,3)",
|
|
646
706
|
)
|
|
647
707
|
@click.option(
|
|
648
708
|
"--month",
|
|
649
|
-
type=
|
|
709
|
+
type=click.IntRange(1, 12),
|
|
650
710
|
required=False,
|
|
651
711
|
help="Show daily breakdown for a specific month (1-12)",
|
|
652
712
|
)
|
|
@@ -655,26 +715,25 @@ async def energy(
|
|
|
655
715
|
mqtt: NavienMqttClient,
|
|
656
716
|
device: Any,
|
|
657
717
|
year: int,
|
|
658
|
-
months:
|
|
718
|
+
months: list[int] | None,
|
|
659
719
|
month: int | None,
|
|
660
720
|
) -> None:
|
|
661
721
|
"""Query historical energy usage.
|
|
662
722
|
|
|
663
723
|
Use either --months for monthly summary or --month for daily breakdown.
|
|
724
|
+
Which option was passed decides the view, not how many months it names:
|
|
725
|
+
``--months 5`` is a one-month summary, ``--month 5`` is a daily
|
|
726
|
+
breakdown of that month.
|
|
664
727
|
"""
|
|
728
|
+
# _EnergySelection rejected every combination but these two during
|
|
729
|
+
# parsing, so exactly one of the options is set here.
|
|
665
730
|
if month is not None:
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
await handlers.handle_get_energy_request(mqtt, device, year, [month])
|
|
731
|
+
await handlers.handle_get_energy_request(
|
|
732
|
+
mqtt, device, year, [month], daily=True
|
|
733
|
+
)
|
|
670
734
|
elif months is not None:
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
await handlers.handle_get_energy_request(mqtt, device, year, month_list)
|
|
674
|
-
else:
|
|
675
|
-
raise click.ClickException(
|
|
676
|
-
"Either --months (for monthly summary) or --month "
|
|
677
|
-
"(for daily breakdown) required"
|
|
735
|
+
await handlers.handle_get_energy_request(
|
|
736
|
+
mqtt, device, year, months, daily=False
|
|
678
737
|
)
|
|
679
738
|
|
|
680
739
|
|