nwp500-python 9.4.0__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.0 → nwp500_python-9.4.2}/CHANGELOG.rst +30 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/Makefile +13 -5
- {nwp500_python-9.4.0/src/nwp500_python.egg-info → nwp500_python-9.4.2}/PKG-INFO +1 -1
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/explanation/tank-energy.rst +7 -7
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/data_conversions.rst +1 -1
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/models.rst +1 -1
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/converters.py +1 -1
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/reconnection.py +7 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/subscriptions.py +38 -1
- {nwp500_python-9.4.0 → 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.0 → nwp500_python-9.4.2}/tests/test_mqtt_reliability.py +39 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_multi_device.py +149 -0
- nwp500_python-9.4.0/src/nwp500_python.egg-info/scm_version.json +0 -8
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.coveragerc +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.github/workflows/ci.yml +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.gitignore +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.readthedocs.yml +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/AUTHORS.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/LICENSE.txt +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/README.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/RELEASE.md +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/Makefile +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/conf.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/explanation/advanced-features.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/explanation/index.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/explanation/tou-recovery-cap.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/explanation/unknown-values.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/maintenance.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/manage-units.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/optimize-tou.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/schedule-operation.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/track-energy.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/index.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/openapi.yaml +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/project/authors.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/project/changelog.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/project/history.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/project/license.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/index.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/installation.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/device_status.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/quick_reference.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/cli.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/events.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/mqtt_client.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/requirements.txt +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/.ruff.toml +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/README.md +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/air_filter_reset.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/auto_recovery.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/combined_callbacks.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/demand_response.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/device_capabilities.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/device_status_debug.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/energy_analytics.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/firmware_payload_capture.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/mqtt_diagnostics.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/power_control.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/recirculation_control.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/reconnection_demo.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/simple_auto_recovery.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/token_restoration.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/tou_schedule.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/water_reservation.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/beginner/01_authentication.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/beginner/02_list_devices.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/beginner/04_set_temperature.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/advanced_auth_patterns.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/command_queue.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/device_status_callback.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/event_driven_control.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/periodic_requests.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/set_mode.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/vacation_mode.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/mask.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/testing/periodic_device_info.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/testing/test_api_client.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/testing/test_mqtt_connection.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/testing/test_mqtt_messaging.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/testing/test_periodic_minimal.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/pyproject.toml +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/README.md +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/bump_version.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/format.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/lint.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/validate_version.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/setup.cfg +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/setup.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/__init__.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/__main__.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/handlers.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/output_formatters.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/presentation.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/rich_output.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/config.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/device_capabilities.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/encoding.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/enums.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/events.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/__init__.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/device.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/diagnostics.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/energy.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/feature.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/firmware.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/schedule.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/status.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/tou.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/__init__.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/_control_commands.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/client.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/command_queue.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/connection.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/control.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/state_tracker.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/types.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/utils.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt_events.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/reservations.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/tou_schedule.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/SOURCES.txt +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/scm_file_list.json +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/conftest.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_app_queries.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_auth.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_canonical_schedule.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_cli_commands.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_cli_tristate_presentation.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_command_queue.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_device_capabilities.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_device_rest_models.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_events.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_model_converters.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_models.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_mqtt_events.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_openei.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_protocol_correctness.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_public_api.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_reservations.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_tou_schedule.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_unit_system_process_wide.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_utils.py +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tox.ini +0 -0
- {nwp500_python-9.4.0 → nwp500_python-9.4.2}/uv.lock +0 -0
|
@@ -5,6 +5,36 @@ 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
|
+
|
|
24
|
+
Version 9.4.1 (2026-09-17)
|
|
25
|
+
==========================
|
|
26
|
+
|
|
27
|
+
Fixed
|
|
28
|
+
-----
|
|
29
|
+
- **The reconnect attempt counter never reset after a successful quick or
|
|
30
|
+
deep reconnect.** Both build a new connection, so the SDK never calls
|
|
31
|
+
``on_connection_resumed``, the only place the counter was reset. Each
|
|
32
|
+
later interruption started where the last one stopped: its first delay
|
|
33
|
+
was longer and it reached the deep-reconnect threshold sooner. A
|
|
34
|
+
long-running client saw the first delay grow from about 1 s to 12 s
|
|
35
|
+
over five daily AWS IoT disconnects. The counter now resets whenever the
|
|
36
|
+
backoff loop ends connected.
|
|
37
|
+
|
|
8
38
|
Version 9.4.0 (2026-09-12)
|
|
9
39
|
==========================
|
|
10
40
|
|
|
@@ -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
|
|
@@ -3,7 +3,7 @@ Tank Energy
|
|
|
3
3
|
===========
|
|
4
4
|
|
|
5
5
|
The NWP500 reports two energy figures, ``totalEnergyCapacity`` and
|
|
6
|
-
``availableEnergyCapacity``. Both names mislead, and before
|
|
6
|
+
``availableEnergyCapacity``. Both names mislead, and before v9.3.0 this
|
|
7
7
|
library also scaled them wrongly. This page explains what they
|
|
8
8
|
actually measure and shows the evidence for the correction.
|
|
9
9
|
|
|
@@ -33,7 +33,7 @@ The short version
|
|
|
33
33
|
whenever the setpoint moves.
|
|
34
34
|
|
|
35
35
|
Both are raw counts of 4 Wh each, not Watt-hours. Library versions before
|
|
36
|
-
|
|
36
|
+
9.3.0 multiplied by 10, overstating tank energy by 2.5x.
|
|
37
37
|
|
|
38
38
|
Both are also measured **from the setpoint**, so both describe potential
|
|
39
39
|
rather than content: move the setpoint and both change while the water in
|
|
@@ -226,7 +226,7 @@ coefficient of performance:
|
|
|
226
226
|
* - 4 Wh/count (corrected)
|
|
227
227
|
- 2.89
|
|
228
228
|
- Normal for a heat pump water heater
|
|
229
|
-
* - 10 Wh/count (pre-
|
|
229
|
+
* - 10 Wh/count (pre-9.3.0)
|
|
230
230
|
- 7.02
|
|
231
231
|
- Physically impossible
|
|
232
232
|
|
|
@@ -236,7 +236,7 @@ A heat pump water heater in a 72 degF room runs at a COP of roughly 2 to
|
|
|
236
236
|
This argument is worth stating separately because it needs no tank
|
|
237
237
|
volume, specific heat or stratification model - only the device's own
|
|
238
238
|
reported energy and its own reported power. It cannot tell you what the
|
|
239
|
-
quantum *is*, but it rules out the pre-
|
|
239
|
+
quantum *is*, but it rules out the pre-9.3.0 value regardless of anything
|
|
240
240
|
assumed elsewhere on this page.
|
|
241
241
|
|
|
242
242
|
.. _two branches:
|
|
@@ -403,8 +403,8 @@ the heat capacity is 156 Wh per degF:
|
|
|
403
403
|
drawable_Wh = 156 * (tank_mean_temperature - your_floor_degF)
|
|
404
404
|
|
|
405
405
|
|
|
406
|
-
Migrating from before
|
|
407
|
-
|
|
406
|
+
Migrating from before v9.3.0
|
|
407
|
+
============================
|
|
408
408
|
|
|
409
409
|
.. list-table::
|
|
410
410
|
:header-rows: 1
|
|
@@ -429,7 +429,7 @@ rename that is missed fails immediately with ``AttributeError`` instead
|
|
|
429
429
|
of silently returning a number 2.5x too large.
|
|
430
430
|
|
|
431
431
|
If you logged these values historically, the stored series needs
|
|
432
|
-
rescaling by 0.4 to be comparable with values from
|
|
432
|
+
rescaling by 0.4 to be comparable with values from v9.3.0 onward.
|
|
433
433
|
|
|
434
434
|
|
|
435
435
|
See also
|
|
@@ -367,7 +367,7 @@ Power and Energy Fields
|
|
|
367
367
|
candidate implying a water volume below the nameplate. Two further
|
|
368
368
|
checks agree -- 183 heating recoveries give 4.11 Wh/count by a
|
|
369
369
|
noisier route, and the same recoveries imply a heat-pump COP of 2.89.
|
|
370
|
-
The library uses 4.0. Versions before
|
|
370
|
+
The library uses 4.0. Versions before 9.3.0 used 10, which overstated
|
|
371
371
|
reported tank energy by 2.5x and implied a physically impossible COP
|
|
372
372
|
of 7.0. See :doc:`../../explanation/tank-energy`.
|
|
373
373
|
|
|
@@ -263,7 +263,7 @@ Complete real-time device status with 100+ fields.
|
|
|
263
263
|
|
|
264
264
|
.. warning::
|
|
265
265
|
These replace ``total_energy_capacity`` and
|
|
266
|
-
``available_energy_capacity``, which were **removed** in
|
|
266
|
+
``available_energy_capacity``, which were **removed** in v9.3.0. The
|
|
267
267
|
old names were misleading -- ``available_energy_capacity`` is the
|
|
268
268
|
energy *needed*, not the energy available -- and both were scaled
|
|
269
269
|
2.5x too large. For energy you can actually draw, see
|
|
@@ -45,7 +45,7 @@ __all__ = [
|
|
|
45
45
|
#: p90 4.45), agreeing to within 2% by a noisier route
|
|
46
46
|
#: * the same recoveries imply a heat pump COP of 2.89 at 4 Wh/count
|
|
47
47
|
#:
|
|
48
|
-
#: Library versions before
|
|
48
|
+
#: Library versions before 9.3.0 used 10 Wh/count, which overstated reported
|
|
49
49
|
#: tank energy by 2.5x and implied a physically impossible COP of 7.0.
|
|
50
50
|
#:
|
|
51
51
|
#: See ``docs/explanation/tank-energy.rst`` for the full derivation.
|
|
@@ -347,6 +347,13 @@ class MqttReconnectionHandler:
|
|
|
347
347
|
f"Error during reconnection attempt: {e}", exc_info=True
|
|
348
348
|
)
|
|
349
349
|
|
|
350
|
+
# A successful quick or deep reconnect builds a new connection, so
|
|
351
|
+
# the SDK never calls on_connection_resumed for it. Reset the
|
|
352
|
+
# counter here, or every later interruption starts with a longer
|
|
353
|
+
# backoff and an earlier deep reconnect than the first one did.
|
|
354
|
+
if self._is_connected_func():
|
|
355
|
+
self._reconnect_attempts = 0
|
|
356
|
+
|
|
350
357
|
# Check final state: report failure when retries are exhausted
|
|
351
358
|
# (limited mode) or a fatal error stopped the loop.
|
|
352
359
|
attempts_exhausted = (
|
|
@@ -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)
|
|
@@ -231,6 +231,45 @@ class TestReconnectLoopExceptionHandling:
|
|
|
231
231
|
assert all(d <= config.max_reconnect_delay for d in sleeps)
|
|
232
232
|
assert all(d > 0 for d in sleeps)
|
|
233
233
|
|
|
234
|
+
@pytest.mark.asyncio
|
|
235
|
+
@pytest.mark.parametrize("deep", [False, True])
|
|
236
|
+
async def test_successful_reconnect_resets_attempt_count(self, deep):
|
|
237
|
+
"""Regression: a quick or deep reconnect makes a new connection, so
|
|
238
|
+
on_connection_resumed never fires and the counter kept growing
|
|
239
|
+
across interruptions, lengthening each later backoff."""
|
|
240
|
+
connected = False
|
|
241
|
+
attempts = []
|
|
242
|
+
|
|
243
|
+
async def reconnect():
|
|
244
|
+
nonlocal connected
|
|
245
|
+
attempts.append(1)
|
|
246
|
+
# Fail the first try of each interruption, then succeed.
|
|
247
|
+
if len(attempts) % 2 == 0:
|
|
248
|
+
connected = True
|
|
249
|
+
return
|
|
250
|
+
raise MqttNotConnectedError("down")
|
|
251
|
+
|
|
252
|
+
kwargs = {"deep_reconnect_func": reconnect} if deep else {}
|
|
253
|
+
config = _fast_config(
|
|
254
|
+
max_reconnect_attempts=-1,
|
|
255
|
+
deep_reconnect_threshold=1,
|
|
256
|
+
)
|
|
257
|
+
handler = _make_handler(
|
|
258
|
+
config,
|
|
259
|
+
lambda: connected,
|
|
260
|
+
reconnect,
|
|
261
|
+
**kwargs,
|
|
262
|
+
)
|
|
263
|
+
handler.enable()
|
|
264
|
+
|
|
265
|
+
for _ in range(3):
|
|
266
|
+
connected = False
|
|
267
|
+
await handler._reconnect_with_backoff()
|
|
268
|
+
assert connected
|
|
269
|
+
assert handler.attempt_count == 0
|
|
270
|
+
|
|
271
|
+
assert len(attempts) == 6
|
|
272
|
+
|
|
234
273
|
|
|
235
274
|
class TestDisconnectDuringInterruption:
|
|
236
275
|
"""disconnect() must fully shut down even when not connected."""
|
|
@@ -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
|