nwp500-python 9.4.1__tar.gz → 9.4.2__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.4.1 → nwp500_python-9.4.2}/CHANGELOG.rst +16 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/Makefile +13 -5
- {nwp500_python-9.4.1/src/nwp500_python.egg-info → nwp500_python-9.4.2}/PKG-INFO +1 -1
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/subscriptions.py +38 -1
- {nwp500_python-9.4.1 → nwp500_python-9.4.2/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- nwp500_python-9.4.2/src/nwp500_python.egg-info/scm_version.json +8 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_multi_device.py +149 -0
- nwp500_python-9.4.1/src/nwp500_python.egg-info/scm_version.json +0 -8
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.coveragerc +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.github/workflows/ci.yml +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.gitignore +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.readthedocs.yml +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/AUTHORS.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/LICENSE.txt +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/README.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/RELEASE.md +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/Makefile +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/conf.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/explanation/advanced-features.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/explanation/index.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/explanation/tank-energy.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/explanation/tou-recovery-cap.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/explanation/unknown-values.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/maintenance.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/manage-units.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/optimize-tou.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/schedule-operation.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/track-energy.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/index.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/openapi.yaml +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/project/authors.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/project/changelog.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/project/history.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/project/license.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/index.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/installation.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/data_conversions.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/device_status.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/quick_reference.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/cli.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/events.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/models.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/mqtt_client.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/requirements.txt +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/.ruff.toml +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/README.md +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/air_filter_reset.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/auto_recovery.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/combined_callbacks.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/demand_response.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/device_capabilities.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/device_status_debug.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/energy_analytics.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/firmware_payload_capture.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/mqtt_diagnostics.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/power_control.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/recirculation_control.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/reconnection_demo.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/simple_auto_recovery.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/token_restoration.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/tou_schedule.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/water_reservation.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/beginner/01_authentication.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/beginner/02_list_devices.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/beginner/04_set_temperature.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/advanced_auth_patterns.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/command_queue.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/device_status_callback.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/event_driven_control.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/periodic_requests.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/set_mode.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/vacation_mode.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/mask.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/testing/periodic_device_info.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/testing/test_api_client.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/testing/test_mqtt_connection.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/testing/test_mqtt_messaging.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/testing/test_periodic_minimal.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/pyproject.toml +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/README.md +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/bump_version.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/format.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/lint.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/validate_version.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/setup.cfg +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/setup.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/__init__.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/__main__.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/handlers.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/output_formatters.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/presentation.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/rich_output.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/config.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/converters.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/device_capabilities.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/encoding.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/enums.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/events.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/__init__.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/device.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/diagnostics.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/energy.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/feature.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/firmware.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/schedule.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/status.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/tou.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/__init__.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/_control_commands.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/client.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/command_queue.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/connection.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/control.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/state_tracker.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/types.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/utils.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt_events.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/reservations.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/tou_schedule.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/SOURCES.txt +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/scm_file_list.json +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/conftest.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_app_queries.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_auth.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_canonical_schedule.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_cli_commands.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_cli_tristate_presentation.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_command_queue.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_device_capabilities.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_device_rest_models.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_events.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_model_converters.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_models.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_events.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_reliability.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_openei.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_protocol_correctness.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_public_api.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_reservations.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_tou_schedule.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_unit_system_process_wide.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_utils.py +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tox.ini +0 -0
- {nwp500_python-9.4.1 → nwp500_python-9.4.2}/uv.lock +0 -0
|
@@ -5,6 +5,22 @@ Changelog
|
|
|
5
5
|
Unreleased
|
|
6
6
|
==========
|
|
7
7
|
|
|
8
|
+
Version 9.4.2 (2026-09-17)
|
|
9
|
+
==========================
|
|
10
|
+
|
|
11
|
+
Changed
|
|
12
|
+
-------
|
|
13
|
+
- **A dropped response message now says why it was dropped.** The typed
|
|
14
|
+
response handlers declined a message in three places without a word: a
|
|
15
|
+
topic that does not end in the required suffix, a payload with no data
|
|
16
|
+
under the expected key, and a reply whose ``macAddress`` names another
|
|
17
|
+
device. A caller watching for a reply that never reached its callback
|
|
18
|
+
could not tell a dropped message from one the cloud never published, and
|
|
19
|
+
the two want opposite fixes. Each branch now logs its reason at DEBUG,
|
|
20
|
+
with the topic and MAC redacted as elsewhere, and guarded by
|
|
21
|
+
``isEnabledFor`` so the redaction regexes do not run on the normal
|
|
22
|
+
dispatch path when debug logging is off.
|
|
23
|
+
|
|
8
24
|
Version 9.4.1 (2026-09-17)
|
|
9
25
|
==========================
|
|
10
26
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.PHONY: help install install-dev lint format test clean build release check-release ci-lint ci-format ci-check version-bump validate-version
|
|
1
|
+
.PHONY: help install install-dev lint format test test-cov require-python clean build release check-release ci-lint ci-format ci-check version-bump validate-version
|
|
2
2
|
|
|
3
3
|
help: ## Show this help message
|
|
4
4
|
@echo 'Usage: make [target]'
|
|
@@ -35,11 +35,19 @@ ci-format: ## Run formatting exactly as CI does (via tox format)
|
|
|
35
35
|
ci-check: ci-lint ## Run the same checks as CI (ensures local/CI consistency)
|
|
36
36
|
@echo "🎉 All CI checks passed locally!"
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
# The package declares python_requires = >=3.14 and uses 3.14 syntax, but
|
|
39
|
+
# that floor is only enforced when installing - running pytest against the
|
|
40
|
+
# source tree happens on whatever interpreter is active. Fail with a clear
|
|
41
|
+
# message instead of a SyntaxError in every test module.
|
|
42
|
+
require-python:
|
|
43
|
+
@python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 14) else 1)' \
|
|
44
|
+
|| { echo "Python 3.14+ required, found $$(python3 -V 2>&1). Activate the project environment."; exit 1; }
|
|
40
45
|
|
|
41
|
-
test-
|
|
42
|
-
|
|
46
|
+
test: require-python ## Run tests with pytest
|
|
47
|
+
python3 -m pytest
|
|
48
|
+
|
|
49
|
+
test-cov: require-python ## Run tests with coverage report
|
|
50
|
+
python3 -m pytest --cov=nwp500 --cov-report=html --cov-report=term-missing
|
|
43
51
|
|
|
44
52
|
clean: ## Remove build artifacts and cache files
|
|
45
53
|
rm -rf build dist *.egg-info .eggs
|
|
@@ -40,7 +40,12 @@ from ..mqtt_events import (
|
|
|
40
40
|
from ..topic_builder import MqttTopicBuilder
|
|
41
41
|
from .state_tracker import DeviceStateTracker
|
|
42
42
|
from .types import QoS, to_awscrt_qos
|
|
43
|
-
from .utils import
|
|
43
|
+
from .utils import (
|
|
44
|
+
get_response_data,
|
|
45
|
+
redact_mac,
|
|
46
|
+
redact_topic,
|
|
47
|
+
topic_matches_pattern,
|
|
48
|
+
)
|
|
44
49
|
|
|
45
50
|
if TYPE_CHECKING:
|
|
46
51
|
from ..device_info_cache import MqttDeviceInfoCache
|
|
@@ -614,13 +619,45 @@ class MqttSubscriptionManager:
|
|
|
614
619
|
"""
|
|
615
620
|
|
|
616
621
|
def handler(topic: str, message: dict[str, Any]) -> None:
|
|
622
|
+
# Every branch that declines a message says so at debug level.
|
|
623
|
+
# Silence here is expensive to diagnose: a caller watching for a
|
|
624
|
+
# reply that never reaches its callback cannot tell a dropped
|
|
625
|
+
# message from one the cloud never sent, and the two want
|
|
626
|
+
# opposite fixes. The logs are the only evidence either way.
|
|
627
|
+
#
|
|
628
|
+
# Each log is guarded: redact_topic runs a dozen regexes, and
|
|
629
|
+
# these branches are the normal case for a handler registered
|
|
630
|
+
# under the device wildcard, which sees every message for that
|
|
631
|
+
# device and keeps only its own.
|
|
617
632
|
if topic_suffix and not topic.endswith(topic_suffix):
|
|
633
|
+
if _logger.isEnabledFor(logging.DEBUG):
|
|
634
|
+
_logger.debug(
|
|
635
|
+
"Ignoring a message on %s: the topic does not end "
|
|
636
|
+
"with %r, which this handler requires",
|
|
637
|
+
redact_topic(topic),
|
|
638
|
+
topic_suffix,
|
|
639
|
+
)
|
|
618
640
|
return
|
|
619
641
|
try:
|
|
620
642
|
data = get_response_data(message, key)
|
|
621
643
|
if not data:
|
|
644
|
+
if _logger.isEnabledFor(logging.DEBUG):
|
|
645
|
+
_logger.debug(
|
|
646
|
+
"Ignoring a message on %s: no %s data to parse "
|
|
647
|
+
"as %s",
|
|
648
|
+
redact_topic(topic),
|
|
649
|
+
f"{key!r}" if key else "response",
|
|
650
|
+
model.__name__,
|
|
651
|
+
)
|
|
622
652
|
return
|
|
623
653
|
if only_mac and not _same_device(data, message, only_mac):
|
|
654
|
+
if _logger.isEnabledFor(logging.DEBUG):
|
|
655
|
+
_logger.debug(
|
|
656
|
+
"Ignoring a message on %s: it names another "
|
|
657
|
+
"device, not %s",
|
|
658
|
+
redact_topic(topic),
|
|
659
|
+
redact_mac(only_mac),
|
|
660
|
+
)
|
|
624
661
|
return
|
|
625
662
|
|
|
626
663
|
parsed = parse(data) if parse else model.model_validate(data)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from unittest.mock import AsyncMock, MagicMock
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
@@ -5,6 +6,7 @@ import pytest
|
|
|
5
6
|
from nwp500.enums import CurrentOperationMode
|
|
6
7
|
from nwp500.events import EventEmitter
|
|
7
8
|
from nwp500.models import DeviceFeature, DeviceStatus
|
|
9
|
+
from nwp500.mqtt import subscriptions as subscriptions_module
|
|
8
10
|
from nwp500.mqtt.state_tracker import DeviceStateTracker
|
|
9
11
|
from nwp500.mqtt.subscriptions import MqttSubscriptionManager
|
|
10
12
|
from nwp500.mqtt_events import (
|
|
@@ -241,3 +243,150 @@ def test_make_handler_injects_mac():
|
|
|
241
243
|
parsed = callback_called[0]
|
|
242
244
|
assert isinstance(parsed, DeviceStatus)
|
|
243
245
|
assert parsed.mac_address == mac
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _manager() -> MqttSubscriptionManager:
|
|
249
|
+
return MqttSubscriptionManager(
|
|
250
|
+
connection=MagicMock(),
|
|
251
|
+
client_id="test_client",
|
|
252
|
+
event_emitter=MagicMock(),
|
|
253
|
+
schedule_coroutine=MagicMock(),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@pytest.mark.parametrize(
|
|
258
|
+
("kwargs", "topic", "message", "expected"),
|
|
259
|
+
[
|
|
260
|
+
(
|
|
261
|
+
{"topic_suffix": "st/rsv"},
|
|
262
|
+
"cmd/52/client/res/td/rd",
|
|
263
|
+
{"response": {"reservation": []}},
|
|
264
|
+
"does not end with",
|
|
265
|
+
),
|
|
266
|
+
(
|
|
267
|
+
{"key": "status"},
|
|
268
|
+
"cmd/52/client/res/st",
|
|
269
|
+
{"response": {}},
|
|
270
|
+
"no 'status' data to parse",
|
|
271
|
+
),
|
|
272
|
+
(
|
|
273
|
+
{"key": "status", "only_mac": "00:11:22:33:44:55"},
|
|
274
|
+
"cmd/52/client/res/st",
|
|
275
|
+
{"status": {"macAddress": "aa:bb:cc:dd:ee:ff"}},
|
|
276
|
+
"names another device",
|
|
277
|
+
),
|
|
278
|
+
],
|
|
279
|
+
ids=["topic-suffix", "no-data", "other-device"],
|
|
280
|
+
)
|
|
281
|
+
def test_a_declined_message_says_why(kwargs, topic, message, expected, caplog):
|
|
282
|
+
"""Every branch that drops a message logs its reason.
|
|
283
|
+
|
|
284
|
+
Silence here cost real diagnosis time: a reply that never reaches its
|
|
285
|
+
callback looks identical to one the cloud never sent, and the two want
|
|
286
|
+
opposite fixes.
|
|
287
|
+
"""
|
|
288
|
+
delivered = []
|
|
289
|
+
handler = _manager()._make_handler(
|
|
290
|
+
model=DeviceStatus, callback=delivered.append, **kwargs
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
with caplog.at_level(logging.DEBUG, logger="nwp500.mqtt.subscriptions"):
|
|
294
|
+
handler(topic, message)
|
|
295
|
+
|
|
296
|
+
assert delivered == []
|
|
297
|
+
assert any(expected in r.getMessage() for r in caplog.records), caplog.text
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_a_declined_message_never_logs_a_raw_identifier(caplog):
|
|
301
|
+
"""The reason is logged; the MAC in it is not.
|
|
302
|
+
|
|
303
|
+
These lines carry a topic and a MAC straight from the wire, so the
|
|
304
|
+
redaction is the whole reason they are safe to emit at all.
|
|
305
|
+
"""
|
|
306
|
+
mac = "04786332fca0"
|
|
307
|
+
other = "aabbccddeeff"
|
|
308
|
+
topic = f"cmd/52/navilink-{mac}/st/td/rd"
|
|
309
|
+
delivered = []
|
|
310
|
+
handler = _manager()._make_handler(
|
|
311
|
+
model=DeviceStatus,
|
|
312
|
+
callback=delivered.append,
|
|
313
|
+
key="status",
|
|
314
|
+
only_mac=mac,
|
|
315
|
+
parse=lambda data: data,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
with caplog.at_level(logging.DEBUG, logger="nwp500.mqtt.subscriptions"):
|
|
319
|
+
handler(topic, {"status": {"macAddress": other, "command": 0}})
|
|
320
|
+
|
|
321
|
+
assert delivered == []
|
|
322
|
+
assert "names another device" in caplog.text
|
|
323
|
+
assert mac not in caplog.text
|
|
324
|
+
assert other not in caplog.text
|
|
325
|
+
assert "REDACTED" in caplog.text
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@pytest.mark.parametrize(
|
|
329
|
+
("level", "redactions"),
|
|
330
|
+
[(logging.INFO, 0), (logging.DEBUG, 1)],
|
|
331
|
+
ids=["debug-off", "debug-on"],
|
|
332
|
+
)
|
|
333
|
+
def test_declining_a_message_redacts_only_when_debug_is_on(
|
|
334
|
+
level, redactions, monkeypatch
|
|
335
|
+
):
|
|
336
|
+
"""The guard, not just the log level.
|
|
337
|
+
|
|
338
|
+
redact_topic is a dozen regexes, and a handler registered under the
|
|
339
|
+
device wildcard declines most of what it sees, so an unguarded call
|
|
340
|
+
would run them on the normal dispatch path for nothing.
|
|
341
|
+
"""
|
|
342
|
+
calls = []
|
|
343
|
+
real = subscriptions_module.redact_topic
|
|
344
|
+
|
|
345
|
+
def counting_redact_topic(topic):
|
|
346
|
+
calls.append(topic)
|
|
347
|
+
return real(topic)
|
|
348
|
+
|
|
349
|
+
monkeypatch.setattr(
|
|
350
|
+
subscriptions_module, "redact_topic", counting_redact_topic
|
|
351
|
+
)
|
|
352
|
+
# setLevel, not the attribute: isEnabledFor caches its answer, and
|
|
353
|
+
# only setLevel invalidates that cache.
|
|
354
|
+
logger = subscriptions_module._logger
|
|
355
|
+
original_level = logger.level
|
|
356
|
+
logger.setLevel(level)
|
|
357
|
+
monkeypatch.setattr(logger, "propagate", False)
|
|
358
|
+
|
|
359
|
+
handler = _manager()._make_handler(
|
|
360
|
+
model=DeviceStatus,
|
|
361
|
+
callback=lambda parsed: None,
|
|
362
|
+
key="status",
|
|
363
|
+
topic_suffix="st/rsv",
|
|
364
|
+
)
|
|
365
|
+
try:
|
|
366
|
+
handler("cmd/52/client/res/td/rd", {"status": {"command": 0}})
|
|
367
|
+
finally:
|
|
368
|
+
logger.setLevel(original_level)
|
|
369
|
+
|
|
370
|
+
assert len(calls) == redactions
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def test_a_message_for_this_device_is_still_delivered(caplog):
|
|
374
|
+
"""The mac filter accepts its own device, and says nothing about it."""
|
|
375
|
+
mac = "00:11:22:33:44:55"
|
|
376
|
+
delivered = []
|
|
377
|
+
handler = _manager()._make_handler(
|
|
378
|
+
model=DeviceStatus,
|
|
379
|
+
callback=delivered.append,
|
|
380
|
+
key="status",
|
|
381
|
+
only_mac=mac,
|
|
382
|
+
parse=lambda data: data,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
with caplog.at_level(logging.DEBUG, logger="nwp500.mqtt.subscriptions"):
|
|
386
|
+
handler(
|
|
387
|
+
"cmd/52/client/res/st",
|
|
388
|
+
{"response": {"status": {"macAddress": mac, "command": 0}}},
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
assert len(delivered) == 1
|
|
392
|
+
assert "Ignoring" not in caplog.text
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/legacy_auth_constructor.py
RENAMED
|
File without changes
|
{nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/mqtt_realtime_monitoring.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|