nwp500-python 7.4.7__tar.gz → 7.4.8__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-7.4.7 → nwp500_python-7.4.8}/CHANGELOG.rst +12 -0
- {nwp500_python-7.4.7/src/nwp500_python.egg-info → nwp500_python-7.4.8}/PKG-INFO +1 -1
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/guides/scheduling.rst +123 -11
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/__init__.py +11 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/cli/handlers.py +36 -174
- nwp500_python-7.4.8/src/nwp500/reservations.py +313 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500_python.egg-info/SOURCES.txt +2 -0
- nwp500_python-7.4.8/tests/test_reservations.py +396 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/.coveragerc +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/.github/copilot-instructions.md +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/.github/workflows/ci.yml +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/.github/workflows/release.yml +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/.gitignore +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/.pre-commit-config.yaml +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/.readthedocs.yml +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/AUTHORS.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/CONTRIBUTING.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/LICENSE.txt +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/Makefile +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/README.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/RELEASE.md +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/Makefile +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/_static/.gitignore +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/api/nwp500.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/authors.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/changelog.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/conf.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/configuration.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/development/contributing.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/development/history.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/enumerations.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/guides/advanced_features_explained.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/guides/authentication.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/guides/auto_recovery.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/guides/command_queue.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/guides/energy_monitoring.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/guides/event_system.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/guides/home_assistant_integration.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/guides/mqtt_diagnostics.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/guides/time_of_use.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/guides/unit_conversion.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/index.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/installation.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/license.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/openapi.yaml +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/protocol/data_conversions.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/protocol/device_features.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/protocol/device_status.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/protocol/error_codes.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/protocol/quick_reference.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/protocol/rest_api.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/python_api/api_client.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/python_api/auth_client.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/python_api/cli.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/python_api/device_control.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/python_api/events.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/python_api/exceptions.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/python_api/models.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/python_api/mqtt_client.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/quickstart.rst +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/docs/requirements.txt +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/.ruff.toml +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/README.md +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/air_filter_reset.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/auto_recovery.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/combined_callbacks.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/demand_response.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/device_capabilities.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/device_status_debug.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/energy_analytics.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/mqtt_diagnostics.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/power_control.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/recirculation_control.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/reconnection_demo.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/simple_auto_recovery.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/token_restoration.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/tou_schedule.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/advanced/water_reservation.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/beginner/01_authentication.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/beginner/02_list_devices.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/beginner/04_set_temperature.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/intermediate/advanced_auth_patterns.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/intermediate/command_queue.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/intermediate/device_status_callback.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/intermediate/event_driven_control.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/intermediate/periodic_requests.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/intermediate/set_mode.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/intermediate/vacation_mode.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/mask.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/testing/periodic_device_info.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/testing/test_api_client.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/testing/test_mqtt_connection.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/testing/test_mqtt_messaging.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/examples/testing/test_periodic_minimal.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/pyproject.toml +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/scripts/README.md +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/scripts/bump_version.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/scripts/extract_changelog.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/scripts/format.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/scripts/lint.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/scripts/setup-dev.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/scripts/validate_version.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/setup.cfg +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/setup.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/api_client.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/auth.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/cli/__main__.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/cli/commands.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/cli/output_formatters.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/cli/rich_output.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/config.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/converters.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/device_capabilities.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/encoding.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/enums.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/events.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/factory.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/models.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/mqtt/__init__.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/mqtt/client.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/mqtt/command_queue.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/mqtt/connection.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/mqtt/control.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/mqtt/subscriptions.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/mqtt/utils.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/mqtt_events.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/openei.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/py.typed +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/temperature.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500/utils.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/conftest.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_api_helpers.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_auth.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_cli_basic.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_cli_commands.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_command_decorators.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_command_queue.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_device_capabilities.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_events.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_exceptions.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_model_converters.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_models.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_openei.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_tou_api.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_unit_switching.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tests/test_utils.py +0 -0
- {nwp500_python-7.4.7 → nwp500_python-7.4.8}/tox.ini +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
Changelog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
Version 7.4.8 (2026-02-17)
|
|
6
|
+
==========================
|
|
7
|
+
|
|
8
|
+
Added
|
|
9
|
+
-----
|
|
10
|
+
- **Reservation CRUD Helpers**: New public functions ``fetch_reservations()``,
|
|
11
|
+
``add_reservation()``, ``delete_reservation()``, and ``update_reservation()``
|
|
12
|
+
in ``nwp500.reservations`` (and exported from ``nwp500``). These abstract the
|
|
13
|
+
read-modify-write pattern for single-entry schedule management so library
|
|
14
|
+
users no longer need to fetch the full schedule, splice it manually, and send
|
|
15
|
+
it back. The CLI now delegates to these library functions.
|
|
16
|
+
|
|
5
17
|
Version 7.4.7 (2026-02-17)
|
|
6
18
|
==========================
|
|
7
19
|
|
|
@@ -325,13 +325,19 @@ Managing Reservations
|
|
|
325
325
|
**Important:** The device protocol requires sending the **full list**
|
|
326
326
|
of reservations for every update. Individual add/delete/update
|
|
327
327
|
operations work by fetching the current schedule, modifying it, and
|
|
328
|
-
sending the full list back.
|
|
329
|
-
automatically.
|
|
328
|
+
sending the full list back.
|
|
330
329
|
|
|
331
|
-
|
|
330
|
+
Low-Level Method (``NavienMqttClient``)
|
|
331
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
332
|
+
|
|
333
|
+
Use ``update_reservations()`` when you need full control or are managing
|
|
334
|
+
multiple entries at once:
|
|
332
335
|
|
|
333
336
|
.. code-block:: python
|
|
334
337
|
|
|
338
|
+
from nwp500.mqtt import NavienMqttClient
|
|
339
|
+
from nwp500.encoding import build_reservation_entry
|
|
340
|
+
|
|
335
341
|
reservations = [
|
|
336
342
|
build_reservation_entry(
|
|
337
343
|
enabled=True,
|
|
@@ -358,22 +364,128 @@ automatically.
|
|
|
358
364
|
device, [], enabled=False
|
|
359
365
|
)
|
|
360
366
|
|
|
367
|
+
**Request current schedule:**
|
|
368
|
+
|
|
369
|
+
.. code-block:: python
|
|
370
|
+
|
|
371
|
+
await mqtt.control.request_reservations(device)
|
|
372
|
+
|
|
361
373
|
**Read the current schedule using models:**
|
|
362
374
|
|
|
363
375
|
.. code-block:: python
|
|
364
376
|
|
|
365
377
|
from nwp500 import ReservationSchedule
|
|
366
378
|
|
|
367
|
-
# Subscribe
|
|
379
|
+
# Subscribe to responses
|
|
380
|
+
def on_reservations(schedule: ReservationSchedule) -> None:
|
|
381
|
+
print(f"Enabled: {schedule.enabled}")
|
|
382
|
+
for entry in schedule.reservation:
|
|
383
|
+
print(f" {entry.time} - {', '.join(entry.days)}"
|
|
384
|
+
f" - {entry.temperature}{entry.unit}"
|
|
385
|
+
f" - {entry.mode_name}")
|
|
386
|
+
|
|
387
|
+
await mqtt.subscribe_device_feature(device, on_reservations)
|
|
368
388
|
await mqtt.control.request_reservations(device)
|
|
369
389
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
390
|
+
CLI Helpers
|
|
391
|
+
^^^^^^^^^^^
|
|
392
|
+
|
|
393
|
+
The CLI provides convenience commands:
|
|
394
|
+
|
|
395
|
+
**List current reservations:**
|
|
396
|
+
|
|
397
|
+
.. code-block:: bash
|
|
398
|
+
|
|
399
|
+
nwp-cli reservations get # Formatted table
|
|
400
|
+
nwp-cli reservations get --json # JSON output
|
|
401
|
+
|
|
402
|
+
**Add a single reservation:**
|
|
403
|
+
|
|
404
|
+
.. code-block:: bash
|
|
405
|
+
|
|
406
|
+
nwp-cli reservations add --days MO,TU,WE,TH,FR \
|
|
407
|
+
--hour 6 --minute 30 --mode 4 --temperature 60
|
|
408
|
+
|
|
409
|
+
**Update an existing reservation:**
|
|
410
|
+
|
|
411
|
+
.. code-block:: bash
|
|
412
|
+
|
|
413
|
+
nwp-cli reservations update --mode 3 --temperature 58 1
|
|
414
|
+
|
|
415
|
+
**Delete a reservation:**
|
|
416
|
+
|
|
417
|
+
.. code-block:: bash
|
|
418
|
+
|
|
419
|
+
nwp-cli reservations delete 1
|
|
420
|
+
|
|
421
|
+
Library Helpers
|
|
422
|
+
^^^^^^^^^^^^^^^^
|
|
423
|
+
|
|
424
|
+
The library provides convenience functions that abstract the
|
|
425
|
+
read-modify-write pattern for individual reservation entries.
|
|
426
|
+
|
|
427
|
+
**fetch_reservations()** — Retrieve the current schedule:
|
|
428
|
+
|
|
429
|
+
.. code-block:: python
|
|
430
|
+
|
|
431
|
+
from nwp500 import fetch_reservations
|
|
432
|
+
|
|
433
|
+
schedule = await fetch_reservations(mqtt, device)
|
|
434
|
+
if schedule is not None:
|
|
435
|
+
print(f"Schedule enabled: {schedule.enabled}")
|
|
436
|
+
for entry in schedule.reservation:
|
|
437
|
+
print(f" {entry.time} {', '.join(entry.days)}"
|
|
438
|
+
f" — {entry.temperature}{entry.unit}"
|
|
439
|
+
f" — {entry.mode_name}")
|
|
440
|
+
|
|
441
|
+
**add_reservation()** — Append a new entry to the schedule:
|
|
442
|
+
|
|
443
|
+
.. code-block:: python
|
|
444
|
+
|
|
445
|
+
from nwp500 import add_reservation
|
|
446
|
+
|
|
447
|
+
await add_reservation(
|
|
448
|
+
mqtt, device,
|
|
449
|
+
enabled=True,
|
|
450
|
+
days=["MO", "TU", "WE", "TH", "FR"],
|
|
451
|
+
hour=6,
|
|
452
|
+
minute=30,
|
|
453
|
+
mode=4, # High Demand
|
|
454
|
+
temperature=60.0, # In user's preferred unit
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
**delete_reservation()** — Remove an entry by 1-based index:
|
|
458
|
+
|
|
459
|
+
.. code-block:: python
|
|
460
|
+
|
|
461
|
+
from nwp500 import delete_reservation
|
|
462
|
+
|
|
463
|
+
await delete_reservation(mqtt, device, index=2)
|
|
464
|
+
|
|
465
|
+
**update_reservation()** — Modify specific fields of an existing entry.
|
|
466
|
+
Only the keyword arguments you supply are changed; all others are kept:
|
|
467
|
+
|
|
468
|
+
.. code-block:: python
|
|
469
|
+
|
|
470
|
+
from nwp500 import update_reservation
|
|
471
|
+
|
|
472
|
+
# Change temperature only
|
|
473
|
+
await update_reservation(mqtt, device, 1, temperature=55.0)
|
|
474
|
+
|
|
475
|
+
# Change days and time
|
|
476
|
+
await update_reservation(mqtt, device, 1, days=["SA", "SU"], hour=8, minute=0)
|
|
477
|
+
|
|
478
|
+
# Disable without deleting
|
|
479
|
+
await update_reservation(mqtt, device, 1, enabled=False)
|
|
480
|
+
|
|
481
|
+
These helpers raise :class:`ValueError` for out-of-range arguments,
|
|
482
|
+
:class:`~nwp500.exceptions.RangeValidationError` or
|
|
483
|
+
:class:`~nwp500.exceptions.ValidationError` for device-protocol
|
|
484
|
+
violations. :func:`fetch_reservations` returns ``None`` on timeout and
|
|
485
|
+
logs the failure, while the mutating helpers (:func:`add_reservation`,
|
|
486
|
+
:func:`update_reservation`, :func:`delete_reservation`) raise
|
|
487
|
+
:class:`TimeoutError` if the device does not respond.
|
|
488
|
+
|
|
377
489
|
|
|
378
490
|
Mode Selection Strategy
|
|
379
491
|
-----------------------
|
|
@@ -134,6 +134,12 @@ from nwp500.mqtt_events import (
|
|
|
134
134
|
from nwp500.openei import (
|
|
135
135
|
OpenEIClient,
|
|
136
136
|
)
|
|
137
|
+
from nwp500.reservations import (
|
|
138
|
+
add_reservation,
|
|
139
|
+
delete_reservation,
|
|
140
|
+
fetch_reservations,
|
|
141
|
+
update_reservation,
|
|
142
|
+
)
|
|
137
143
|
from nwp500.unit_system import (
|
|
138
144
|
get_unit_system,
|
|
139
145
|
reset_unit_system,
|
|
@@ -223,6 +229,11 @@ __all__ = [
|
|
|
223
229
|
"NavienAPIClient",
|
|
224
230
|
# OpenEI Client
|
|
225
231
|
"OpenEIClient",
|
|
232
|
+
# Reservation helpers
|
|
233
|
+
"fetch_reservations",
|
|
234
|
+
"add_reservation",
|
|
235
|
+
"delete_reservation",
|
|
236
|
+
"update_reservation",
|
|
226
237
|
# MQTT Client
|
|
227
238
|
"NavienMqttClient",
|
|
228
239
|
"MqttConnectionConfig",
|
|
@@ -23,7 +23,13 @@ from nwp500.exceptions import (
|
|
|
23
23
|
)
|
|
24
24
|
from nwp500.models import ReservationSchedule
|
|
25
25
|
from nwp500.mqtt.utils import redact_serial
|
|
26
|
-
from nwp500.
|
|
26
|
+
from nwp500.reservations import (
|
|
27
|
+
add_reservation,
|
|
28
|
+
delete_reservation,
|
|
29
|
+
fetch_reservations,
|
|
30
|
+
update_reservation,
|
|
31
|
+
)
|
|
32
|
+
from nwp500.unit_system import get_unit_system
|
|
27
33
|
|
|
28
34
|
from .output_formatters import (
|
|
29
35
|
print_device_info,
|
|
@@ -36,16 +42,6 @@ from .rich_output import get_formatter
|
|
|
36
42
|
_logger = logging.getLogger(__name__)
|
|
37
43
|
_formatter = get_formatter()
|
|
38
44
|
|
|
39
|
-
# Raw protocol fields for ReservationEntry (used in model_dump include)
|
|
40
|
-
_RAW_RESERVATION_FIELDS = {
|
|
41
|
-
"enable",
|
|
42
|
-
"week",
|
|
43
|
-
"hour",
|
|
44
|
-
"min",
|
|
45
|
-
"mode",
|
|
46
|
-
"param",
|
|
47
|
-
}
|
|
48
|
-
|
|
49
45
|
T = TypeVar("T")
|
|
50
46
|
|
|
51
47
|
|
|
@@ -275,45 +271,6 @@ async def handle_power_request(
|
|
|
275
271
|
)
|
|
276
272
|
|
|
277
273
|
|
|
278
|
-
async def _fetch_reservations(
|
|
279
|
-
mqtt: NavienMqttClient, device: Device
|
|
280
|
-
) -> ReservationSchedule | None:
|
|
281
|
-
"""Fetch current reservations from device and return as a model.
|
|
282
|
-
|
|
283
|
-
Returns None on timeout.
|
|
284
|
-
"""
|
|
285
|
-
future: asyncio.Future[ReservationSchedule] = (
|
|
286
|
-
asyncio.get_running_loop().create_future()
|
|
287
|
-
)
|
|
288
|
-
caller_unit_system = get_unit_system()
|
|
289
|
-
|
|
290
|
-
def raw_callback(topic: str, message: dict[str, Any]) -> None:
|
|
291
|
-
if (
|
|
292
|
-
future.done()
|
|
293
|
-
or "response" not in message
|
|
294
|
-
or "/res/rsv/" not in topic
|
|
295
|
-
):
|
|
296
|
-
return
|
|
297
|
-
response = message.get("response", {})
|
|
298
|
-
# Ensure it's actually a reservation response (not some other /res/ msg)
|
|
299
|
-
if "reservationUse" not in response and "reservation" not in response:
|
|
300
|
-
return
|
|
301
|
-
if caller_unit_system:
|
|
302
|
-
set_unit_system(caller_unit_system)
|
|
303
|
-
schedule = ReservationSchedule(**response)
|
|
304
|
-
future.set_result(schedule)
|
|
305
|
-
|
|
306
|
-
device_type = str(device.device_info.device_type)
|
|
307
|
-
response_pattern = f"cmd/{device_type}/+/#"
|
|
308
|
-
await mqtt.subscribe(response_pattern, raw_callback)
|
|
309
|
-
await mqtt.control.request_reservations(device)
|
|
310
|
-
try:
|
|
311
|
-
return await asyncio.wait_for(future, timeout=10)
|
|
312
|
-
except TimeoutError:
|
|
313
|
-
_logger.error("Timed out waiting for reservations.")
|
|
314
|
-
return None
|
|
315
|
-
|
|
316
|
-
|
|
317
274
|
def _schedule_to_display_list(
|
|
318
275
|
schedule: ReservationSchedule,
|
|
319
276
|
) -> list[dict[str, Any]]:
|
|
@@ -331,7 +288,7 @@ async def handle_get_reservations_request(
|
|
|
331
288
|
mqtt: NavienMqttClient, device: Device, output_json: bool = False
|
|
332
289
|
) -> None:
|
|
333
290
|
"""Request current reservation schedule."""
|
|
334
|
-
schedule = await
|
|
291
|
+
schedule = await fetch_reservations(mqtt, device)
|
|
335
292
|
if schedule is None:
|
|
336
293
|
return
|
|
337
294
|
|
|
@@ -388,53 +345,21 @@ async def handle_add_reservation_request(
|
|
|
388
345
|
temperature: float,
|
|
389
346
|
) -> None:
|
|
390
347
|
"""Add a single reservation to the existing schedule."""
|
|
391
|
-
from nwp500.encoding import build_reservation_entry
|
|
392
|
-
|
|
393
|
-
# Validate inputs
|
|
394
|
-
if not 0 <= hour <= 23:
|
|
395
|
-
_logger.error("Hour must be between 0 and 23")
|
|
396
|
-
return
|
|
397
|
-
if not 0 <= minute <= 59:
|
|
398
|
-
_logger.error("Minute must be between 0 and 59")
|
|
399
|
-
return
|
|
400
|
-
if not 1 <= mode <= 6:
|
|
401
|
-
_logger.error("Mode must be between 1 and 6")
|
|
402
|
-
return
|
|
403
|
-
|
|
404
|
-
# Parse day string (comma-separated: "MO,WE,FR" or full day names)
|
|
405
348
|
day_list = [d.strip() for d in days.split(",")]
|
|
406
|
-
|
|
407
349
|
try:
|
|
408
|
-
|
|
409
|
-
|
|
350
|
+
await add_reservation(
|
|
351
|
+
mqtt,
|
|
352
|
+
device,
|
|
410
353
|
enabled=enabled,
|
|
411
354
|
days=day_list,
|
|
412
355
|
hour=hour,
|
|
413
356
|
minute=minute,
|
|
414
|
-
|
|
357
|
+
mode=mode,
|
|
415
358
|
temperature=temperature,
|
|
416
359
|
)
|
|
417
|
-
|
|
418
|
-
# Fetch current reservations using shared helper
|
|
419
|
-
schedule = await _fetch_reservations(mqtt, device)
|
|
420
|
-
if schedule is None:
|
|
421
|
-
_logger.error("Timed out fetching current reservations")
|
|
422
|
-
return
|
|
423
|
-
|
|
424
|
-
# Build raw entry list and append new one
|
|
425
|
-
current_reservations = [
|
|
426
|
-
e.model_dump(include=_RAW_RESERVATION_FIELDS)
|
|
427
|
-
for e in schedule.reservation
|
|
428
|
-
]
|
|
429
|
-
current_reservations.append(reservation_entry)
|
|
430
|
-
|
|
431
|
-
# Update the full schedule
|
|
432
|
-
await mqtt.control.update_reservations(
|
|
433
|
-
device, current_reservations, enabled=True
|
|
434
|
-
)
|
|
435
|
-
|
|
436
360
|
print("✓ Reservation added successfully")
|
|
437
|
-
|
|
361
|
+
except (ValueError, TimeoutError) as e:
|
|
362
|
+
_logger.error(str(e))
|
|
438
363
|
except (RangeValidationError, ValidationError) as e:
|
|
439
364
|
_logger.error(f"Failed to add reservation: {e}")
|
|
440
365
|
|
|
@@ -445,34 +370,11 @@ async def handle_delete_reservation_request(
|
|
|
445
370
|
index: int,
|
|
446
371
|
) -> None:
|
|
447
372
|
"""Delete a single reservation by 1-based index."""
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
count = len(schedule.reservation)
|
|
454
|
-
if index < 1 or index > count:
|
|
455
|
-
_logger.error(
|
|
456
|
-
f"Invalid reservation index {index}. "
|
|
457
|
-
f"Valid range: 1-{count} ({count} reservation(s) exist)"
|
|
458
|
-
)
|
|
459
|
-
return
|
|
460
|
-
|
|
461
|
-
# Build raw entry list and remove the target
|
|
462
|
-
current_reservations = [
|
|
463
|
-
e.model_dump(include=_RAW_RESERVATION_FIELDS)
|
|
464
|
-
for e in schedule.reservation
|
|
465
|
-
]
|
|
466
|
-
removed = current_reservations.pop(index - 1)
|
|
467
|
-
_logger.info(f"Removing reservation {index}: {removed}")
|
|
468
|
-
|
|
469
|
-
# Determine if reservations should stay enabled
|
|
470
|
-
still_enabled = schedule.enabled and len(current_reservations) > 0
|
|
471
|
-
|
|
472
|
-
await mqtt.control.update_reservations(
|
|
473
|
-
device, current_reservations, enabled=still_enabled
|
|
474
|
-
)
|
|
475
|
-
print(f"✓ Reservation {index} deleted successfully")
|
|
373
|
+
try:
|
|
374
|
+
await delete_reservation(mqtt, device, index)
|
|
375
|
+
print(f"✓ Reservation {index} deleted successfully")
|
|
376
|
+
except (ValueError, TimeoutError) as e:
|
|
377
|
+
_logger.error(str(e))
|
|
476
378
|
|
|
477
379
|
|
|
478
380
|
async def handle_update_reservation_request(
|
|
@@ -491,66 +393,26 @@ async def handle_update_reservation_request(
|
|
|
491
393
|
|
|
492
394
|
Only the provided fields are modified; others are preserved.
|
|
493
395
|
"""
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
schedule = await _fetch_reservations(mqtt, device)
|
|
497
|
-
if schedule is None:
|
|
498
|
-
_logger.error("Timed out fetching current reservations")
|
|
499
|
-
return
|
|
500
|
-
|
|
501
|
-
count = len(schedule.reservation)
|
|
502
|
-
if index < 1 or index > count:
|
|
503
|
-
_logger.error(
|
|
504
|
-
f"Invalid reservation index {index}. "
|
|
505
|
-
f"Valid range: 1-{count} ({count} reservation(s) exist)"
|
|
506
|
-
)
|
|
507
|
-
return
|
|
508
|
-
|
|
509
|
-
existing = schedule.reservation[index - 1]
|
|
510
|
-
|
|
511
|
-
# Merge: use provided values or fall back to existing
|
|
512
|
-
new_enabled = enabled if enabled is not None else existing.enabled
|
|
513
|
-
new_days: list[str] = (
|
|
514
|
-
[d.strip() for d in days.split(",")] if days else existing.days
|
|
396
|
+
day_list: list[str] | None = (
|
|
397
|
+
[d.strip() for d in days.split(",")] if days is not None else None
|
|
515
398
|
)
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
hour=new_hour,
|
|
527
|
-
minute=new_minute,
|
|
528
|
-
mode_id=new_mode,
|
|
399
|
+
try:
|
|
400
|
+
await update_reservation(
|
|
401
|
+
mqtt,
|
|
402
|
+
device,
|
|
403
|
+
index,
|
|
404
|
+
enabled=enabled,
|
|
405
|
+
days=day_list,
|
|
406
|
+
hour=hour,
|
|
407
|
+
minute=minute,
|
|
408
|
+
mode=mode,
|
|
529
409
|
temperature=temperature,
|
|
530
410
|
)
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
"week": encode_week_bitfield(new_days),
|
|
537
|
-
"hour": new_hour,
|
|
538
|
-
"min": new_minute,
|
|
539
|
-
"mode": new_mode,
|
|
540
|
-
"param": existing.param,
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
# Build full list with the replacement
|
|
544
|
-
current_reservations = [
|
|
545
|
-
e.model_dump(include=_RAW_RESERVATION_FIELDS)
|
|
546
|
-
for e in schedule.reservation
|
|
547
|
-
]
|
|
548
|
-
current_reservations[index - 1] = new_entry
|
|
549
|
-
|
|
550
|
-
await mqtt.control.update_reservations(
|
|
551
|
-
device, current_reservations, enabled=schedule.enabled
|
|
552
|
-
)
|
|
553
|
-
print(f"✓ Reservation {index} updated successfully")
|
|
411
|
+
print(f"✓ Reservation {index} updated successfully")
|
|
412
|
+
except (ValueError, TimeoutError) as e:
|
|
413
|
+
_logger.error(str(e))
|
|
414
|
+
except (RangeValidationError, ValidationError) as e:
|
|
415
|
+
_logger.error(f"Failed to update reservation: {e}")
|
|
554
416
|
|
|
555
417
|
|
|
556
418
|
async def handle_enable_anti_legionella_request(
|