nwp500-python 9.5.1__tar.gz → 9.7.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.1 → nwp500_python-9.7.0}/CHANGELOG.rst +59 -0
- {nwp500_python-9.5.1/src/nwp500_python.egg-info → nwp500_python-9.7.0}/PKG-INFO +1 -1
- nwp500_python-9.7.0/docs/how-to/calibrate-simulator.rst +337 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/python_api/sim.rst +8 -1
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/setup.cfg +11 -0
- nwp500_python-9.7.0/src/nwp500/cli/calibration.py +190 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim_adapters.py +299 -63
- nwp500_python-9.5.1/src/nwp500/cli/calibration.py → nwp500_python-9.7.0/src/nwp500/sim_calibrate.py +225 -196
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim_home_assistant.py +288 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/status_history.py +12 -5
- {nwp500_python-9.5.1 → nwp500_python-9.7.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/SOURCES.txt +3 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/scm_file_list.json +3 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/scm_version.json +2 -2
- nwp500_python-9.7.0/tests/test_cli_calibrate.py +217 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_adapters.py +45 -3
- nwp500_python-9.5.1/tests/test_cli_calibrate.py → nwp500_python-9.7.0/tests/test_sim_calibrate.py +119 -157
- nwp500_python-9.7.0/tests/test_sim_change_history.py +527 -0
- nwp500_python-9.5.1/docs/how-to/calibrate-simulator.rst +0 -214
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/.coveragerc +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/.github/workflows/ci.yml +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/.gitignore +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/.readthedocs.yml +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/AUTHORS.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/LICENSE.txt +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/Makefile +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/README.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/RELEASE.md +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/Makefile +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/conf.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/explanation/draws.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/explanation/heating-elements.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/explanation/index.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/explanation/recovery-speed.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/explanation/simulator.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/explanation/standby-loss.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/explanation/tank-energy.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/explanation/tou-recovery-cap.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/explanation/unknown-values.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/explanation/what-starts-a-recovery.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/bluetooth.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/maintenance.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/manage-units.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/optimize-tou.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/schedule-operation.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/simulate-heater.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/how-to/track-energy.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/index.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/openapi.yaml +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/project/authors.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/project/changelog.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/project/history.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/project/license.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/index.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/installation.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/protocol/data_conversions.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/protocol/device_status.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/protocol/quick_reference.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/python_api/cli.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/python_api/events.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/python_api/models.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/python_api/mqtt_client.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/reference/simulator-inputs.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/requirements.txt +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/.ruff.toml +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/README.md +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/air_filter_reset.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/auto_recovery.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/combined_callbacks.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/demand_response.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/device_capabilities.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/device_status_debug.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/energy_analytics.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/firmware_payload_capture.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/mqtt_diagnostics.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/power_control.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/recirculation_control.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/reconnection_demo.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/simple_auto_recovery.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/token_restoration.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/tou_schedule.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/advanced/water_reservation.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/beginner/01_authentication.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/beginner/02_list_devices.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/beginner/04_set_temperature.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/intermediate/command_queue.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/intermediate/device_status_callback.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/intermediate/event_driven_control.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/intermediate/periodic_requests.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/intermediate/set_mode.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/intermediate/vacation_mode.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/mask.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/simulator/lower-setpoint.json +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/simulator/plan.json +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/simulator/scenario.json +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/testing/periodic_device_info.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/testing/test_api_client.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/testing/test_mqtt_connection.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/testing/test_mqtt_messaging.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/examples/testing/test_periodic_minimal.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/pyproject.toml +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/scripts/README.md +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/scripts/bump_version.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/scripts/format.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/scripts/lint.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/scripts/validate_version.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/setup.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/__init__.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/ble/__init__.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/ble/__main__.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/ble/client.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/ble/mqtt_adapter.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/ble/protocol.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/cli/__main__.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/cli/ble_backend.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/cli/handlers.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/cli/output_formatters.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/cli/presentation.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/cli/recording.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/cli/rich_output.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/cli/simulate.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/config.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/converters.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/device_capabilities.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/encoding.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/enums.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/events.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/models/__init__.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/models/account.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/models/device.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/models/diagnostics.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/models/energy.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/models/feature.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/models/firmware.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/models/maintenance.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/models/schedule.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/models/status.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/models/tou.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/__init__.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/_control_commands.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/client.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/command_queue.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/connection.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/control.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/state_tracker.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/subscriptions.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/types.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt/utils.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/mqtt_events.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/reservations.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/__init__.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/api.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/attribution.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/book.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/calibration.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/circulation.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/codec.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/cold_supply.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/controller.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/decay.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/defaults.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/delivery.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/draw_forecast.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/engine.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/extensions.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/heat_pump_recovery.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/heating_trajectory.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/inputs.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/location_air.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/meter.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/observed.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/params.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/physics/__init__.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/physics/compressor.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/physics/draw.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/physics/element.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/physics/refrigerant.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/physics/tank.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/profile.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/ready.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/record.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/result.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/schedules.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/tank_physics.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/tou_triggers.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim/units.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/sim_live.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/tou_schedule.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/_sim_site.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/conftest.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/fixtures/sim/engine_golden.json +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/fixtures/sim/heater_params.json +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/fixtures/sim/heater_params_fitted.json +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_api_endpoints_201.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_app_queries.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_auth.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_auth_session_refresh.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_ble_mqtt_adapter.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_ble_protocol.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_canonical_schedule.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_cli_commands.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_cli_simulate.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_cli_tristate_presentation.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_command_queue.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_device_capabilities.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_device_rest_models.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_events.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_model_converters.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_models.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_mqtt_control_fail.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_mqtt_events.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_mqtt_last_will.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_mqtt_reliability.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_multi_device.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_navilink_201_enums.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_openei.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_protocol_correctness.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_public_api.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_reservation_validation.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_reservations.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_api.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_book.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_book_history.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_calibration.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_circulation.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_codec.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_cold_supply.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_controller.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_defaults.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_draw_forecast.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_draw_loss.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_element.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_element_fit.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_forecast.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_golden.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_home_assistant.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_hot_water.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_import_contract.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_live.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_location_air.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_meter.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_no_clock_no_state.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_observed.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_profile.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_readiness.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_readiness_fit.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_ready.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_ready_api.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_replay.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_schedules.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_sim_tank_physics.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_status_history.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_tou_schedule.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_unit_system_process_wide.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tests/test_utils.py +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/tox.ini +0 -0
- {nwp500_python-9.5.1 → nwp500_python-9.7.0}/uv.lock +0 -0
|
@@ -2,6 +2,65 @@
|
|
|
2
2
|
Changelog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
Version 9.7.0 (2026-10-21)
|
|
6
|
+
==========================
|
|
7
|
+
|
|
8
|
+
Added
|
|
9
|
+
-----
|
|
10
|
+
- **Calibrate from Home Assistant's history** (#179), for a heater with
|
|
11
|
+
no recording of its statuses.
|
|
12
|
+
``sim_home_assistant.read_heater_history`` reads the heater's own
|
|
13
|
+
entities a day at a time: the probes, what is heating, the in-use
|
|
14
|
+
flag, the stop threshold, the operation mode, the error code, the
|
|
15
|
+
power, the energy to setpoint and the heater's own air, each number
|
|
16
|
+
from the unit it was recorded in. An entity that changes on every
|
|
17
|
+
response (``heard``) says when the heater was observed.
|
|
18
|
+
``sim_calibrate.calibrate_changes`` fits on what it read, and gives
|
|
19
|
+
the same ``Calibrated`` and ``report()`` as ``calibrate``.
|
|
20
|
+
- ``sim_adapters.ChangeRows`` and ``sim_adapters.change_history``: the
|
|
21
|
+
calibration frame from each value's changes and the instants the
|
|
22
|
+
heater was heard from, for any store of that history.
|
|
23
|
+
- ``History.states``: every row's state as written, numbers or not.
|
|
24
|
+
``HomeAssistantClient.read_history`` fills it (``parse_states``).
|
|
25
|
+
- ``sim_home_assistant.to_w`` and ``to_wh``.
|
|
26
|
+
|
|
27
|
+
Version 9.6.0 (2026-10-21)
|
|
28
|
+
==========================
|
|
29
|
+
|
|
30
|
+
Added
|
|
31
|
+
-----
|
|
32
|
+
- **Calibration outside the CLI** (``nwp500.sim_calibrate``, #177).
|
|
33
|
+
``calibrate``, ``Calibrated``, ``report``, ``Term`` and ``max_gap``
|
|
34
|
+
moved here from ``nwp500.cli.calibration`` so an application can
|
|
35
|
+
calibrate without ``click`` or ``rich``. ``calibrate`` takes
|
|
36
|
+
``HeaterParams`` or a parameters document as ``params`` and raises
|
|
37
|
+
``CalibrationInputError`` (a ``ValueError``). Its notes no longer name
|
|
38
|
+
the command's options; ``nwp-cli simulate calibrate`` adds them.
|
|
39
|
+
- ``status_history.iter_read`` yields a recording's statuses one at a
|
|
40
|
+
time.
|
|
41
|
+
- ``sim_adapters.add_inputs`` adds the air, its humidity, a meter's
|
|
42
|
+
draws, the circulation's runs and the unobserved gaps to a frame
|
|
43
|
+
``sim_adapters.history`` built.
|
|
44
|
+
|
|
45
|
+
Changed
|
|
46
|
+
-------
|
|
47
|
+
- **A long history calibrates in bounded memory** (#177).
|
|
48
|
+
``sim_adapters.history`` reads its statuses one at a time and keeps
|
|
49
|
+
only the frame's values, and ``sim_calibrate.calibrate`` takes the
|
|
50
|
+
record's span and gaps from the frame, so a generator over day files
|
|
51
|
+
is read once. 60 days of statuses (about 540,000) now take about
|
|
52
|
+
150 MB instead of about 6 GB of parsed statuses. The drawing and
|
|
53
|
+
circulating columns are computed per span instead of per row and
|
|
54
|
+
span.
|
|
55
|
+
|
|
56
|
+
Documentation
|
|
57
|
+
-------------
|
|
58
|
+
- The calibration how-to says the recorder writes every status the
|
|
59
|
+
heater sends and what that costs on disk, shows calibrating from
|
|
60
|
+
day files with ``sim_calibrate``, adds calibrating in an
|
|
61
|
+
application, and says what Home Assistant's 10-day retention means
|
|
62
|
+
for a longer record.
|
|
63
|
+
|
|
5
64
|
Version 9.5.1 (2026-10-21)
|
|
6
65
|
==========================
|
|
7
66
|
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
======================================
|
|
2
|
+
Calibrate the Simulator to Your Heater
|
|
3
|
+
======================================
|
|
4
|
+
|
|
5
|
+
The simulator's default parameters are one NWP500-65's, fitted at one
|
|
6
|
+
installation. Record what your own heater reports for a few weeks and
|
|
7
|
+
fit its standby loss, its compressor and its power to that record.
|
|
8
|
+
|
|
9
|
+
.. contents::
|
|
10
|
+
:local:
|
|
11
|
+
:depth: 1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Record the history
|
|
15
|
+
==================
|
|
16
|
+
|
|
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:
|
|
24
|
+
|
|
25
|
+
.. code-block:: bash
|
|
26
|
+
|
|
27
|
+
nwp-cli simulate record history.jsonl
|
|
28
|
+
nwp-cli simulate record history.jsonl --every 60
|
|
29
|
+
|
|
30
|
+
Each line is one status, as JSON: the instant it arrived and the
|
|
31
|
+
status payload as the heater sent it:
|
|
32
|
+
|
|
33
|
+
.. code-block:: json
|
|
34
|
+
|
|
35
|
+
{"t": "2026-10-18T23:09:10.298+00:00", "status": {"dhwUse": 1, "...": 0}}
|
|
36
|
+
|
|
37
|
+
Stopping and starting again appends to the same file, so the record
|
|
38
|
+
can have gaps. The heater may have heated or delivered water during a
|
|
39
|
+
gap, so ``calibrate`` takes any spacing between statuses of more than
|
|
40
|
+
4 times the record's usual spacing (and at least 5 minutes) as
|
|
41
|
+
unobserved. No quiet window and no recovery spans such a gap, and the
|
|
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.
|
|
51
|
+
|
|
52
|
+
Record through quiet nights and full recoveries. The standby fit needs
|
|
53
|
+
windows of 3 hours or more with no heating and no hot water in use. The
|
|
54
|
+
compressor fit needs at least 5 recoveries that reached their target,
|
|
55
|
+
and holds out the latest fifth once there are 10. The power fits need
|
|
56
|
+
10 compressor cycles.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
Record the air beside the heater
|
|
60
|
+
================================
|
|
61
|
+
|
|
62
|
+
The standby loss is the heat the tank loses to the air it stands in,
|
|
63
|
+
so the fit needs that air over the whole record. The heater does not
|
|
64
|
+
read it while idle: its sensor sits at the heat pump's intake, and it
|
|
65
|
+
reads the air the heater stands in only while the evaporator fan runs.
|
|
66
|
+
Idle, it reads the cabinet, about 6 °F warm. So give one of these:
|
|
67
|
+
|
|
68
|
+
- **A sensor beside the heater:** a CSV file of its readings, an ISO
|
|
69
|
+
8601 instant with its UTC offset and a temperature in your preferred
|
|
70
|
+
unit on each row (a header row is skipped):
|
|
71
|
+
|
|
72
|
+
.. code-block:: text
|
|
73
|
+
|
|
74
|
+
time,temperature
|
|
75
|
+
2026-10-18T00:00:00-07:00,61.2
|
|
76
|
+
2026-10-18T00:05:00-07:00,61.0
|
|
77
|
+
|
|
78
|
+
- **A constant,** for a heater in conditioned space that holds one
|
|
79
|
+
temperature.
|
|
80
|
+
|
|
81
|
+
- **Home Assistant:** ``--sources FILE``, the same sources file as
|
|
82
|
+
``simulate run`` takes (:doc:`simulate-heater`). The command reads the
|
|
83
|
+
air beside the heater and its humidity over the record. It also reads
|
|
84
|
+
the meter's draws and the circulation's runs, which make the quiet
|
|
85
|
+
test exact: the heater's own "hot water in use" flag also fires on
|
|
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
|
|
94
|
+
meter's or the circulation's history doesn't cover the whole record,
|
|
95
|
+
the heater's own flag is used instead. ``--air``, ``--ambient`` and
|
|
96
|
+
``--humidity`` override the sources.
|
|
97
|
+
|
|
98
|
+
Without the air, the standby loss keeps the base's value and the report
|
|
99
|
+
says so. When you give the air, the recoveries use it too; without it,
|
|
100
|
+
they use the heater's own reading while the compressor runs.
|
|
101
|
+
|
|
102
|
+
``--humidity`` takes the same kind of file, in %RH, for the
|
|
103
|
+
evaporator; without it the command gives the fits 50 %.
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
Calibrate
|
|
107
|
+
=========
|
|
108
|
+
|
|
109
|
+
``nwp-cli simulate calibrate`` needs the ``cli`` and ``sim`` extras. It
|
|
110
|
+
reads the history and prints what each fit found. With ``-o`` it also
|
|
111
|
+
writes the fitted parameters, as a ``HeaterParams`` document:
|
|
112
|
+
|
|
113
|
+
.. code-block:: bash
|
|
114
|
+
|
|
115
|
+
nwp-cli simulate calibrate history.jsonl --air air.csv -o params.json
|
|
116
|
+
nwp-cli simulate calibrate history.jsonl --ambient 68 -o params.json
|
|
117
|
+
nwp-cli simulate calibrate history.jsonl --sources sources.toml -o params.json
|
|
118
|
+
nwp-cli simulate run --params params.json
|
|
119
|
+
|
|
120
|
+
It refits the 65 gal defaults. For another tank size, start from that
|
|
121
|
+
size's parameters with ``--params``:
|
|
122
|
+
|
|
123
|
+
.. code-block:: python
|
|
124
|
+
|
|
125
|
+
import json
|
|
126
|
+
from nwp500 import sim
|
|
127
|
+
from nwp500.sim.params import params_to_document
|
|
128
|
+
|
|
129
|
+
with open("base.json", "w") as f:
|
|
130
|
+
json.dump(params_to_document(sim.default_params(3)), f) # 80 gal
|
|
131
|
+
|
|
132
|
+
The report has a row for each term. Each row gives the term's value,
|
|
133
|
+
the evidence it was fitted from and its error, and whether it was
|
|
134
|
+
fitted or kept the base's value, and why:
|
|
135
|
+
|
|
136
|
+
.. list-table::
|
|
137
|
+
:header-rows: 1
|
|
138
|
+
:widths: 30 70
|
|
139
|
+
|
|
140
|
+
* - Term
|
|
141
|
+
- Fitted from
|
|
142
|
+
* - Standby loss and its exponent
|
|
143
|
+
- Quiet windows of 3 h or more, starting 45 minutes after any
|
|
144
|
+
heating or hot water in use. A window counts only if its fit
|
|
145
|
+
tracks the probes within 2 °F. The exponent is fitted only when
|
|
146
|
+
the windows span two 20 °F bands of tank-to-air difference; it
|
|
147
|
+
is held at 0 (a constant loss) otherwise.
|
|
148
|
+
* - Zone gap time constant
|
|
149
|
+
- How fast the upper and lower probes' gap closes over quiet
|
|
150
|
+
windows; it needs 20 windows that open with a gap of 2 °F or
|
|
151
|
+
more.
|
|
152
|
+
* - Compressor constant, evaporator approach
|
|
153
|
+
- Heat-pump recoveries that reached their target, against the loss
|
|
154
|
+
just fitted. The error is the mean absolute error of the
|
|
155
|
+
recovery durations, in minutes, also on the held-out recoveries.
|
|
156
|
+
A fit pinned on a bound is reported and not applied.
|
|
157
|
+
* - Input power scale, COP scale, start-up energy, COP lift
|
|
158
|
+
- The heater's own power readings over each compressor cycle,
|
|
159
|
+
against the compressor just fitted. The COP uses only cycles
|
|
160
|
+
with no hot water in use from 30 minutes before them to their
|
|
161
|
+
end.
|
|
162
|
+
|
|
163
|
+
``Unfitted`` lists every term that kept the base's value. The answers
|
|
164
|
+
those terms reach say so in their ``reasons`` (for example
|
|
165
|
+
``standby_uncalibrated``). The ``book``, ``readiness`` and ``draw``
|
|
166
|
+
terms are always listed, because they are fitted only from a hot-water
|
|
167
|
+
meter's draws, which the command does not take (see below).
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
From Python
|
|
171
|
+
===========
|
|
172
|
+
|
|
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:
|
|
177
|
+
|
|
178
|
+
.. code-block:: python
|
|
179
|
+
|
|
180
|
+
from pathlib import Path
|
|
181
|
+
|
|
182
|
+
from nwp500 import sim, sim_calibrate, status_history
|
|
183
|
+
|
|
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)
|
|
188
|
+
|
|
189
|
+
done = sim_calibrate.calibrate(
|
|
190
|
+
recorded(sorted(Path("history").glob("*.jsonl"))),
|
|
191
|
+
air=air_readings, # [(instant, temperature), ...]
|
|
192
|
+
humidity=humidity_readings, # [(instant, %RH), ...]
|
|
193
|
+
params=sim.default_params(), # or your own HeaterParams
|
|
194
|
+
)
|
|
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`.
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
Calibrate from Home Assistant's history
|
|
236
|
+
=======================================
|
|
237
|
+
|
|
238
|
+
A heater whose integration writes its values to Home Assistant can be
|
|
239
|
+
calibrated from that history, with no recording of its statuses. Home
|
|
240
|
+
Assistant keeps each entity's changes, and a value that holds writes
|
|
241
|
+
none, so the history also needs an entity that changes every time the
|
|
242
|
+
heater answers, such as the integration's last response time. Its
|
|
243
|
+
changes are when the heater was heard from; a silence between them
|
|
244
|
+
longer than 4 times their usual spacing (and at least 5 minutes) is
|
|
245
|
+
unobserved, as a gap between statuses is.
|
|
246
|
+
|
|
247
|
+
.. code-block:: python
|
|
248
|
+
|
|
249
|
+
from datetime import UTC, datetime, timedelta
|
|
250
|
+
|
|
251
|
+
from nwp500 import sim_calibrate
|
|
252
|
+
from nwp500 import sim_home_assistant as ha
|
|
253
|
+
|
|
254
|
+
entities = ha.HeaterEntities(
|
|
255
|
+
heard="sensor.water_heater_last_mqtt_response",
|
|
256
|
+
upper="sensor.water_heater_tank_upper_temperature",
|
|
257
|
+
lower="sensor.water_heater_tank_lower_temperature",
|
|
258
|
+
compressor="binary_sensor.water_heater_compressor_running",
|
|
259
|
+
upper_element="binary_sensor.water_heater_upper_element",
|
|
260
|
+
lower_element="binary_sensor.water_heater_lower_element",
|
|
261
|
+
heat_source="sensor.water_heater_current_heat_source",
|
|
262
|
+
dhw_in_use="binary_sensor.water_heater_dhw_in_use",
|
|
263
|
+
target="sensor.water_heater_hp_lower_off_temperature",
|
|
264
|
+
operation_mode="sensor.water_heater_current_operation_mode",
|
|
265
|
+
error_code="sensor.water_heater_error_code",
|
|
266
|
+
power="sensor.water_heater_current_power",
|
|
267
|
+
energy_to_setpoint="sensor.water_heater_energy_to_setpoint",
|
|
268
|
+
ambient="sensor.water_heater_ambient_temperature",
|
|
269
|
+
)
|
|
270
|
+
end = datetime.now(UTC)
|
|
271
|
+
start = end - timedelta(days=60)
|
|
272
|
+
async with ha.HomeAssistantClient(url, token) as client:
|
|
273
|
+
history = await ha.read_heater_history(client, entities, start, end)
|
|
274
|
+
record = await ha.record_inputs(client, ha.Entities(air=..., humidity=...),
|
|
275
|
+
start, end)
|
|
276
|
+
done = sim_calibrate.calibrate_changes(
|
|
277
|
+
history.rows, history.observed, record=record, notes=history.notes
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
Use your own entity ids. Only ``heard`` and the two probes are needed;
|
|
281
|
+
leave out what your installation does not record. ``read_heater_history``:
|
|
282
|
+
|
|
283
|
+
- reads a day at a time, so the heartbeat's rows are never all held as
|
|
284
|
+
Home Assistant gives them;
|
|
285
|
+
- takes each number from the unit it was recorded in (°F, °C or K; W or
|
|
286
|
+
kW; Wh or kWh), ``on`` and ``off`` from the binary sensors, and the
|
|
287
|
+
heat source, operation mode and error code by their enum names
|
|
288
|
+
(``HeatSource``, ``CurrentOperationMode``, ``ErrorCode``);
|
|
289
|
+
- takes ``unavailable`` or ``unknown`` on any of the entities as an
|
|
290
|
+
outage of the heater, unobserved (an enum's own ``UNKNOWN`` is a
|
|
291
|
+
value);
|
|
292
|
+
- takes the heat source as the compressor's state where no compressor
|
|
293
|
+
entity is given;
|
|
294
|
+
- notes a value it could not read, and an entity whose history starts
|
|
295
|
+
after ``start``.
|
|
296
|
+
|
|
297
|
+
Give ``anti_legionella`` where it is recorded. Without it an
|
|
298
|
+
anti-legionella cycle is not kept out of the recoveries, and the notes
|
|
299
|
+
say so.
|
|
300
|
+
|
|
301
|
+
Home Assistant keeps 10 days of history by default, so ``start`` reaches
|
|
302
|
+
back only that far unless the recorder's ``purge_keep_days`` is raised
|
|
303
|
+
to cover the record you want to fit. The same is true of the air.
|
|
304
|
+
|
|
305
|
+
Rows from another store of the same history (an export, say) go in as
|
|
306
|
+
``sim_adapters.ChangeRows``, each value's changes as ``(instant,
|
|
307
|
+
value)`` pairs, with the instants heard from:
|
|
308
|
+
``sim_calibrate.calibrate_changes(rows, observed, ...)``.
|
|
309
|
+
|
|
310
|
+
On a desktop computer, 60 days with a heartbeat every 10 seconds
|
|
311
|
+
(about 550,000 rows) took about 13 seconds to read, not counting Home
|
|
312
|
+
Assistant's own queries, and 11 to fit, in about 180 MB.
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
Limitations
|
|
316
|
+
===========
|
|
317
|
+
|
|
318
|
+
- **The draws.** The heater reports no draw volume, and in recorded
|
|
319
|
+
statuses its inlet and flow readings read 0. The draw terms
|
|
320
|
+
(``book``, ``readiness``, ``draw``) need a hot-water meter and an
|
|
321
|
+
inlet probe.
|
|
322
|
+
- **Circulation the heater does not run.** The quiet test is the
|
|
323
|
+
heater's own "hot water in use" flag, which also fires on
|
|
324
|
+
circulation. A loop's heat that does not trip the flag is invisible
|
|
325
|
+
to the heater and lands in the standby loss. From Python, pass the
|
|
326
|
+
circulation's runs (``record=`` or ``sa.history(...,
|
|
327
|
+
circulating=...)``) to keep them out of the quiet windows.
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
See also
|
|
331
|
+
========
|
|
332
|
+
|
|
333
|
+
* :doc:`simulate-heater`: run the simulator with your parameters
|
|
334
|
+
* :doc:`/reference/python_api/sim`: ``sim_calibrate``,
|
|
335
|
+
``sim.calibration``, ``sim_adapters.history``,
|
|
336
|
+
``sim_adapters.change_history``, ``status_history`` and
|
|
337
|
+
``sim_home_assistant.read_heater_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, calibrate_changes, 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
|
======================
|
|
@@ -304,6 +309,8 @@ Home Assistant sources
|
|
|
304
309
|
.. automodule:: nwp500.sim_home_assistant
|
|
305
310
|
:members: HomeAssistantEnvironment, HomeAssistantClient, record_inputs,
|
|
306
311
|
RecordInputs, Sources, Entities, sources_from, State, parse_state,
|
|
307
|
-
parse_history, parse_outages, History, history_for,
|
|
312
|
+
parse_history, parse_outages, parse_states, History, history_for,
|
|
313
|
+
parse_forecast, read_heater_history, HeaterEntities, HeaterHistory,
|
|
314
|
+
HISTORY_WINDOW, to_w, to_wh,
|
|
308
315
|
pump_runs, draw_spans, busy, covers_start, reports_after, percent,
|
|
309
316
|
fresh, to_f, to_gpm, to_gal, to_rpm, HomeAssistantError
|
|
@@ -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")
|