nwp500-python 9.5.0__tar.gz → 9.6.0__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.5.0 → nwp500_python-9.6.0}/CHANGELOG.rst +51 -0
- {nwp500_python-9.5.0/src/nwp500_python.egg-info → nwp500_python-9.6.0}/PKG-INFO +2 -4
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/README.rst +1 -3
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/calibrate-simulator.rst +80 -39
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/python_api/sim.rst +5 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/setup.cfg +11 -0
- nwp500_python-9.6.0/src/nwp500/cli/calibration.py +190 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim_adapters.py +126 -62
- nwp500_python-9.5.0/src/nwp500/cli/calibration.py → nwp500_python-9.6.0/src/nwp500/sim_calibrate.py +149 -191
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim_home_assistant.py +8 -4
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/status_history.py +12 -5
- {nwp500_python-9.5.0 → nwp500_python-9.6.0/src/nwp500_python.egg-info}/PKG-INFO +2 -4
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500_python.egg-info/SOURCES.txt +2 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500_python.egg-info/scm_file_list.json +2 -0
- nwp500_python-9.6.0/src/nwp500_python.egg-info/scm_version.json +8 -0
- nwp500_python-9.6.0/tests/test_cli_calibrate.py +217 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_adapters.py +45 -3
- nwp500_python-9.5.0/tests/test_cli_calibrate.py → nwp500_python-9.6.0/tests/test_sim_calibrate.py +119 -157
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_home_assistant.py +15 -3
- nwp500_python-9.5.0/src/nwp500_python.egg-info/scm_version.json +0 -8
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/.coveragerc +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/.github/workflows/ci.yml +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/.gitignore +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/.readthedocs.yml +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/AUTHORS.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/LICENSE.txt +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/Makefile +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/RELEASE.md +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/Makefile +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/conf.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/explanation/draws.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/explanation/heating-elements.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/explanation/index.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/explanation/recovery-speed.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/explanation/simulator.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/explanation/standby-loss.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/explanation/tank-energy.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/explanation/tou-recovery-cap.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/explanation/unknown-values.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/explanation/what-starts-a-recovery.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/bluetooth.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/maintenance.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/manage-units.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/optimize-tou.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/schedule-operation.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/simulate-heater.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/how-to/track-energy.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/index.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/openapi.yaml +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/project/authors.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/project/changelog.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/project/history.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/project/license.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/index.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/installation.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/protocol/data_conversions.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/protocol/device_status.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/protocol/quick_reference.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/python_api/cli.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/python_api/events.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/python_api/models.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/python_api/mqtt_client.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/reference/simulator-inputs.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/requirements.txt +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/.ruff.toml +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/README.md +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/air_filter_reset.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/auto_recovery.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/combined_callbacks.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/demand_response.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/device_capabilities.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/device_status_debug.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/energy_analytics.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/firmware_payload_capture.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/mqtt_diagnostics.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/power_control.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/recirculation_control.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/reconnection_demo.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/simple_auto_recovery.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/token_restoration.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/tou_schedule.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/advanced/water_reservation.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/beginner/01_authentication.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/beginner/02_list_devices.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/beginner/04_set_temperature.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/intermediate/command_queue.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/intermediate/device_status_callback.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/intermediate/event_driven_control.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/intermediate/periodic_requests.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/intermediate/set_mode.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/intermediate/vacation_mode.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/mask.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/simulator/lower-setpoint.json +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/simulator/plan.json +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/simulator/scenario.json +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/testing/periodic_device_info.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/testing/test_api_client.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/testing/test_mqtt_connection.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/testing/test_mqtt_messaging.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/examples/testing/test_periodic_minimal.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/pyproject.toml +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/scripts/README.md +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/scripts/bump_version.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/scripts/format.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/scripts/lint.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/scripts/validate_version.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/setup.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/__init__.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/ble/__init__.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/ble/__main__.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/ble/client.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/ble/mqtt_adapter.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/ble/protocol.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/cli/__main__.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/cli/ble_backend.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/cli/handlers.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/cli/output_formatters.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/cli/presentation.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/cli/recording.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/cli/rich_output.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/cli/simulate.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/config.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/converters.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/device_capabilities.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/encoding.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/enums.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/events.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/models/__init__.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/models/account.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/models/device.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/models/diagnostics.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/models/energy.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/models/feature.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/models/firmware.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/models/maintenance.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/models/schedule.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/models/status.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/models/tou.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/__init__.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/_control_commands.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/client.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/command_queue.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/connection.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/control.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/state_tracker.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/subscriptions.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/types.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt/utils.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/mqtt_events.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/reservations.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/__init__.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/api.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/attribution.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/book.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/calibration.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/circulation.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/codec.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/cold_supply.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/controller.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/decay.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/defaults.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/delivery.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/draw_forecast.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/engine.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/extensions.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/heat_pump_recovery.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/heating_trajectory.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/inputs.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/location_air.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/meter.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/observed.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/params.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/physics/__init__.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/physics/compressor.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/physics/draw.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/physics/element.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/physics/refrigerant.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/physics/tank.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/profile.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/ready.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/record.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/result.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/schedules.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/tank_physics.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/tou_triggers.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim/units.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/sim_live.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/tou_schedule.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/_sim_site.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/conftest.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/fixtures/sim/engine_golden.json +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/fixtures/sim/heater_params.json +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/fixtures/sim/heater_params_fitted.json +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_api_endpoints_201.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_app_queries.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_auth.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_auth_session_refresh.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_ble_mqtt_adapter.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_ble_protocol.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_canonical_schedule.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_cli_commands.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_cli_simulate.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_cli_tristate_presentation.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_command_queue.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_device_capabilities.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_device_rest_models.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_events.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_model_converters.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_models.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_mqtt_control_fail.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_mqtt_events.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_mqtt_last_will.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_mqtt_reliability.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_multi_device.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_navilink_201_enums.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_openei.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_protocol_correctness.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_public_api.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_reservation_validation.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_reservations.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_api.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_book.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_book_history.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_calibration.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_circulation.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_codec.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_cold_supply.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_controller.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_defaults.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_draw_forecast.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_draw_loss.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_element.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_element_fit.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_forecast.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_golden.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_hot_water.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_import_contract.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_live.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_location_air.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_meter.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_no_clock_no_state.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_observed.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_profile.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_readiness.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_readiness_fit.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_ready.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_ready_api.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_replay.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_schedules.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_sim_tank_physics.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_status_history.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_tou_schedule.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_unit_system_process_wide.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tests/test_utils.py +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/tox.ini +0 -0
- {nwp500_python-9.5.0 → nwp500_python-9.6.0}/uv.lock +0 -0
|
@@ -2,6 +2,57 @@
|
|
|
2
2
|
Changelog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
Version 9.6.0 (2026-10-21)
|
|
6
|
+
==========================
|
|
7
|
+
|
|
8
|
+
Added
|
|
9
|
+
-----
|
|
10
|
+
- **Calibration outside the CLI** (``nwp500.sim_calibrate``, #177).
|
|
11
|
+
``calibrate``, ``Calibrated``, ``report``, ``Term`` and ``max_gap``
|
|
12
|
+
moved here from ``nwp500.cli.calibration`` so an application can
|
|
13
|
+
calibrate without ``click`` or ``rich``. ``calibrate`` takes
|
|
14
|
+
``HeaterParams`` or a parameters document as ``params`` and raises
|
|
15
|
+
``CalibrationInputError`` (a ``ValueError``). Its notes no longer name
|
|
16
|
+
the command's options; ``nwp-cli simulate calibrate`` adds them.
|
|
17
|
+
- ``status_history.iter_read`` yields a recording's statuses one at a
|
|
18
|
+
time.
|
|
19
|
+
- ``sim_adapters.add_inputs`` adds the air, its humidity, a meter's
|
|
20
|
+
draws, the circulation's runs and the unobserved gaps to a frame
|
|
21
|
+
``sim_adapters.history`` built.
|
|
22
|
+
|
|
23
|
+
Changed
|
|
24
|
+
-------
|
|
25
|
+
- **A long history calibrates in bounded memory** (#177).
|
|
26
|
+
``sim_adapters.history`` reads its statuses one at a time and keeps
|
|
27
|
+
only the frame's values, and ``sim_calibrate.calibrate`` takes the
|
|
28
|
+
record's span and gaps from the frame, so a generator over day files
|
|
29
|
+
is read once. 60 days of statuses (about 540,000) now take about
|
|
30
|
+
150 MB instead of about 6 GB of parsed statuses. The drawing and
|
|
31
|
+
circulating columns are computed per span instead of per row and
|
|
32
|
+
span.
|
|
33
|
+
|
|
34
|
+
Documentation
|
|
35
|
+
-------------
|
|
36
|
+
- The calibration how-to says the recorder writes every status the
|
|
37
|
+
heater sends and what that costs on disk, shows calibrating from
|
|
38
|
+
day files with ``sim_calibrate``, adds calibrating in an
|
|
39
|
+
application, and says what Home Assistant's 10-day retention means
|
|
40
|
+
for a longer record.
|
|
41
|
+
|
|
42
|
+
Version 9.5.1 (2026-10-21)
|
|
43
|
+
==========================
|
|
44
|
+
|
|
45
|
+
Fixed
|
|
46
|
+
-----
|
|
47
|
+
- **A steady Home Assistant sensor no longer goes stale in the
|
|
48
|
+
simulator** (#174). ``sim_home_assistant.parse_state`` dated a state by
|
|
49
|
+
``last_updated``, which moves only when the value or its attributes
|
|
50
|
+
change. An air sensor holding the same value for over an hour failed
|
|
51
|
+
``fresh()``, and the simulation fell back to the default air
|
|
52
|
+
temperature. A state is now dated by ``last_reported`` (Home Assistant
|
|
53
|
+
2024.3 and later), then ``last_updated``, then ``last_changed``. The
|
|
54
|
+
history parsers still date each change row by its change.
|
|
55
|
+
|
|
5
56
|
Version 9.5.0 (2026-10-20)
|
|
6
57
|
==========================
|
|
7
58
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nwp500-python
|
|
3
|
-
Version: 9.
|
|
3
|
+
Version: 9.6.0
|
|
4
4
|
Summary: A library for controlling Navien NWP500 Water Heaters via NaviLink
|
|
5
5
|
Home-page: https://github.com/eman/nwp500-python
|
|
6
6
|
Author: Emmanuel Levijarvi
|
|
@@ -72,9 +72,7 @@ Features
|
|
|
72
72
|
* **Monitoring:** Real-time tracking of temperature, power usage, tank charge, and component status.
|
|
73
73
|
* **Control:** Remote control of target temperatures, operation modes, and vacation settings.
|
|
74
74
|
* **Scheduling:** Reservations, time-of-use (TOU) schedules and anti-legionella cycles.
|
|
75
|
-
* **
|
|
76
|
-
* **Async:** Built on asyncio.
|
|
77
|
-
* **Simulator (optional):** A physics model of the heater, ``nwp500.sim``, that answers "when will the tank be hot?", "how much hot water is there?" and "what would this change do?", and calibrates from a heater's own history. ``nwp-cli simulate`` runs it on the heater as it is now or on a scenario file. Install it with ``pip install nwp500-python[sim]``.
|
|
75
|
+
* **Simulator (optional):** ``nwp500.sim``, a physics model of the heater. It estimates recovery time and available hot water, projects the effect of a change to the schedule or settings, and calibrates against a heater's own history. ``nwp-cli simulate`` runs it against the live heater or a scenario file. Install with ``pip install nwp500-python[sim]``.
|
|
78
76
|
|
|
79
77
|
Getting Started
|
|
80
78
|
===============
|
|
@@ -18,9 +18,7 @@ Features
|
|
|
18
18
|
* **Monitoring:** Real-time tracking of temperature, power usage, tank charge, and component status.
|
|
19
19
|
* **Control:** Remote control of target temperatures, operation modes, and vacation settings.
|
|
20
20
|
* **Scheduling:** Reservations, time-of-use (TOU) schedules and anti-legionella cycles.
|
|
21
|
-
* **
|
|
22
|
-
* **Async:** Built on asyncio.
|
|
23
|
-
* **Simulator (optional):** A physics model of the heater, ``nwp500.sim``, that answers "when will the tank be hot?", "how much hot water is there?" and "what would this change do?", and calibrates from a heater's own history. ``nwp-cli simulate`` runs it on the heater as it is now or on a scenario file. Install it with ``pip install nwp500-python[sim]``.
|
|
21
|
+
* **Simulator (optional):** ``nwp500.sim``, a physics model of the heater. It estimates recovery time and available hot water, projects the effect of a change to the schedule or settings, and calibrates against a heater's own history. ``nwp-cli simulate`` runs it against the live heater or a scenario file. Install with ``pip install nwp500-python[sim]``.
|
|
24
22
|
|
|
25
23
|
Getting Started
|
|
26
24
|
===============
|
|
@@ -14,9 +14,13 @@ fit its standby loss, its compressor and its power to that record.
|
|
|
14
14
|
Record the history
|
|
15
15
|
==================
|
|
16
16
|
|
|
17
|
-
``nwp-cli simulate record``
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
``nwp-cli simulate record`` appends every status the heater sends to a
|
|
18
|
+
file until you stop it (Ctrl+C): the answers to the status requests it
|
|
19
|
+
makes every 30 seconds (``--every``), and the statuses the heater sends
|
|
20
|
+
between them. One heater sent about 370 an hour, about half of them
|
|
21
|
+
identical to the one before. A line is about 2.3 kB, so a day of
|
|
22
|
+
recording is about 20 MB, or 0.2 MB gzipped. The command needs the
|
|
23
|
+
``cli`` extra and no other:
|
|
20
24
|
|
|
21
25
|
.. code-block:: bash
|
|
22
26
|
|
|
@@ -35,11 +39,15 @@ can have gaps. The heater may have heated or delivered water during a
|
|
|
35
39
|
gap, so ``calibrate`` takes any spacing between statuses of more than
|
|
36
40
|
4 times the record's usual spacing (and at least 5 minutes) as
|
|
37
41
|
unobserved. No quiet window and no recovery spans such a gap, and the
|
|
38
|
-
report counts them.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
report counts them. The identical statuses are what show the heater was
|
|
43
|
+
observed between changes, so keep them.
|
|
44
|
+
|
|
45
|
+
Write the file any other way you like (an MQTT callback, an export
|
|
46
|
+
from a home-automation recorder) as long as each line has this shape:
|
|
47
|
+
``nwp500.status_history.line(instant, raw_status)`` writes one.
|
|
48
|
+
``status_history.read(lines)`` reads them back as
|
|
49
|
+
``(instant, DeviceStatus)`` pairs, and ``status_history.iter_read(lines)``
|
|
50
|
+
yields them one at a time, for a long recording.
|
|
43
51
|
|
|
44
52
|
Record through quiet nights and full recoveries. The standby fit needs
|
|
45
53
|
windows of 3 hours or more with no heating and no hot water in use. The
|
|
@@ -75,10 +83,14 @@ Idle, it reads the cabinet, about 6 °F warm. So give one of these:
|
|
|
75
83
|
air beside the heater and its humidity over the record. It also reads
|
|
76
84
|
the meter's draws and the circulation's runs, which make the quiet
|
|
77
85
|
test exact: the heater's own "hot water in use" flag also fires on
|
|
78
|
-
circulation.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
circulation. The air is used only over the span its history covers,
|
|
87
|
+
and not while its sensor was ``unavailable``; the humidity takes the
|
|
88
|
+
50 % default outside that span, and the report says how much that
|
|
89
|
+
is. Home Assistant keeps 10 days of history by default, so of a
|
|
90
|
+
60-day record only the last 10 days have the air, and the standby
|
|
91
|
+
loss is fitted from those alone. For the whole record, raise the
|
|
92
|
+
recorder's ``purge_keep_days`` to cover it before you start
|
|
93
|
+
recording, or give the air another way (``--air``). Where the
|
|
82
94
|
meter's or the circulation's history doesn't cover the whole record,
|
|
83
95
|
the heater's own flag is used instead. ``--air``, ``--ambient`` and
|
|
84
96
|
``--humidity`` override the sources.
|
|
@@ -158,37 +170,66 @@ meter's draws, which the command does not take (see below).
|
|
|
158
170
|
From Python
|
|
159
171
|
===========
|
|
160
172
|
|
|
161
|
-
|
|
173
|
+
``nwp500.sim_calibrate.calibrate`` does what the command does, and
|
|
174
|
+
``report`` gives the command's rows as ``Term`` values. It takes the
|
|
175
|
+
statuses one at a time, so a recording kept as a file per day can be
|
|
176
|
+
read in order without holding every status:
|
|
162
177
|
|
|
163
178
|
.. code-block:: python
|
|
164
179
|
|
|
165
|
-
from
|
|
180
|
+
from pathlib import Path
|
|
181
|
+
|
|
182
|
+
from nwp500 import sim, sim_calibrate, status_history
|
|
166
183
|
|
|
167
|
-
|
|
168
|
-
|
|
184
|
+
def recorded(days):
|
|
185
|
+
for day in days:
|
|
186
|
+
with day.open(encoding="utf-8") as f:
|
|
187
|
+
yield from status_history.iter_read(f)
|
|
169
188
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
history = sa.history(
|
|
173
|
-
readings,
|
|
189
|
+
done = sim_calibrate.calibrate(
|
|
190
|
+
recorded(sorted(Path("history").glob("*.jsonl"))),
|
|
174
191
|
air=air_readings, # [(instant, temperature), ...]
|
|
175
192
|
humidity=humidity_readings, # [(instant, %RH), ...]
|
|
176
|
-
|
|
193
|
+
params=sim.default_params(), # or your own HeaterParams
|
|
177
194
|
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
Give
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
params = done.calibration.params
|
|
196
|
+
for term in sim_calibrate.report(done):
|
|
197
|
+
print(term.name, term.value, "fitted" if term.fitted else term.why)
|
|
198
|
+
|
|
199
|
+
Give ``ambient`` instead of ``air`` for a constant, or neither: the
|
|
200
|
+
standby loss then keeps the base's value. Without ``humidity`` the fits
|
|
201
|
+
get 50 %. An input it cannot use raises ``CalibrationInputError``.
|
|
202
|
+
|
|
203
|
+
On a desktop computer, 60 days of statuses (about 540,000) took about
|
|
204
|
+
40 seconds to read and 10 to fit, in about 150 MB.
|
|
205
|
+
|
|
206
|
+
Calibrate in an application
|
|
207
|
+
---------------------------
|
|
208
|
+
|
|
209
|
+
An application that records the heater itself, such as a Home
|
|
210
|
+
Assistant integration, can calibrate the same way:
|
|
211
|
+
|
|
212
|
+
1. Write each status it receives with ``status_history.line``, a file
|
|
213
|
+
per UTC day, and keep as many days as the fit should cover.
|
|
214
|
+
2. Read the air beside the heater and its humidity over the same span
|
|
215
|
+
with ``sim_home_assistant.record_inputs(client, entities, start,
|
|
216
|
+
end)``, and pass the result as ``record=``. It also brings the
|
|
217
|
+
meter's draws and the circulation's runs. Any client with the same
|
|
218
|
+
``read_history`` as ``HomeAssistantClient`` works. The recorder's
|
|
219
|
+
retention limits the air, as above.
|
|
220
|
+
3. Call ``sim_calibrate.calibrate`` in a worker thread. It takes the
|
|
221
|
+
gaps from the statuses' own spacing: a span longer than 4 times
|
|
222
|
+
their usual spacing, and at least 5 minutes, is unobserved.
|
|
223
|
+
4. Store ``params_to_document(done.calibration.params)``, and show the
|
|
224
|
+
``report(done)`` rows and ``done.notes``.
|
|
225
|
+
|
|
226
|
+
``sim.calibration.calibrate`` underneath takes the frame
|
|
227
|
+
``sim_adapters.history`` builds; ``sim_adapters.add_inputs`` adds the
|
|
228
|
+
air, the humidity, a meter's draws and the circulation's runs to it.
|
|
229
|
+
From there you can also give the draws that price the heat each
|
|
230
|
+
cycle's draws took. The fits for the readiness terms, the heat book,
|
|
231
|
+
the draw response and the elements also run there: see "Improve
|
|
232
|
+
accuracy" and "Fit the elements" in :doc:`simulate-heater`.
|
|
192
233
|
|
|
193
234
|
|
|
194
235
|
Limitations
|
|
@@ -202,13 +243,13 @@ Limitations
|
|
|
202
243
|
heater's own "hot water in use" flag, which also fires on
|
|
203
244
|
circulation. A loop's heat that does not trip the flag is invisible
|
|
204
245
|
to the heater and lands in the standby loss. From Python, pass the
|
|
205
|
-
circulation's runs (``sa.history(...,
|
|
206
|
-
them out of the quiet windows.
|
|
246
|
+
circulation's runs (``record=`` or ``sa.history(...,
|
|
247
|
+
circulating=...)``) to keep them out of the quiet windows.
|
|
207
248
|
|
|
208
249
|
|
|
209
250
|
See also
|
|
210
251
|
========
|
|
211
252
|
|
|
212
253
|
* :doc:`simulate-heater`: run the simulator with your parameters
|
|
213
|
-
* :doc:`/reference/python_api/sim`: ``
|
|
214
|
-
``sim_adapters.history`` and ``status_history``
|
|
254
|
+
* :doc:`/reference/python_api/sim`: ``sim_calibrate``,
|
|
255
|
+
``sim.calibration``, ``sim_adapters.history`` and ``status_history``
|
|
@@ -297,6 +297,11 @@ Recorded history
|
|
|
297
297
|
.. automodule:: nwp500.status_history
|
|
298
298
|
:members:
|
|
299
299
|
|
|
300
|
+
.. automodule:: nwp500.sim_calibrate
|
|
301
|
+
:members: calibrate, Calibrated, report, Term, max_gap,
|
|
302
|
+
CalibrationInputError, DEFAULT_HUMIDITY_PCT, GAP_SAMPLES, MIN_GAP,
|
|
303
|
+
DEFAULT_BASE_NOTE, NO_AIR_NOTE
|
|
304
|
+
|
|
300
305
|
|
|
301
306
|
Home Assistant sources
|
|
302
307
|
======================
|
|
@@ -112,6 +112,16 @@ forbidden_modules =
|
|
|
112
112
|
awsiot
|
|
113
113
|
logging
|
|
114
114
|
|
|
115
|
+
[importlinter:contract:calibrate-no-cli]
|
|
116
|
+
name = An application calibrates without the CLI's dependencies
|
|
117
|
+
type = forbidden
|
|
118
|
+
source_modules =
|
|
119
|
+
nwp500.sim_calibrate
|
|
120
|
+
forbidden_modules =
|
|
121
|
+
nwp500.cli
|
|
122
|
+
click
|
|
123
|
+
rich
|
|
124
|
+
|
|
115
125
|
[importlinter:contract:ble-optional]
|
|
116
126
|
name = Only nwp500.ble imports bleak, and only nwp-cli's ble backend imports nwp500.ble
|
|
117
127
|
type = forbidden
|
|
@@ -138,6 +148,7 @@ source_modules =
|
|
|
138
148
|
nwp500.reservations
|
|
139
149
|
nwp500.sim
|
|
140
150
|
nwp500.sim_adapters
|
|
151
|
+
nwp500.sim_calibrate
|
|
141
152
|
nwp500.sim_home_assistant
|
|
142
153
|
nwp500.sim_live
|
|
143
154
|
nwp500.status_history
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""``nwp-cli simulate calibrate``: fit the heater's parameters to its
|
|
2
|
+
recorded history.
|
|
3
|
+
|
|
4
|
+
The history is the statuses the heater reported
|
|
5
|
+
(``nwp500.status_history``, as ``nwp-cli simulate record`` writes them).
|
|
6
|
+
The fits and the report's terms are ``nwp500.sim_calibrate``'s; this
|
|
7
|
+
module reads the command's files and prints the report.
|
|
8
|
+
|
|
9
|
+
This module needs the ``sim`` extra.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import csv
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import math
|
|
16
|
+
import sys
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from dataclasses import replace
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, TextIO
|
|
22
|
+
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
|
|
26
|
+
from nwp500 import sim_calibrate as sc
|
|
27
|
+
from nwp500 import status_history
|
|
28
|
+
from nwp500.sim.params import params_to_document
|
|
29
|
+
from nwp500.sim_calibrate import Calibrated, Term, report
|
|
30
|
+
|
|
31
|
+
from .simulate import InputError, read_params
|
|
32
|
+
|
|
33
|
+
__author__ = "Emmanuel Levijarvi"
|
|
34
|
+
__copyright__ = "Emmanuel Levijarvi"
|
|
35
|
+
__license__ = "MIT"
|
|
36
|
+
|
|
37
|
+
_logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
#: A reading: an instant and a value.
|
|
40
|
+
Reading = tuple[datetime, float]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def read_history(path: str | Path) -> list[status_history.TimedStatus]:
|
|
44
|
+
"""The statuses recorded at ``path``."""
|
|
45
|
+
try:
|
|
46
|
+
with Path(path).open(encoding="utf-8") as f:
|
|
47
|
+
readings = status_history.read(f)
|
|
48
|
+
except (OSError, ValueError) as e:
|
|
49
|
+
raise InputError(f"cannot read the history {path}: {e}") from None
|
|
50
|
+
if not readings:
|
|
51
|
+
raise InputError(f"the history {path} holds no statuses")
|
|
52
|
+
return readings
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def read_readings(path: str | Path, what: str) -> list[Reading]:
|
|
56
|
+
"""A CSV file's readings: an ISO 8601 instant with its offset, then
|
|
57
|
+
a value, on each row. A first row that is not one is a header."""
|
|
58
|
+
try:
|
|
59
|
+
with Path(path).open(encoding="utf-8", newline="") as f:
|
|
60
|
+
rows = [r for r in csv.reader(f) if r and any(c.strip() for c in r)]
|
|
61
|
+
except OSError as e:
|
|
62
|
+
raise InputError(f"cannot read the {what} {path}: {e}") from None
|
|
63
|
+
out: list[Reading] = []
|
|
64
|
+
for number, row in enumerate(rows, start=1):
|
|
65
|
+
try:
|
|
66
|
+
at = datetime.fromisoformat(row[0].strip())
|
|
67
|
+
value = float(row[1])
|
|
68
|
+
except IndexError, ValueError:
|
|
69
|
+
if number == 1:
|
|
70
|
+
continue
|
|
71
|
+
raise InputError(
|
|
72
|
+
f"row {number} of the {what} {path} is not an instant "
|
|
73
|
+
"and a value"
|
|
74
|
+
) from None
|
|
75
|
+
if at.tzinfo is None:
|
|
76
|
+
raise InputError(
|
|
77
|
+
f"row {number} of the {what} {path}: give the instant's "
|
|
78
|
+
"UTC offset"
|
|
79
|
+
)
|
|
80
|
+
if math.isfinite(value):
|
|
81
|
+
out.append((at, value))
|
|
82
|
+
if not out:
|
|
83
|
+
raise InputError(f"the {what} {path} holds no readings")
|
|
84
|
+
return out
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def read_record(
|
|
88
|
+
sources: Any, readings: Sequence[status_history.TimedStatus]
|
|
89
|
+
) -> Any:
|
|
90
|
+
"""What ``sources`` recorded over ``readings``' span
|
|
91
|
+
(``sim_home_assistant.record_inputs``)."""
|
|
92
|
+
from nwp500 import sim_home_assistant as ha
|
|
93
|
+
|
|
94
|
+
first = min(t for t, _ in readings)
|
|
95
|
+
last = max(t for t, _ in readings)
|
|
96
|
+
async with ha.HomeAssistantClient(sources.url, sources.token) as client:
|
|
97
|
+
return await ha.record_inputs(client, sources.entities, first, last)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
#: The library's notes, naming the command's options.
|
|
101
|
+
_OPTION_NOTES = {
|
|
102
|
+
sc.DEFAULT_BASE_NOTE: "refitting the 65 gal defaults (give --params "
|
|
103
|
+
"for another tank size)",
|
|
104
|
+
sc.NO_AIR_NOTE: "no air given (--air, --ambient or --sources): the "
|
|
105
|
+
"standby loss is not fitted",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def calibrate(
|
|
110
|
+
readings: Sequence[status_history.TimedStatus],
|
|
111
|
+
*,
|
|
112
|
+
air: Sequence[Reading] | None = None,
|
|
113
|
+
ambient: float | None = None,
|
|
114
|
+
humidity: Sequence[Reading] | None = None,
|
|
115
|
+
params: Any = None,
|
|
116
|
+
record: Any = None,
|
|
117
|
+
) -> Calibrated:
|
|
118
|
+
"""``sim_calibrate.calibrate`` with ``params`` a parameters
|
|
119
|
+
document, its errors as ``InputError`` and its notes naming the
|
|
120
|
+
command's options."""
|
|
121
|
+
base = None if params is None else read_params(params, "--params")
|
|
122
|
+
try:
|
|
123
|
+
done = sc.calibrate(
|
|
124
|
+
readings,
|
|
125
|
+
air=air,
|
|
126
|
+
ambient=ambient,
|
|
127
|
+
humidity=humidity,
|
|
128
|
+
params=base,
|
|
129
|
+
record=record,
|
|
130
|
+
)
|
|
131
|
+
except sc.CalibrationInputError as e:
|
|
132
|
+
raise InputError(str(e)) from None
|
|
133
|
+
return replace(
|
|
134
|
+
done, notes=tuple(_OPTION_NOTES.get(n, n) for n in done.notes)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _kept(t: Term) -> str:
|
|
139
|
+
if t.fitted:
|
|
140
|
+
return f"fitted: {t.note}" if t.note else "fitted"
|
|
141
|
+
return f"base: {t.why}" if t.why else "base"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def print_report(done: Calibrated, console: Console) -> None:
|
|
145
|
+
"""The report as tables on ``console``."""
|
|
146
|
+
c = done.calibration
|
|
147
|
+
console.print(
|
|
148
|
+
f"{done.n_statuses} statuses; quiet test: {c.standby.quiet_by}; "
|
|
149
|
+
f"{len(c.recoveries)} recoveries found"
|
|
150
|
+
)
|
|
151
|
+
if done.n_gaps:
|
|
152
|
+
minutes = done.max_gap.total_seconds() / 60
|
|
153
|
+
console.print(
|
|
154
|
+
f"{done.n_gaps} gap{'' if done.n_gaps == 1 else 's'} of over "
|
|
155
|
+
f"{minutes:g} min between statuses: no fit spans them"
|
|
156
|
+
)
|
|
157
|
+
console.print(
|
|
158
|
+
{
|
|
159
|
+
"readings": "Air: the readings given",
|
|
160
|
+
"constant": "Air: the constant given",
|
|
161
|
+
"source": "Air: the sources' record beside the heater",
|
|
162
|
+
None: "Air: none given; recoveries use the heater's own reading",
|
|
163
|
+
}[done.air]
|
|
164
|
+
)
|
|
165
|
+
table = Table(title="Calibration")
|
|
166
|
+
for column in ("Term", "Value", "From", "Error", "Kept"):
|
|
167
|
+
table.add_column(column)
|
|
168
|
+
for t in report(done):
|
|
169
|
+
table.add_row(
|
|
170
|
+
t.name,
|
|
171
|
+
t.value,
|
|
172
|
+
t.samples,
|
|
173
|
+
t.error,
|
|
174
|
+
_kept(t),
|
|
175
|
+
)
|
|
176
|
+
console.print(table)
|
|
177
|
+
unfitted = ", ".join(c.params.unfitted) or "none"
|
|
178
|
+
console.print(f"Unfitted: {unfitted}")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def run(
|
|
182
|
+
done: Calibrated, output: str | Path | None, out: TextIO | None = None
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Print the report and write the parameters to ``output``."""
|
|
185
|
+
for note in done.notes:
|
|
186
|
+
_logger.warning(note)
|
|
187
|
+
print_report(done, Console(file=out or sys.stdout))
|
|
188
|
+
if output is not None:
|
|
189
|
+
text = json.dumps(params_to_document(done.calibration.params), indent=2)
|
|
190
|
+
Path(output).write_text(text + "\n", encoding="utf-8")
|