nwp500-python 9.2.0__tar.gz → 9.2.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.2.0 → nwp500_python-9.2.1}/CHANGELOG.rst +59 -0
- {nwp500_python-9.2.0/src/nwp500_python.egg-info → nwp500_python-9.2.1}/PKG-INFO +1 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/air_filter_reset.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/auto_recovery.py +6 -5
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/combined_callbacks.py +3 -2
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/demand_response.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/device_capabilities.py +5 -7
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/device_status_debug.py +6 -8
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/energy_analytics.py +4 -3
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/firmware_payload_capture.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/mqtt_diagnostics.py +6 -6
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/power_control.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/recirculation_control.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/reconnection_demo.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/simple_auto_recovery.py +4 -3
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/token_restoration.py +5 -6
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/tou_schedule.py +1 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/water_reservation.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/beginner/01_authentication.py +3 -2
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/beginner/02_list_devices.py +11 -8
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/beginner/04_set_temperature.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/advanced_auth_patterns.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/command_queue.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/device_status_callback.py +5 -7
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/event_driven_control.py +4 -3
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/mqtt_realtime_monitoring.py +6 -8
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/periodic_requests.py +5 -4
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/set_mode.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/vacation_mode.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/mask.py +5 -2
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/testing/periodic_device_info.py +5 -4
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/testing/test_api_client.py +8 -8
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/testing/test_mqtt_connection.py +2 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/testing/test_mqtt_messaging.py +16 -13
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/testing/test_periodic_minimal.py +13 -10
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/__init__.py +7 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/device_capabilities.py +2 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/encoding.py +2 -2
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/schedule.py +31 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/tou.py +35 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/control.py +32 -2
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/reservations.py +77 -2
- nwp500_python-9.2.1/src/nwp500/tou_schedule.py +105 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/SOURCES.txt +3 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/scm_file_list.json +182 -179
- nwp500_python-9.2.1/src/nwp500_python.egg-info/scm_version.json +8 -0
- nwp500_python-9.2.1/tests/test_canonical_schedule.py +161 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_device_capabilities.py +31 -1
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_protocol_correctness.py +126 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_reservations.py +132 -0
- nwp500_python-9.2.1/tests/test_tou_schedule.py +192 -0
- nwp500_python-9.2.0/src/nwp500_python.egg-info/scm_version.json +0 -8
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.coveragerc +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.github/workflows/ci.yml +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.gitignore +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/.readthedocs.yml +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/AUTHORS.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/LICENSE.txt +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/Makefile +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/README.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/RELEASE.md +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/Makefile +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/conf.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/explanation/advanced-features.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/explanation/index.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/maintenance.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/manage-units.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/optimize-tou.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/schedule-operation.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/how-to/track-energy.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/index.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/openapi.yaml +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/project/authors.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/project/changelog.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/project/history.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/project/license.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/index.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/installation.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/data_conversions.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/device_status.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/quick_reference.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/cli.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/events.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/models.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/reference/python_api/mqtt_client.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/requirements.txt +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/.ruff.toml +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/README.md +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/pyproject.toml +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/README.md +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/bump_version.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/format.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/lint.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/scripts/validate_version.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/setup.cfg +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/setup.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/__main__.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/handlers.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/output_formatters.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/presentation.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/rich_output.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/config.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/converters.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/enums.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/events.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/__init__.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/device.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/energy.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/feature.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/models/status.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/__init__.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/_control_commands.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/client.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/command_queue.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/connection.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/state_tracker.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/subscriptions.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/types.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/utils.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/mqtt_events.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/conftest.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_auth.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_cli_commands.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_command_queue.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_events.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_model_converters.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_models.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_events.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_mqtt_reliability.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_multi_device.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_openei.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_public_api.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_unit_system_process_wide.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tests/test_utils.py +0 -0
- {nwp500_python-9.2.0 → nwp500_python-9.2.1}/tox.ini +0 -0
|
@@ -5,6 +5,65 @@ Changelog
|
|
|
5
5
|
Unreleased
|
|
6
6
|
==========
|
|
7
7
|
|
|
8
|
+
Version 9.2.1 (2026-07-30)
|
|
9
|
+
==========================
|
|
10
|
+
|
|
11
|
+
Added
|
|
12
|
+
-----
|
|
13
|
+
- **Confirmed reservation/TOU write helpers and canonical schedule
|
|
14
|
+
comparison** (`#111 <https://github.com/eman/nwp500-python/issues/111>`_):
|
|
15
|
+
``update_reservations`` and ``configure_tou_schedule`` were
|
|
16
|
+
fire-and-forget, returning only an MQTT publish packet id with no way to
|
|
17
|
+
confirm the device applied the write. New
|
|
18
|
+
``nwp500.reservations.update_reservations_confirmed()`` and
|
|
19
|
+
``nwp500.tou_schedule.configure_tou_schedule_confirmed()`` (also exported
|
|
20
|
+
from the top-level ``nwp500`` package) send the write and await the
|
|
21
|
+
device's ``rsv/rd``/``tou/rd`` echo, returning the parsed schedule the
|
|
22
|
+
device now holds (or ``None`` on timeout). Because the protocol has no
|
|
23
|
+
request/response correlation id on these topics, a response is only
|
|
24
|
+
accepted once it matches the content that was just written, so a
|
|
25
|
+
stale/unrelated response from a concurrent read or a previous write
|
|
26
|
+
can't be mistaken for confirmation. Also added
|
|
27
|
+
``ReservationEntry.canonical_key()``/``ReservationSchedule.canonical()``
|
|
28
|
+
and the ``TOUPeriod``/``TOUReservationSchedule`` equivalents: a stable,
|
|
29
|
+
order-independent, hashable representation of the raw protocol fields so
|
|
30
|
+
a desired program can be compared against a device read-back with
|
|
31
|
+
``desired.canonical() == confirmed.canonical()`` instead of hand-diffing.
|
|
32
|
+
|
|
33
|
+
Fixed
|
|
34
|
+
-----
|
|
35
|
+
- **Freeze protection temperature now validated against device limits**
|
|
36
|
+
(`#112 <https://github.com/eman/nwp500-python/issues/112>`_):
|
|
37
|
+
``set_freeze_protection_temperature`` documented a 35-45°F valid range
|
|
38
|
+
but sent the value straight through with no validation. It now fetches
|
|
39
|
+
device features and validates against
|
|
40
|
+
``freeze_protection_temp_min``/``freeze_protection_temp_max`` (raising
|
|
41
|
+
``RangeValidationError`` if out of range), matching the pattern already
|
|
42
|
+
used by ``set_dhw_temperature``. The command is also now gated behind a
|
|
43
|
+
new ``freeze_protection_use`` capability (added to
|
|
44
|
+
``MqttDeviceCapabilityChecker._CAPABILITY_MAP`` and enforced with
|
|
45
|
+
``@requires_capability``), so it's no longer dispatched to devices that
|
|
46
|
+
don't support freeze protection at all.
|
|
47
|
+
- **Demand response commands gated on the dr_setting_use capability**
|
|
48
|
+
(`#114 <https://github.com/eman/nwp500-python/issues/114>`_):
|
|
49
|
+
``enable_demand_response``/``disable_demand_response`` were dispatched
|
|
50
|
+
unconditionally even though the ``dr_setting_use`` capability flag
|
|
51
|
+
exists on ``DeviceFeature``. Every other controllable feature follows
|
|
52
|
+
flag + ``_CAPABILITY_MAP`` entry + ``@requires_capability`` decorator;
|
|
53
|
+
DR was the one gap. Added the missing map entry and decorators so DR
|
|
54
|
+
now follows the same pattern.
|
|
55
|
+
|
|
56
|
+
Documentation
|
|
57
|
+
-------------
|
|
58
|
+
- **decode_reservation_hex docstring corrected** (`#113
|
|
59
|
+
<https://github.com/eman/nwp500-python/issues/113>`_): the ``param``
|
|
60
|
+
byte was documented as "temperature offset by 20°F", contradicting the
|
|
61
|
+
half-degrees-Celsius encoding used consistently everywhere else
|
|
62
|
+
(``models/schedule.py``, ``models/_converters.py``). Corrected to
|
|
63
|
+
"target temperature, half-degrees Celsius; °C = param/2", and fixed the
|
|
64
|
+
accompanying doctest-style example, which showed a ``minute`` key where
|
|
65
|
+
the function actually returns ``min``.
|
|
66
|
+
|
|
8
67
|
Version 9.2.0 (2026-07-06)
|
|
9
68
|
==========================
|
|
10
69
|
|
|
@@ -8,7 +8,8 @@ after cleaning or replacing the filter on heat pump models.
|
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import logging
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
12
13
|
|
|
13
14
|
# Set up logging to see the air filter control process
|
|
14
15
|
logging.basicConfig(level=logging.INFO)
|
|
File without changes
|
|
@@ -21,6 +21,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
|
|
|
21
21
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
22
22
|
|
|
23
23
|
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
24
|
+
from nwp500.exceptions import Nwp500Error
|
|
24
25
|
from nwp500.mqtt import MqttConnectionConfig
|
|
25
26
|
|
|
26
27
|
# Configure logging
|
|
@@ -167,7 +168,7 @@ async def strategy_full_recreation(auth_client, device):
|
|
|
167
168
|
mqtt_client = await create_and_connect()
|
|
168
169
|
recovery_attempt = 0 # Reset on success
|
|
169
170
|
logger.info("Successfully recreated MQTT client")
|
|
170
|
-
except
|
|
171
|
+
except Nwp500Error as e:
|
|
171
172
|
logger.error(f"Failed to recreate MQTT client: {e}")
|
|
172
173
|
|
|
173
174
|
try:
|
|
@@ -250,7 +251,7 @@ async def strategy_token_refresh(auth_client, device):
|
|
|
250
251
|
|
|
251
252
|
recovery_attempt = 0 # Reset on success
|
|
252
253
|
|
|
253
|
-
except
|
|
254
|
+
except Nwp500Error as e:
|
|
254
255
|
logger.error(f"Failed to refresh and reconnect: {e}")
|
|
255
256
|
|
|
256
257
|
mqtt_client = NavienMqttClient(auth_client, config=config)
|
|
@@ -348,7 +349,7 @@ async def strategy_exponential_backoff(auth_client, device):
|
|
|
348
349
|
try:
|
|
349
350
|
if mqtt_client.is_connected:
|
|
350
351
|
await mqtt_client.disconnect()
|
|
351
|
-
except
|
|
352
|
+
except Nwp500Error as e:
|
|
352
353
|
logger.warning(f"Error disconnecting old client: {e}")
|
|
353
354
|
|
|
354
355
|
# Create new client
|
|
@@ -365,7 +366,7 @@ async def strategy_exponential_backoff(auth_client, device):
|
|
|
365
366
|
recovery_attempt = 0 # Reset on success
|
|
366
367
|
logger.info("All subscriptions restored")
|
|
367
368
|
|
|
368
|
-
except
|
|
369
|
+
except Nwp500Error as e:
|
|
369
370
|
logger.error(f"Recovery attempt failed: {e}")
|
|
370
371
|
|
|
371
372
|
mqtt_client = NavienMqttClient(auth_client, config=config)
|
|
@@ -442,7 +443,7 @@ if __name__ == "__main__":
|
|
|
442
443
|
asyncio.run(main())
|
|
443
444
|
except KeyboardInterrupt:
|
|
444
445
|
print("\n\n[WARNING] Interrupted by user")
|
|
445
|
-
except
|
|
446
|
+
except Nwp500Error as e:
|
|
446
447
|
print(f"\n[ERROR] Error: {e}")
|
|
447
448
|
import traceback
|
|
448
449
|
|
|
@@ -32,6 +32,7 @@ if __name__ == "__main__":
|
|
|
32
32
|
from nwp500.api_client import NavienAPIClient
|
|
33
33
|
from nwp500.auth import NavienAuthClient
|
|
34
34
|
from nwp500.enums import OnOffFlag
|
|
35
|
+
from nwp500.exceptions import Nwp500Error
|
|
35
36
|
from nwp500.models import DeviceFeature, DeviceStatus
|
|
36
37
|
from nwp500.mqtt import NavienMqttClient
|
|
37
38
|
|
|
@@ -157,7 +158,7 @@ async def main():
|
|
|
157
158
|
await mqtt_client.disconnect()
|
|
158
159
|
print("\n[SUCCESS] Disconnected")
|
|
159
160
|
|
|
160
|
-
except
|
|
161
|
+
except Nwp500Error as e:
|
|
161
162
|
print(f"[ERROR] Error: {e}")
|
|
162
163
|
if mqtt_client.is_connected:
|
|
163
164
|
await mqtt_client.disconnect()
|
|
@@ -165,7 +166,7 @@ async def main():
|
|
|
165
166
|
|
|
166
167
|
return 0
|
|
167
168
|
|
|
168
|
-
except
|
|
169
|
+
except Nwp500Error as e:
|
|
169
170
|
print(f"[ERROR] Error: {e}")
|
|
170
171
|
import traceback
|
|
171
172
|
|
|
@@ -8,7 +8,8 @@ to help utilities manage peak loads.
|
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import logging
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
12
13
|
|
|
13
14
|
# Set up logging to see the demand response control process
|
|
14
15
|
logging.basicConfig(level=logging.INFO)
|
|
@@ -26,6 +26,8 @@ logging.getLogger("nwp500.mqtt_client").setLevel(logging.INFO)
|
|
|
26
26
|
logging.getLogger("nwp500.auth").setLevel(logging.INFO)
|
|
27
27
|
logging.getLogger("nwp500.api_client").setLevel(logging.INFO)
|
|
28
28
|
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
29
31
|
# If running from examples directory, add parent to path
|
|
30
32
|
if __name__ == "__main__":
|
|
31
33
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
|
|
@@ -39,7 +41,7 @@ from nwp500.mqtt import NavienMqttClient
|
|
|
39
41
|
|
|
40
42
|
try:
|
|
41
43
|
from mask import mask_mac # type: ignore
|
|
42
|
-
except
|
|
44
|
+
except ImportError:
|
|
43
45
|
|
|
44
46
|
def mask_mac(mac): # pragma: no cover - fallback
|
|
45
47
|
return "[REDACTED_MAC]"
|
|
@@ -254,9 +256,7 @@ async def main():
|
|
|
254
256
|
print("[SUCCESS] Disconnected successfully")
|
|
255
257
|
|
|
256
258
|
except Exception:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
logging.exception("MQTT error in device_feature_callback")
|
|
259
|
+
logger.exception("MQTT error in device_feature_callback")
|
|
260
260
|
|
|
261
261
|
if mqtt_client.is_connected:
|
|
262
262
|
await mqtt_client.disconnect()
|
|
@@ -276,9 +276,7 @@ async def main():
|
|
|
276
276
|
return 1
|
|
277
277
|
|
|
278
278
|
except Exception:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
logging.exception("Unexpected error in device_feature_callback")
|
|
279
|
+
logger.exception("Unexpected error in device_feature_callback")
|
|
282
280
|
return 1
|
|
283
281
|
|
|
284
282
|
|
|
@@ -18,6 +18,8 @@ logging.basicConfig(
|
|
|
18
18
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
21
23
|
# If running from examples directory, add parent to path
|
|
22
24
|
if __name__ == "__main__":
|
|
23
25
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
|
|
@@ -31,7 +33,7 @@ from nwp500.mqtt import NavienMqttClient
|
|
|
31
33
|
|
|
32
34
|
try:
|
|
33
35
|
from mask import mask_mac # type: ignore
|
|
34
|
-
except
|
|
36
|
+
except ImportError:
|
|
35
37
|
|
|
36
38
|
def mask_mac(mac): # pragma: no cover - fallback
|
|
37
39
|
return "[REDACTED_MAC]"
|
|
@@ -79,7 +81,7 @@ async def main():
|
|
|
79
81
|
|
|
80
82
|
try:
|
|
81
83
|
from mask import mask_any # type: ignore
|
|
82
|
-
except
|
|
84
|
+
except ImportError:
|
|
83
85
|
|
|
84
86
|
def mask_any(_):
|
|
85
87
|
return "[REDACTED]"
|
|
@@ -177,9 +179,7 @@ async def main():
|
|
|
177
179
|
print("[SUCCESS] Disconnected successfully")
|
|
178
180
|
|
|
179
181
|
except Exception:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
logging.exception("MQTT error in device_status_callback_debug")
|
|
182
|
+
logger.exception("MQTT error in device_status_callback_debug")
|
|
183
183
|
|
|
184
184
|
if mqtt_client.is_connected:
|
|
185
185
|
await mqtt_client.disconnect()
|
|
@@ -199,9 +199,7 @@ async def main():
|
|
|
199
199
|
return 1
|
|
200
200
|
|
|
201
201
|
except Exception:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
logging.exception("Unexpected error in device_status_callback_debug")
|
|
202
|
+
logger.exception("Unexpected error in device_status_callback_debug")
|
|
205
203
|
return 1
|
|
206
204
|
|
|
207
205
|
|
|
@@ -15,7 +15,7 @@ The energy data comes from the EMS (Energy Management System) API.
|
|
|
15
15
|
import asyncio
|
|
16
16
|
import os
|
|
17
17
|
import sys
|
|
18
|
-
from datetime import datetime
|
|
18
|
+
from datetime import UTC, datetime
|
|
19
19
|
|
|
20
20
|
from nwp500 import (
|
|
21
21
|
EnergyUsageResponse,
|
|
@@ -23,6 +23,7 @@ from nwp500 import (
|
|
|
23
23
|
NavienAuthClient,
|
|
24
24
|
NavienMqttClient,
|
|
25
25
|
)
|
|
26
|
+
from nwp500.exceptions import Nwp500Error
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
async def main():
|
|
@@ -135,7 +136,7 @@ async def main():
|
|
|
135
136
|
print("[OK] Subscribed to energy usage responses")
|
|
136
137
|
|
|
137
138
|
# Request energy usage for current month
|
|
138
|
-
now = datetime.now()
|
|
139
|
+
now = datetime.now(UTC).astimezone()
|
|
139
140
|
current_year = now.year
|
|
140
141
|
current_month = now.month
|
|
141
142
|
|
|
@@ -160,7 +161,7 @@ if __name__ == "__main__":
|
|
|
160
161
|
asyncio.run(main())
|
|
161
162
|
except KeyboardInterrupt:
|
|
162
163
|
print("\n\nInterrupted by user")
|
|
163
|
-
except
|
|
164
|
+
except Nwp500Error as e:
|
|
164
165
|
print(f"\n\nError: {e}")
|
|
165
166
|
import traceback
|
|
166
167
|
|
|
File without changes
|
|
@@ -37,6 +37,7 @@ from pathlib import Path
|
|
|
37
37
|
from typing import Any
|
|
38
38
|
|
|
39
39
|
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
40
|
+
from nwp500.exceptions import Nwp500Error
|
|
40
41
|
from nwp500.models import DeviceFeature
|
|
41
42
|
from nwp500.mqtt.utils import redact, redact_topic
|
|
42
43
|
from nwp500.topic_builder import MqttTopicBuilder
|
|
@@ -163,7 +164,7 @@ async def main() -> None:
|
|
|
163
164
|
try:
|
|
164
165
|
await mqtt_client.request_tou_settings(device, serial)
|
|
165
166
|
await asyncio.sleep(5)
|
|
166
|
-
except
|
|
167
|
+
except Nwp500Error as exc:
|
|
167
168
|
print(f" TOU request failed: {exc}")
|
|
168
169
|
|
|
169
170
|
# --- Step 5: wait a bit more to catch any late-arriving messages ---
|
|
@@ -94,8 +94,8 @@ class MqttDiagnosticsExample:
|
|
|
94
94
|
|
|
95
95
|
except asyncio.CancelledError:
|
|
96
96
|
break
|
|
97
|
-
except Exception
|
|
98
|
-
_logger.
|
|
97
|
+
except Exception:
|
|
98
|
+
_logger.exception("Error exporting diagnostics")
|
|
99
99
|
|
|
100
100
|
async def monitor_connection_state(self, interval: float = 10.0) -> None:
|
|
101
101
|
"""
|
|
@@ -124,8 +124,8 @@ class MqttDiagnosticsExample:
|
|
|
124
124
|
|
|
125
125
|
except asyncio.CancelledError:
|
|
126
126
|
break
|
|
127
|
-
except Exception
|
|
128
|
-
_logger.
|
|
127
|
+
except Exception:
|
|
128
|
+
_logger.exception("Error monitoring state")
|
|
129
129
|
|
|
130
130
|
async def on_connection_drop(self, event) -> None:
|
|
131
131
|
"""Handle connection drop event."""
|
|
@@ -289,8 +289,8 @@ class MqttDiagnosticsExample:
|
|
|
289
289
|
|
|
290
290
|
_logger.info("Example complete")
|
|
291
291
|
|
|
292
|
-
except Exception
|
|
293
|
-
_logger.
|
|
292
|
+
except Exception:
|
|
293
|
+
_logger.exception("Error during example")
|
|
294
294
|
raise
|
|
295
295
|
|
|
296
296
|
|
|
@@ -8,7 +8,8 @@ and receive confirmation of the power state change.
|
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import logging
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
12
13
|
|
|
13
14
|
# Set up logging to see the power control process
|
|
14
15
|
logging.basicConfig(level=logging.INFO)
|
|
@@ -8,7 +8,8 @@ trigger the hot button, and configure scheduling.
|
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import logging
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
12
13
|
|
|
13
14
|
# Set up logging to see the recirculation control process
|
|
14
15
|
logging.basicConfig(level=logging.INFO)
|
|
@@ -21,6 +21,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
|
|
|
21
21
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
22
22
|
|
|
23
23
|
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
24
|
+
from nwp500.exceptions import Nwp500Error
|
|
24
25
|
from nwp500.mqtt import MqttConnectionConfig
|
|
25
26
|
|
|
26
27
|
|
|
@@ -142,7 +143,7 @@ if __name__ == "__main__":
|
|
|
142
143
|
asyncio.run(main())
|
|
143
144
|
except KeyboardInterrupt:
|
|
144
145
|
print("\n\n[WARNING] Interrupted by user")
|
|
145
|
-
except
|
|
146
|
+
except Nwp500Error as e:
|
|
146
147
|
print(f"\n[ERROR] Error: {e}")
|
|
147
148
|
import traceback
|
|
148
149
|
|
|
File without changes
|
|
@@ -24,6 +24,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
|
|
|
24
24
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
25
25
|
|
|
26
26
|
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
27
|
+
from nwp500.exceptions import Nwp500Error
|
|
27
28
|
from nwp500.mqtt import MqttConnectionConfig
|
|
28
29
|
|
|
29
30
|
# Configure logging
|
|
@@ -76,7 +77,7 @@ class ResilientMqttClient:
|
|
|
76
77
|
if self.mqtt_client and self.mqtt_client.is_connected:
|
|
77
78
|
try:
|
|
78
79
|
await self.mqtt_client.disconnect()
|
|
79
|
-
except
|
|
80
|
+
except Nwp500Error as e:
|
|
80
81
|
logger.warning(f"Error disconnecting old client: {e}")
|
|
81
82
|
|
|
82
83
|
# Create new client
|
|
@@ -150,7 +151,7 @@ class ResilientMqttClient:
|
|
|
150
151
|
self.recovery_attempt = 0
|
|
151
152
|
logger.info("[SUCCESS] Recovery successful!")
|
|
152
153
|
|
|
153
|
-
except
|
|
154
|
+
except Nwp500Error as e:
|
|
154
155
|
logger.error(f"Recovery attempt failed: {e}")
|
|
155
156
|
# The next reconnection_failed event will trigger another recovery attempt
|
|
156
157
|
finally:
|
|
@@ -252,7 +253,7 @@ if __name__ == "__main__":
|
|
|
252
253
|
asyncio.run(main())
|
|
253
254
|
except KeyboardInterrupt:
|
|
254
255
|
print("\n\n[WARNING] Interrupted by user")
|
|
255
|
-
except
|
|
256
|
+
except Nwp500Error as e:
|
|
256
257
|
print(f"\n[ERROR] Error: {e}")
|
|
257
258
|
import traceback
|
|
258
259
|
|
|
@@ -57,8 +57,7 @@ async def save_tokens_example():
|
|
|
57
57
|
token_data = tokens.to_dict()
|
|
58
58
|
|
|
59
59
|
# Save to file
|
|
60
|
-
|
|
61
|
-
json.dump(token_data, f, indent=2)
|
|
60
|
+
await asyncio.to_thread(TOKEN_FILE.write_text, json.dumps(token_data, indent=2))
|
|
62
61
|
|
|
63
62
|
logger.info(f"[OK] Tokens saved to {TOKEN_FILE}")
|
|
64
63
|
logger.info("You can now use --restore to skip authentication on future runs")
|
|
@@ -81,8 +80,7 @@ async def restore_tokens_example():
|
|
|
81
80
|
)
|
|
82
81
|
|
|
83
82
|
# Load saved tokens
|
|
84
|
-
|
|
85
|
-
token_data = json.load(f)
|
|
83
|
+
token_data = json.loads(await asyncio.to_thread(TOKEN_FILE.read_text))
|
|
86
84
|
|
|
87
85
|
logger.info(f"Loading tokens from {TOKEN_FILE}...")
|
|
88
86
|
|
|
@@ -116,8 +114,9 @@ async def restore_tokens_example():
|
|
|
116
114
|
if tokens.issued_at != stored_tokens.issued_at:
|
|
117
115
|
logger.info("Tokens were refreshed, updating stored copy...")
|
|
118
116
|
token_data = tokens.to_dict()
|
|
119
|
-
|
|
120
|
-
json.
|
|
117
|
+
await asyncio.to_thread(
|
|
118
|
+
TOKEN_FILE.write_text, json.dumps(token_data, indent=2)
|
|
119
|
+
)
|
|
121
120
|
logger.info(f"[OK] Updated tokens saved to {TOKEN_FILE}")
|
|
122
121
|
|
|
123
122
|
|
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
10
|
-
from nwp500.encoding import
|
|
10
|
+
from nwp500.encoding import build_tou_period, decode_price, decode_week_bitfield
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
async def _wait_for_controller_serial(mqtt_client: NavienMqttClient, device) -> str:
|
|
@@ -8,7 +8,8 @@ system for scheduling water heating.
|
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import logging
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
12
13
|
|
|
13
14
|
# Set up logging to see the water program configuration process
|
|
14
15
|
logging.basicConfig(level=logging.INFO)
|
|
@@ -26,6 +26,7 @@ from nwp500.auth import NavienAuthClient
|
|
|
26
26
|
from nwp500.exceptions import (
|
|
27
27
|
AuthenticationError,
|
|
28
28
|
InvalidCredentialsError,
|
|
29
|
+
Nwp500Error,
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
|
|
@@ -97,8 +98,8 @@ async def main():
|
|
|
97
98
|
print(f"Error code: {e.code}")
|
|
98
99
|
return 1
|
|
99
100
|
|
|
100
|
-
except
|
|
101
|
-
print(f"\n[ERROR] Unexpected error: {
|
|
101
|
+
except Nwp500Error as e:
|
|
102
|
+
print(f"\n[ERROR] Unexpected error: {e!s}")
|
|
102
103
|
import traceback
|
|
103
104
|
|
|
104
105
|
traceback.print_exc()
|
|
@@ -17,20 +17,23 @@ logging.basicConfig(
|
|
|
17
17
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
20
22
|
# If running from examples directory, add parent to path
|
|
21
23
|
if __name__ == "__main__":
|
|
22
24
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
|
|
23
25
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
24
26
|
|
|
27
|
+
import re
|
|
28
|
+
|
|
25
29
|
from nwp500 import NavienAPIClient
|
|
26
30
|
from nwp500.auth import NavienAuthClient
|
|
27
31
|
from nwp500.exceptions import (
|
|
28
32
|
APIError,
|
|
29
33
|
AuthenticationError,
|
|
34
|
+
Nwp500Error,
|
|
30
35
|
)
|
|
31
36
|
|
|
32
|
-
import re
|
|
33
|
-
|
|
34
37
|
|
|
35
38
|
def mask_mac(mac: str) -> str:
|
|
36
39
|
"""Redact all MAC addresses in the input string."""
|
|
@@ -72,7 +75,7 @@ async def example_basic_usage():
|
|
|
72
75
|
# Display device information
|
|
73
76
|
try:
|
|
74
77
|
from mask import mask_mac # type: ignore
|
|
75
|
-
except
|
|
78
|
+
except ImportError:
|
|
76
79
|
# fallback helper if import fails when running examples directly
|
|
77
80
|
|
|
78
81
|
def mask_mac(mac: str) -> str: # pragma: no cover - small fallback
|
|
@@ -81,7 +84,7 @@ async def example_basic_usage():
|
|
|
81
84
|
|
|
82
85
|
try:
|
|
83
86
|
from mask import mask_any, mask_location # type: ignore
|
|
84
|
-
except
|
|
87
|
+
except ImportError:
|
|
85
88
|
|
|
86
89
|
def mask_any(_):
|
|
87
90
|
return "[REDACTED]"
|
|
@@ -172,7 +175,7 @@ async def example_basic_usage():
|
|
|
172
175
|
|
|
173
176
|
except Exception:
|
|
174
177
|
# Avoid printing raw exception details to stdout in examples
|
|
175
|
-
|
|
178
|
+
logger.exception("Unexpected error in api_client_example")
|
|
176
179
|
return 1
|
|
177
180
|
|
|
178
181
|
|
|
@@ -199,7 +202,7 @@ async def example_convenience_function():
|
|
|
199
202
|
|
|
200
203
|
try:
|
|
201
204
|
from mask import mask_any, mask_location # type: ignore
|
|
202
|
-
except
|
|
205
|
+
except ImportError:
|
|
203
206
|
|
|
204
207
|
def mask_any(_):
|
|
205
208
|
return "[REDACTED]"
|
|
@@ -218,8 +221,8 @@ async def example_convenience_function():
|
|
|
218
221
|
|
|
219
222
|
return 0
|
|
220
223
|
|
|
221
|
-
except
|
|
222
|
-
print(f"[ERROR] Error: {
|
|
224
|
+
except Nwp500Error as e:
|
|
225
|
+
print(f"[ERROR] Error: {e!s}")
|
|
223
226
|
return 1
|
|
224
227
|
|
|
225
228
|
|
|
@@ -8,7 +8,8 @@ This demonstrates how to programmatically change the water heater DHW
|
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import logging
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
|
|
12
13
|
|
|
13
14
|
# Set up logging to see the temperature change process
|
|
14
15
|
logging.basicConfig(level=logging.INFO)
|
|
@@ -16,6 +16,7 @@ from nwp500 import (
|
|
|
16
16
|
NavienAuthClient,
|
|
17
17
|
NavienMqttClient,
|
|
18
18
|
)
|
|
19
|
+
from nwp500.exceptions import Nwp500Error
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
async def example_basic_pattern():
|
|
@@ -109,7 +110,7 @@ async def example_with_mqtt():
|
|
|
109
110
|
await mqtt_client.disconnect()
|
|
110
111
|
print("✓ MQTT Disconnected")
|
|
111
112
|
|
|
112
|
-
except
|
|
113
|
+
except Nwp500Error as e:
|
|
113
114
|
print(f"✗ MQTT error: {e}")
|
|
114
115
|
|
|
115
116
|
print("✓ Context exited, session closed")
|
|
@@ -26,6 +26,7 @@ logging.basicConfig(
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
from nwp500.auth import NavienAuthClient
|
|
29
|
+
from nwp500.exceptions import Nwp500Error
|
|
29
30
|
from nwp500.mqtt import MqttConnectionConfig, NavienMqttClient
|
|
30
31
|
|
|
31
32
|
|
|
@@ -197,7 +198,7 @@ async def command_queue_demo():
|
|
|
197
198
|
|
|
198
199
|
return True
|
|
199
200
|
|
|
200
|
-
except
|
|
201
|
+
except Nwp500Error as e:
|
|
201
202
|
print(f"\n[ERROR] Error: {e}")
|
|
202
203
|
import traceback
|
|
203
204
|
|
|
@@ -29,6 +29,8 @@ logging.getLogger("nwp500.mqtt_client").setLevel(logging.DEBUG)
|
|
|
29
29
|
logging.getLogger("nwp500.auth").setLevel(logging.INFO)
|
|
30
30
|
logging.getLogger("nwp500.api_client").setLevel(logging.INFO)
|
|
31
31
|
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
32
34
|
# If running from examples directory, add parent to path
|
|
33
35
|
if __name__ == "__main__":
|
|
34
36
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
|
|
@@ -42,7 +44,7 @@ from nwp500.mqtt import NavienMqttClient
|
|
|
42
44
|
|
|
43
45
|
try:
|
|
44
46
|
from mask import mask_mac, mask_mac_in_topic # type: ignore
|
|
45
|
-
except
|
|
47
|
+
except ImportError:
|
|
46
48
|
|
|
47
49
|
def mask_mac(mac): # pragma: no cover - fallback
|
|
48
50
|
return "[REDACTED_MAC]"
|
|
@@ -236,9 +238,7 @@ async def main():
|
|
|
236
238
|
print("[SUCCESS] Disconnected successfully")
|
|
237
239
|
|
|
238
240
|
except Exception:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
logging.exception("MQTT error in device_status_callback")
|
|
241
|
+
logger.exception("MQTT error in device_status_callback")
|
|
242
242
|
|
|
243
243
|
if mqtt_client.is_connected:
|
|
244
244
|
await mqtt_client.disconnect()
|
|
@@ -258,9 +258,7 @@ async def main():
|
|
|
258
258
|
return 1
|
|
259
259
|
|
|
260
260
|
except Exception:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
logging.exception("Unexpected error in device_status_callback")
|
|
261
|
+
logger.exception("Unexpected error in device_status_callback")
|
|
264
262
|
return 1
|
|
265
263
|
|
|
266
264
|
|