nwp500-python 9.0.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.0.0 → nwp500_python-9.2.1}/CHANGELOG.rst +168 -0
- {nwp500_python-9.0.0/src/nwp500_python.egg-info → nwp500_python-9.2.1}/PKG-INFO +1 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/events.rst +17 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/air_filter_reset.py +2 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/auto_recovery.py +6 -5
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/combined_callbacks.py +3 -2
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/demand_response.py +2 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/device_capabilities.py +5 -7
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/device_status_debug.py +6 -8
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/energy_analytics.py +4 -3
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/firmware_payload_capture.py +2 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/mqtt_diagnostics.py +6 -6
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/power_control.py +2 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/recirculation_control.py +2 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/reconnection_demo.py +2 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/simple_auto_recovery.py +4 -3
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/token_restoration.py +5 -6
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/tou_schedule.py +1 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/water_reservation.py +2 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/beginner/01_authentication.py +3 -2
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/beginner/02_list_devices.py +11 -8
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/beginner/04_set_temperature.py +2 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/advanced_auth_patterns.py +2 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/command_queue.py +2 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/device_status_callback.py +5 -7
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/event_driven_control.py +4 -3
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/mqtt_realtime_monitoring.py +6 -8
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/periodic_requests.py +5 -4
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/set_mode.py +2 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/vacation_mode.py +2 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/mask.py +5 -2
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/testing/periodic_device_info.py +5 -4
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/testing/test_api_client.py +8 -8
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/testing/test_mqtt_connection.py +2 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/testing/test_mqtt_messaging.py +16 -13
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/testing/test_periodic_minimal.py +13 -10
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/bump_version.py +86 -16
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/__init__.py +9 -0
- nwp500_python-9.2.1/src/nwp500/cli/output_formatters.py +172 -0
- nwp500_python-9.0.0/src/nwp500/cli/output_formatters.py → nwp500_python-9.2.1/src/nwp500/cli/presentation.py +189 -358
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/cli/rich_output.py +144 -402
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/device_capabilities.py +2 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/encoding.py +2 -2
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/events.py +20 -3
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/feature.py +20 -2
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/schedule.py +51 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/status.py +20 -2
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/tou.py +43 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/__init__.py +3 -0
- nwp500_python-9.2.1/src/nwp500/mqtt/_control_commands.py +217 -0
- nwp500_python-9.2.1/src/nwp500/mqtt/_device_subscriptions.py +227 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/client.py +22 -401
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/command_queue.py +2 -5
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/connection.py +49 -9
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/control.py +32 -2
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/state_tracker.py +8 -7
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/subscriptions.py +80 -11
- nwp500_python-9.2.1/src/nwp500/mqtt/types.py +50 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/utils.py +2 -3
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt_events.py +14 -2
- {nwp500_python-9.0.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.0.0 → nwp500_python-9.2.1/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/SOURCES.txt +9 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/scm_file_list.json +182 -173
- 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.0.0 → nwp500_python-9.2.1}/tests/test_command_queue.py +3 -4
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_device_capabilities.py +31 -1
- nwp500_python-9.2.1/tests/test_mqtt_events.py +120 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_mqtt_reliability.py +86 -2
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_protocol_correctness.py +133 -9
- {nwp500_python-9.0.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.1/tests/test_unit_system_process_wide.py +133 -0
- nwp500_python-9.0.0/src/nwp500_python.egg-info/scm_version.json +0 -8
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.coveragerc +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.github/workflows/ci.yml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.gitignore +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/.readthedocs.yml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/AUTHORS.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/LICENSE.txt +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/Makefile +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/README.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/RELEASE.md +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/Makefile +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/conf.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/explanation/advanced-features.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/explanation/index.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/maintenance.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/manage-units.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/optimize-tou.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/schedule-operation.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/how-to/track-energy.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/index.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/openapi.yaml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/project/authors.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/project/changelog.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/project/history.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/project/license.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/index.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/installation.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/data_conversions.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/device_status.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/quick_reference.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/cli.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/models.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/reference/python_api/mqtt_client.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/requirements.txt +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/.ruff.toml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/README.md +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/pyproject.toml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/README.md +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/format.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/lint.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/scripts/validate_version.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/setup.cfg +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/setup.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/cli/__main__.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/cli/handlers.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/config.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/converters.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/enums.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/__init__.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/device.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/energy.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/conftest.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_auth.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_cli_commands.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_events.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_model_converters.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_models.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_multi_device.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_openei.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_public_api.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tests/test_utils.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.1}/tox.ini +0 -0
|
@@ -5,6 +5,174 @@ 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
|
+
|
|
67
|
+
Version 9.2.0 (2026-07-06)
|
|
68
|
+
==========================
|
|
69
|
+
|
|
70
|
+
Changed
|
|
71
|
+
-------
|
|
72
|
+
- **CLI formatting stacks merged; Rich is the sole human renderer**
|
|
73
|
+
(`#100 <https://github.com/eman/nwp500-python/issues/100>`_): a new
|
|
74
|
+
``src/nwp500/cli/presentation.py`` owns all data-shaping for CLI output
|
|
75
|
+
(field selection, labels, units, ordering, value formatting and energy
|
|
76
|
+
aggregation) as presentation-neutral structures. Because the CLI
|
|
77
|
+
hard-requires ``rich`` (there is no plain-text fallback), the redundant
|
|
78
|
+
plain-text human renderer and the Rich-vs-plain fallback machinery were
|
|
79
|
+
removed: ``rich_output.py`` no longer contains ``_should_use_rich``,
|
|
80
|
+
``_rich_available``, the ``NWP500_NO_RICH`` toggle, or any
|
|
81
|
+
``_print_*_plain`` methods, and now renders the neutral structures
|
|
82
|
+
(including the energy ``TOTAL SUMMARY``) exclusively with Rich, consuming
|
|
83
|
+
the presentation dataclasses directly instead of dict adapters.
|
|
84
|
+
``output_formatters.py`` keeps only JSON/CSV rendering plus thin
|
|
85
|
+
human-output dispatch. Net CLI code drops from ~2088 to ~1838 lines with a
|
|
86
|
+
single data-shaping layer and a single human renderer. Non-energy output
|
|
87
|
+
is byte-for-byte unchanged (verified by golden capture and the existing
|
|
88
|
+
CLI tests); energy output now renders a summary table plus a breakdown
|
|
89
|
+
table entirely in Rich rather than printing plain text and a Rich table.
|
|
90
|
+
- **mqtt/client.py slimmed toward a thin façade** (`#99
|
|
91
|
+
<https://github.com/eman/nwp500-python/issues/99>`_): the ~40 device
|
|
92
|
+
control command proxies and the typed ``subscribe_*``/``unsubscribe_*``
|
|
93
|
+
device-subscription proxies were moved out of ``NavienMqttClient`` into
|
|
94
|
+
two focused mixins, ``DeviceControlCommandsMixin``
|
|
95
|
+
(``mqtt/_control_commands.py``) and ``DeviceSubscriptionsMixin``
|
|
96
|
+
(``mqtt/_device_subscriptions.py``). ``NavienMqttClient`` now inherits
|
|
97
|
+
both, so its public API is unchanged, while ``mqtt/client.py`` shrinks
|
|
98
|
+
from ~1572 to ~1196 lines and reads more clearly as connection
|
|
99
|
+
orchestration plus a public façade. No behavior change.
|
|
100
|
+
- **Event system relationship clarified and de-duplicated** (`#102
|
|
101
|
+
<https://github.com/eman/nwp500-python/issues/102>`_): ``events.py`` and
|
|
102
|
+
``mqtt_events.py`` are not two competing event mechanisms.
|
|
103
|
+
``EventEmitter`` (``events.py``) is the sole delivery mechanism, while
|
|
104
|
+
``mqtt_events.py`` provides the event-name registry
|
|
105
|
+
(``MqttClientEvents``) and the typed dataclass payloads it carries.
|
|
106
|
+
Internal ``emit`` call sites in ``mqtt/state_tracker.py``,
|
|
107
|
+
``mqtt/client.py`` and ``mqtt/subscriptions.py`` now reference the
|
|
108
|
+
``MqttClientEvents`` constants instead of duplicating the raw event-name
|
|
109
|
+
strings, so each event name is defined in exactly one place. Module
|
|
110
|
+
docstrings and the event-system reference docs were updated to document
|
|
111
|
+
the relationship, and typed-payload delivery is now covered by tests.
|
|
112
|
+
- **awscrt types wrapped out of public MQTT signatures** (`#101
|
|
113
|
+
<https://github.com/eman/nwp500-python/issues/101>`_): the library no
|
|
114
|
+
longer exposes ``awscrt`` SDK types on its own public/semi-public API
|
|
115
|
+
surface. A library-owned ``nwp500.mqtt.QoS`` ``IntEnum`` (also exported
|
|
116
|
+
as ``nwp500.QoS``) now replaces ``awscrt.mqtt.QoS`` on the ``publish``
|
|
117
|
+
and ``subscribe`` methods of ``NavienMqttClient``, ``MqttConnection``
|
|
118
|
+
and ``MqttSubscriptionManager``, on ``MqttCommandQueue.enqueue`` and on
|
|
119
|
+
the ``QueuedCommand`` dataclass. ``awscrt.mqtt.Connection`` handles are
|
|
120
|
+
typed behind the ``MqttConnectionHandle`` alias, and translation to/from
|
|
121
|
+
``awscrt`` happens only at the connection-layer boundary
|
|
122
|
+
(``nwp500/mqtt/types.py``). ``NavienMqttClient.publish`` now wraps
|
|
123
|
+
otherwise-uncaught ``AwsCrtError`` in ``MqttPublishError`` so ``awscrt``
|
|
124
|
+
exceptions no longer leak out of the public boundary.
|
|
125
|
+
|
|
126
|
+
.. code-block:: python
|
|
127
|
+
|
|
128
|
+
# OLD
|
|
129
|
+
from awscrt import mqtt
|
|
130
|
+
await client.publish(topic, payload, qos=mqtt.QoS.AT_LEAST_ONCE)
|
|
131
|
+
|
|
132
|
+
# NEW
|
|
133
|
+
from nwp500 import QoS
|
|
134
|
+
await client.publish(topic, payload, qos=QoS.AT_LEAST_ONCE)
|
|
135
|
+
|
|
136
|
+
Fixed
|
|
137
|
+
-----
|
|
138
|
+
- **MQTT ack futures now consumed on abandonment** (`#97
|
|
139
|
+
<https://github.com/eman/nwp500-python/issues/97>`_): ``_await_ack()``
|
|
140
|
+
in ``mqtt/connection.py`` and the inline subscribe/unsubscribe
|
|
141
|
+
acknowledgement waits in ``mqtt/subscriptions.py`` shield the AWS CRT
|
|
142
|
+
future so a timeout or cancellation doesn't propagate into the SDK
|
|
143
|
+
future. Previously, if the shielded future later completed with an
|
|
144
|
+
exception (e.g. ``AwsCrtError`` from clean-session cancellation during
|
|
145
|
+
reconnect) after the awaiting task had already given up, nobody
|
|
146
|
+
retrieved that exception, and asyncio logged a "Future exception was
|
|
147
|
+
never retrieved" warning at garbage-collection time. A done callback
|
|
148
|
+
is now attached whenever a wait is abandoned so the eventual
|
|
149
|
+
result/exception is always retrieved and logged at debug level
|
|
150
|
+
instead of leaking as an unhandled asyncio warning.
|
|
151
|
+
|
|
152
|
+
Documentation
|
|
153
|
+
-------------
|
|
154
|
+
- **Unit-system preference is a deliberate process-wide global** (`#103
|
|
155
|
+
<https://github.com/eman/nwp500-python/issues/103>`_): clarified and
|
|
156
|
+
locked in that the preference in ``nwp500.unit_system`` is an intentional
|
|
157
|
+
process-wide module-level global rather than a
|
|
158
|
+
``contextvars.ContextVar``. A context-local value silently reverted to
|
|
159
|
+
auto-detect for real-time data because MQTT message handling runs in tasks
|
|
160
|
+
scheduled from AWS CRT callback threads whose context never inherits from
|
|
161
|
+
the application task. Added prominent docstring notes to the affected
|
|
162
|
+
models (``DeviceStatus``, ``DeviceFeature``, ``ReservationEntry``,
|
|
163
|
+
``WeeklyReservationEntry``, and ``TOUPeriod``) explaining that unit-aware
|
|
164
|
+
computed fields read the preference at access time and share it across all
|
|
165
|
+
async tasks and threads. This is a non-breaking documentation and test
|
|
166
|
+
change; no API changes.
|
|
167
|
+
|
|
168
|
+
Added
|
|
169
|
+
-----
|
|
170
|
+
- **Tests for process-wide unit-system semantics** (`#103
|
|
171
|
+
<https://github.com/eman/nwp500-python/issues/103>`_): new
|
|
172
|
+
``tests/test_unit_system_process_wide.py`` verifies that setting the global
|
|
173
|
+
preference affects already-constructed model instances at access time and
|
|
174
|
+
that the preference is visible across async tasks and threads.
|
|
175
|
+
|
|
8
176
|
Version 9.0.0 (2026-07-05)
|
|
9
177
|
==========================
|
|
10
178
|
|
|
@@ -17,6 +17,23 @@ Use the event system when you want to react to connection changes, status
|
|
|
17
17
|
transitions, or derived state changes such as temperature deltas and error
|
|
18
18
|
conditions.
|
|
19
19
|
|
|
20
|
+
How the pieces fit together
|
|
21
|
+
---------------------------
|
|
22
|
+
|
|
23
|
+
The event system is split across two complementary modules:
|
|
24
|
+
|
|
25
|
+
* :mod:`nwp500.events` provides the delivery **mechanism** — the generic
|
|
26
|
+
:class:`~nwp500.events.EventEmitter` (multiple listeners, async handlers,
|
|
27
|
+
one-time listeners, priority ordering). ``NavienMqttClient`` extends it.
|
|
28
|
+
* :mod:`nwp500.mqtt_events` provides the **vocabulary** — the
|
|
29
|
+
:class:`~nwp500.mqtt_events.MqttClientEvents` name registry and the typed,
|
|
30
|
+
frozen dataclass payloads carried by each event.
|
|
31
|
+
|
|
32
|
+
These are not two competing systems. Internal ``emit`` call sites reference the
|
|
33
|
+
``MqttClientEvents`` constants and emit the matching payload dataclass, and you
|
|
34
|
+
subscribe with the same constants, so an event name is defined in exactly one
|
|
35
|
+
place.
|
|
36
|
+
|
|
20
37
|
Two Subscription Patterns
|
|
21
38
|
=========================
|
|
22
39
|
|
|
@@ -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()
|