nwp500-python 9.6.0__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.6.0 → nwp500_python-9.7.0}/CHANGELOG.rst +22 -0
- {nwp500_python-9.6.0/src/nwp500_python.egg-info → nwp500_python-9.7.0}/PKG-INFO +1 -1
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/calibrate-simulator.rst +83 -1
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/python_api/sim.rst +4 -2
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim_adapters.py +173 -1
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim_calibrate.py +82 -11
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim_home_assistant.py +288 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/SOURCES.txt +1 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/scm_file_list.json +1 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/scm_version.json +2 -2
- nwp500_python-9.7.0/tests/test_sim_change_history.py +527 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/.coveragerc +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/.github/workflows/ci.yml +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/.gitignore +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/.readthedocs.yml +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/AUTHORS.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/LICENSE.txt +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/Makefile +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/README.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/RELEASE.md +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/Makefile +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/conf.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/explanation/draws.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/explanation/heating-elements.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/explanation/index.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/explanation/recovery-speed.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/explanation/simulator.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/explanation/standby-loss.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/explanation/tank-energy.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/explanation/tou-recovery-cap.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/explanation/unknown-values.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/explanation/what-starts-a-recovery.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/bluetooth.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/maintenance.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/manage-units.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/optimize-tou.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/schedule-operation.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/simulate-heater.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/how-to/track-energy.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/index.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/openapi.yaml +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/project/authors.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/project/changelog.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/project/history.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/project/license.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/index.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/installation.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/protocol/data_conversions.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/protocol/device_status.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/protocol/quick_reference.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/python_api/cli.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/python_api/events.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/python_api/models.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/python_api/mqtt_client.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/reference/simulator-inputs.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/requirements.txt +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/.ruff.toml +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/README.md +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/air_filter_reset.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/auto_recovery.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/combined_callbacks.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/demand_response.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/device_capabilities.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/device_status_debug.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/energy_analytics.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/firmware_payload_capture.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/mqtt_diagnostics.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/power_control.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/recirculation_control.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/reconnection_demo.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/simple_auto_recovery.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/token_restoration.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/tou_schedule.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/advanced/water_reservation.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/beginner/01_authentication.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/beginner/02_list_devices.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/beginner/04_set_temperature.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/intermediate/command_queue.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/intermediate/device_status_callback.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/intermediate/event_driven_control.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/intermediate/periodic_requests.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/intermediate/set_mode.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/intermediate/vacation_mode.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/mask.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/simulator/lower-setpoint.json +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/simulator/plan.json +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/simulator/scenario.json +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/testing/periodic_device_info.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/testing/test_api_client.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/testing/test_mqtt_connection.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/testing/test_mqtt_messaging.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/examples/testing/test_periodic_minimal.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/pyproject.toml +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/scripts/README.md +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/scripts/bump_version.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/scripts/format.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/scripts/lint.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/scripts/validate_version.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/setup.cfg +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/setup.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/__init__.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/ble/__init__.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/ble/__main__.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/ble/client.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/ble/mqtt_adapter.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/ble/protocol.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/cli/__main__.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/cli/ble_backend.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/cli/calibration.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/cli/handlers.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/cli/output_formatters.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/cli/presentation.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/cli/recording.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/cli/rich_output.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/cli/simulate.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/config.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/converters.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/device_capabilities.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/encoding.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/enums.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/events.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/models/__init__.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/models/account.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/models/device.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/models/diagnostics.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/models/energy.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/models/feature.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/models/firmware.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/models/maintenance.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/models/schedule.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/models/status.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/models/tou.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/__init__.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/_control_commands.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/client.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/command_queue.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/connection.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/control.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/state_tracker.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/subscriptions.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/types.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt/utils.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/mqtt_events.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/reservations.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/__init__.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/api.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/attribution.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/book.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/calibration.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/circulation.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/codec.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/cold_supply.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/controller.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/decay.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/defaults.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/delivery.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/draw_forecast.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/engine.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/extensions.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/heat_pump_recovery.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/heating_trajectory.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/inputs.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/location_air.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/meter.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/observed.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/params.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/physics/__init__.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/physics/compressor.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/physics/draw.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/physics/element.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/physics/refrigerant.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/physics/tank.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/profile.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/ready.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/record.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/result.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/schedules.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/tank_physics.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/tou_triggers.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim/units.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/sim_live.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/status_history.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/tou_schedule.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/_sim_site.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/conftest.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/fixtures/sim/engine_golden.json +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/fixtures/sim/heater_params.json +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/fixtures/sim/heater_params_fitted.json +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_api_endpoints_201.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_app_queries.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_auth.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_auth_session_refresh.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_ble_mqtt_adapter.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_ble_protocol.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_canonical_schedule.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_cli_calibrate.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_cli_commands.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_cli_simulate.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_cli_tristate_presentation.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_command_queue.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_device_capabilities.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_device_rest_models.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_events.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_model_converters.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_models.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_mqtt_control_fail.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_mqtt_events.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_mqtt_last_will.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_mqtt_reliability.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_multi_device.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_navilink_201_enums.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_openei.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_protocol_correctness.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_public_api.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_reservation_validation.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_reservations.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_adapters.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_api.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_book.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_book_history.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_calibrate.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_calibration.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_circulation.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_codec.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_cold_supply.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_controller.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_defaults.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_draw_forecast.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_draw_loss.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_element.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_element_fit.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_forecast.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_golden.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_home_assistant.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_hot_water.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_import_contract.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_live.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_location_air.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_meter.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_no_clock_no_state.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_observed.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_profile.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_readiness.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_readiness_fit.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_ready.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_ready_api.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_replay.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_schedules.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_sim_tank_physics.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_status_history.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_tou_schedule.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_unit_system_process_wide.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tests/test_utils.py +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/tox.ini +0 -0
- {nwp500_python-9.6.0 → nwp500_python-9.7.0}/uv.lock +0 -0
|
@@ -2,6 +2,28 @@
|
|
|
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
|
+
|
|
5
27
|
Version 9.6.0 (2026-10-21)
|
|
6
28
|
==========================
|
|
7
29
|
|
|
@@ -232,6 +232,86 @@ the draw response and the elements also run there: see "Improve
|
|
|
232
232
|
accuracy" and "Fit the elements" in :doc:`simulate-heater`.
|
|
233
233
|
|
|
234
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
|
+
|
|
235
315
|
Limitations
|
|
236
316
|
===========
|
|
237
317
|
|
|
@@ -252,4 +332,6 @@ See also
|
|
|
252
332
|
|
|
253
333
|
* :doc:`simulate-heater`: run the simulator with your parameters
|
|
254
334
|
* :doc:`/reference/python_api/sim`: ``sim_calibrate``,
|
|
255
|
-
``sim.calibration``, ``sim_adapters.history
|
|
335
|
+
``sim.calibration``, ``sim_adapters.history``,
|
|
336
|
+
``sim_adapters.change_history``, ``status_history`` and
|
|
337
|
+
``sim_home_assistant.read_heater_history``
|
|
@@ -298,7 +298,7 @@ Recorded history
|
|
|
298
298
|
:members:
|
|
299
299
|
|
|
300
300
|
.. automodule:: nwp500.sim_calibrate
|
|
301
|
-
:members: calibrate, Calibrated, report, Term, max_gap,
|
|
301
|
+
:members: calibrate, calibrate_changes, Calibrated, report, Term, max_gap,
|
|
302
302
|
CalibrationInputError, DEFAULT_HUMIDITY_PCT, GAP_SAMPLES, MIN_GAP,
|
|
303
303
|
DEFAULT_BASE_NOTE, NO_AIR_NOTE
|
|
304
304
|
|
|
@@ -309,6 +309,8 @@ Home Assistant sources
|
|
|
309
309
|
.. automodule:: nwp500.sim_home_assistant
|
|
310
310
|
:members: HomeAssistantEnvironment, HomeAssistantClient, record_inputs,
|
|
311
311
|
RecordInputs, Sources, Entities, sources_from, State, parse_state,
|
|
312
|
-
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,
|
|
313
315
|
pump_runs, draw_spans, busy, covers_start, reports_after, percent,
|
|
314
316
|
fresh, to_f, to_gpm, to_gal, to_rpm, HomeAssistantError
|
|
@@ -760,15 +760,187 @@ def add_inputs(
|
|
|
760
760
|
for name, spans in (("drawing", drawing), ("circulating", circulating)):
|
|
761
761
|
if spans is not None:
|
|
762
762
|
frame[name] = _interval_column(index, spans)
|
|
763
|
+
unobserved = np.zeros(len(frame), dtype=bool)
|
|
763
764
|
if max_gap is not None and len(frame) > 1:
|
|
764
|
-
unobserved = np.zeros(len(frame), dtype=bool)
|
|
765
765
|
unobserved[:-1] = np.diff(seconds) > max_gap.total_seconds()
|
|
766
|
+
if "heater_unobserved" in frame.columns:
|
|
767
|
+
# ``change_history``'s outages, into the draws added here too.
|
|
768
|
+
unobserved |= frame["heater_unobserved"].to_numpy(dtype=bool)
|
|
769
|
+
if unobserved.any():
|
|
766
770
|
for name in ("dhw_in_use", "drawing", "excluded"):
|
|
767
771
|
if name in frame.columns:
|
|
768
772
|
frame[name] = frame[name].to_numpy(dtype=bool) | unobserved
|
|
769
773
|
return frame
|
|
770
774
|
|
|
771
775
|
|
|
776
|
+
# ── a heater's history from its values' changes ───────────────────
|
|
777
|
+
|
|
778
|
+
#: A value's changes: each instant it took a new value, and the value.
|
|
779
|
+
Changes = Sequence[tuple[datetime, Any]]
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
@dataclass(frozen=True)
|
|
783
|
+
class ChangeRows:
|
|
784
|
+
"""A heater's history as the changes of each value a status carries
|
|
785
|
+
(Home Assistant's entity history, say): each series the instants a
|
|
786
|
+
value changed and what it changed to, in time order. A value holds
|
|
787
|
+
until its next change. Temperatures are °F, power W, energy Wh.
|
|
788
|
+
|
|
789
|
+
Only the probes are needed. Without a value, its column keeps
|
|
790
|
+
``history``'s default: the flags off, a number unknown.
|
|
791
|
+
"""
|
|
792
|
+
|
|
793
|
+
upper_f: Changes
|
|
794
|
+
lower_f: Changes
|
|
795
|
+
compressor_on: Changes = ()
|
|
796
|
+
upper_element_on: Changes = ()
|
|
797
|
+
lower_element_on: Changes = ()
|
|
798
|
+
#: ``HeatSource`` values.
|
|
799
|
+
heat_source: Changes = ()
|
|
800
|
+
dhw_in_use: Changes = ()
|
|
801
|
+
#: The lower probe's heat-pump stop threshold.
|
|
802
|
+
target_f: Changes = ()
|
|
803
|
+
#: ``CurrentOperationMode`` values.
|
|
804
|
+
operation_mode: Changes = ()
|
|
805
|
+
anti_legionella: Changes = ()
|
|
806
|
+
#: ``ErrorCode`` values.
|
|
807
|
+
error_code: Changes = ()
|
|
808
|
+
power_w: Changes = ()
|
|
809
|
+
energy_to_setpoint_wh: Changes = ()
|
|
810
|
+
#: The heater's own air reading, used only while its compressor runs.
|
|
811
|
+
heater_air_f: Changes = ()
|
|
812
|
+
#: The spans a value was unknown (Home Assistant's ``unavailable``
|
|
813
|
+
#: or ``unknown``): unobserved, like a gap between statuses.
|
|
814
|
+
outages: Sequence[tuple[datetime, datetime]] = ()
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _ns(instants: Iterable[datetime]) -> np.ndarray:
|
|
818
|
+
"""``instants`` as UTC nanoseconds."""
|
|
819
|
+
import pandas as pd
|
|
820
|
+
|
|
821
|
+
return np.asarray(
|
|
822
|
+
pd.to_datetime(list(instants), utc=True).as_unit("ns").asi8,
|
|
823
|
+
dtype=np.int64,
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def _held_at(
|
|
828
|
+
changes: Changes, at: np.ndarray, missing: Any, dtype: Any
|
|
829
|
+
) -> np.ndarray:
|
|
830
|
+
"""``changes`` held from each change to the next, read at ``at``
|
|
831
|
+
(ns); ``missing`` before the first."""
|
|
832
|
+
out = np.full(len(at), missing, dtype=dtype)
|
|
833
|
+
if not changes:
|
|
834
|
+
return out
|
|
835
|
+
rows = sorted(changes, key=lambda r: r[0])
|
|
836
|
+
times = _ns(t for t, _ in rows)
|
|
837
|
+
values = np.array([v for _, v in rows], dtype=dtype)
|
|
838
|
+
i = np.searchsorted(times, at, side="right") - 1
|
|
839
|
+
seen = i >= 0
|
|
840
|
+
out[seen] = values[i[seen]]
|
|
841
|
+
return out
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def change_history(
|
|
845
|
+
rows: ChangeRows, observed: Iterable[datetime]
|
|
846
|
+
) -> pd.DataFrame:
|
|
847
|
+
"""``history``'s frame from a heater's values' changes (``rows``).
|
|
848
|
+
|
|
849
|
+
A value that holds writes no change, so the changes cannot tell an
|
|
850
|
+
observed span from one nobody saw: ``observed`` is the instants the
|
|
851
|
+
heater was heard from (an entity that changes on every status
|
|
852
|
+
response, say). The frame has a row at each of them and at each
|
|
853
|
+
change between the first and the last, each value as it stood
|
|
854
|
+
then. Rows over an outage are marked water in use and excluded, as
|
|
855
|
+
``add_inputs`` marks a gap, and ``heater_unobserved``. Without a
|
|
856
|
+
compressor flag, the heat source says when the compressor runs.
|
|
857
|
+
"""
|
|
858
|
+
import pandas as pd
|
|
859
|
+
|
|
860
|
+
heard = _ns(observed)
|
|
861
|
+
if not len(heard):
|
|
862
|
+
at = heard
|
|
863
|
+
else:
|
|
864
|
+
first, last = heard.min(), heard.max()
|
|
865
|
+
changed = [
|
|
866
|
+
_ns(t for t, _ in series)
|
|
867
|
+
for series in (
|
|
868
|
+
rows.upper_f,
|
|
869
|
+
rows.lower_f,
|
|
870
|
+
rows.compressor_on,
|
|
871
|
+
rows.upper_element_on,
|
|
872
|
+
rows.lower_element_on,
|
|
873
|
+
rows.heat_source,
|
|
874
|
+
rows.dhw_in_use,
|
|
875
|
+
rows.target_f,
|
|
876
|
+
rows.operation_mode,
|
|
877
|
+
rows.anti_legionella,
|
|
878
|
+
rows.error_code,
|
|
879
|
+
rows.power_w,
|
|
880
|
+
rows.energy_to_setpoint_wh,
|
|
881
|
+
rows.heater_air_f,
|
|
882
|
+
)
|
|
883
|
+
if series
|
|
884
|
+
]
|
|
885
|
+
at = np.unique(np.concatenate([heard, *changed]))
|
|
886
|
+
at = at[(at >= first) & (at <= last)]
|
|
887
|
+
nan = float("nan")
|
|
888
|
+
source = _held_at(rows.heat_source, at, -1, np.int64)
|
|
889
|
+
compressor = (
|
|
890
|
+
_held_at(rows.compressor_on, at, False, bool)
|
|
891
|
+
if rows.compressor_on
|
|
892
|
+
# Without its own flag, the heat source says when it runs.
|
|
893
|
+
else np.isin(
|
|
894
|
+
source,
|
|
895
|
+
[int(HeatSource.HEATPUMP), int(HeatSource.HEATPUMP_HEATELEMENT)],
|
|
896
|
+
)
|
|
897
|
+
)
|
|
898
|
+
mode = _held_at(rows.operation_mode, at, -1, np.int64)
|
|
899
|
+
error = _held_at(rows.error_code, at, int(ErrorCode.NO_ERROR), np.int64)
|
|
900
|
+
|
|
901
|
+
def optional(series: Changes) -> np.ndarray | None:
|
|
902
|
+
return _held_at(series, at, nan, float) if series else None
|
|
903
|
+
|
|
904
|
+
columns: dict[str, np.ndarray | None] = {
|
|
905
|
+
"upper_f": _held_at(rows.upper_f, at, nan, float),
|
|
906
|
+
"lower_f": _held_at(rows.lower_f, at, nan, float),
|
|
907
|
+
"compressor_on": compressor,
|
|
908
|
+
"element_on": _held_at(rows.upper_element_on, at, False, bool)
|
|
909
|
+
| _held_at(rows.lower_element_on, at, False, bool)
|
|
910
|
+
| np.isin(
|
|
911
|
+
source,
|
|
912
|
+
[int(HeatSource.HEATELEMENT), int(HeatSource.HEATPUMP_HEATELEMENT)],
|
|
913
|
+
),
|
|
914
|
+
"dhw_in_use": _held_at(rows.dhw_in_use, at, False, bool),
|
|
915
|
+
"target_f": optional(rows.target_f),
|
|
916
|
+
"excluded": np.isin(
|
|
917
|
+
mode,
|
|
918
|
+
[
|
|
919
|
+
int(CurrentOperationMode.HYBRID_EFFICIENCY_MODE),
|
|
920
|
+
int(CurrentOperationMode.HYBRID_BOOST_MODE),
|
|
921
|
+
],
|
|
922
|
+
)
|
|
923
|
+
| _held_at(rows.anti_legionella, at, False, bool)
|
|
924
|
+
| (error != int(ErrorCode.NO_ERROR)),
|
|
925
|
+
"power_w": optional(rows.power_w),
|
|
926
|
+
"energy_to_setpoint_wh": optional(rows.energy_to_setpoint_wh),
|
|
927
|
+
"heater_air_f": np.where(
|
|
928
|
+
compressor, _held_at(rows.heater_air_f, at, nan, float), nan
|
|
929
|
+
),
|
|
930
|
+
}
|
|
931
|
+
# ``history``'s columns, in its order, less the values not given.
|
|
932
|
+
data = {k: v for k, v in columns.items() if v is not None}
|
|
933
|
+
index = pd.DatetimeIndex(pd.to_datetime(at, unit="ns", utc=True))
|
|
934
|
+
frame = pd.DataFrame(data, index=index)
|
|
935
|
+
if rows.outages and len(frame):
|
|
936
|
+
down = np.asarray(_interval_column(index, rows.outages))
|
|
937
|
+
for name in ("dhw_in_use", "excluded"):
|
|
938
|
+
frame[name] = frame[name].to_numpy(dtype=bool) | down
|
|
939
|
+
# Kept, so ``add_inputs`` marks a meter's draws over them too.
|
|
940
|
+
frame["heater_unobserved"] = down
|
|
941
|
+
return frame
|
|
942
|
+
|
|
943
|
+
|
|
772
944
|
# ── a past scenario, from the statuses the heater reported ─────────
|
|
773
945
|
|
|
774
946
|
#: A tank reading older than this at the instant is no state to start
|
|
@@ -34,8 +34,9 @@ statuses::
|
|
|
34
34
|
This module needs the ``sim`` extra.
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
+
import itertools
|
|
37
38
|
from collections.abc import Iterable, Mapping, Sequence
|
|
38
|
-
from dataclasses import dataclass
|
|
39
|
+
from dataclasses import dataclass, replace
|
|
39
40
|
from datetime import datetime, timedelta
|
|
40
41
|
from typing import TYPE_CHECKING, Any, cast
|
|
41
42
|
|
|
@@ -66,6 +67,7 @@ __all__ = (
|
|
|
66
67
|
"Calibrated",
|
|
67
68
|
"Term",
|
|
68
69
|
"calibrate",
|
|
70
|
+
"calibrate_changes",
|
|
69
71
|
"max_gap",
|
|
70
72
|
"report",
|
|
71
73
|
)
|
|
@@ -176,6 +178,20 @@ def _base(params: HeaterParams | Any) -> HeaterParams:
|
|
|
176
178
|
) from None
|
|
177
179
|
|
|
178
180
|
|
|
181
|
+
def _begin(
|
|
182
|
+
air: Sequence[Reading] | None,
|
|
183
|
+
ambient: float | None,
|
|
184
|
+
params: HeaterParams | Any,
|
|
185
|
+
) -> tuple[HeaterParams, list[str]]:
|
|
186
|
+
"""The base to refit and the notes so far; the inputs checked before
|
|
187
|
+
a history is read."""
|
|
188
|
+
if air is not None and ambient is not None:
|
|
189
|
+
raise CalibrationInputError("give air or ambient, not both")
|
|
190
|
+
if params is None:
|
|
191
|
+
return sim.default_params(), [DEFAULT_BASE_NOTE]
|
|
192
|
+
return _base(params), []
|
|
193
|
+
|
|
194
|
+
|
|
179
195
|
def calibrate(
|
|
180
196
|
readings: Iterable[tuple[datetime, DeviceStatus]],
|
|
181
197
|
*,
|
|
@@ -204,17 +220,69 @@ def calibrate(
|
|
|
204
220
|
for both ``air`` and ``ambient``, a ``params`` that is not a
|
|
205
221
|
parameters document, or no readings.
|
|
206
222
|
"""
|
|
207
|
-
|
|
208
|
-
raise CalibrationInputError("give air or ambient, not both")
|
|
209
|
-
notes: list[str] = []
|
|
210
|
-
if params is None:
|
|
211
|
-
base = sim.default_params()
|
|
212
|
-
notes.append(DEFAULT_BASE_NOTE)
|
|
213
|
-
else:
|
|
214
|
-
base = _base(params)
|
|
223
|
+
base, notes = _begin(air, ambient, params)
|
|
215
224
|
frame = sa.history(readings)
|
|
216
225
|
if frame.empty:
|
|
217
226
|
raise CalibrationInputError("the history holds no statuses")
|
|
227
|
+
return _fit(frame, base, notes, air, ambient, humidity, record)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def calibrate_changes(
|
|
231
|
+
rows: sa.ChangeRows,
|
|
232
|
+
observed: Iterable[datetime],
|
|
233
|
+
*,
|
|
234
|
+
air: Sequence[Reading] | None = None,
|
|
235
|
+
ambient: float | None = None,
|
|
236
|
+
humidity: Sequence[Reading] | None = None,
|
|
237
|
+
params: HeaterParams | Any = None,
|
|
238
|
+
record: RecordInputs | None = None,
|
|
239
|
+
notes: Iterable[str] = (),
|
|
240
|
+
) -> Calibrated:
|
|
241
|
+
"""``calibrate`` on a heater's values' changes (``rows``) instead of
|
|
242
|
+
its statuses, with ``observed`` the instants it was heard from
|
|
243
|
+
(``sim_adapters.change_history``; ``sim_home_assistant.
|
|
244
|
+
read_heater_history`` reads both from Home Assistant). ``notes``
|
|
245
|
+
open the result's (a reader's). The rows are counted in
|
|
246
|
+
``n_statuses``. Raises ``CalibrationInputError`` as ``calibrate``
|
|
247
|
+
does, or for no ``observed`` instants.
|
|
248
|
+
"""
|
|
249
|
+
base, more = _begin(air, ambient, params)
|
|
250
|
+
heard = sorted(observed)
|
|
251
|
+
# The gaps are the silences between the instants heard from: a
|
|
252
|
+
# change inside one says nothing of the time around it.
|
|
253
|
+
gap = max_gap(heard)
|
|
254
|
+
silences = [(a, b) for a, b in itertools.pairwise(heard) if b - a > gap]
|
|
255
|
+
frame = sa.change_history(
|
|
256
|
+
replace(rows, outages=(*rows.outages, *silences)), heard
|
|
257
|
+
)
|
|
258
|
+
if frame.empty:
|
|
259
|
+
raise CalibrationInputError("the history holds no observed instants")
|
|
260
|
+
return _fit(
|
|
261
|
+
frame,
|
|
262
|
+
base,
|
|
263
|
+
[*notes, *more],
|
|
264
|
+
air,
|
|
265
|
+
ambient,
|
|
266
|
+
humidity,
|
|
267
|
+
record,
|
|
268
|
+
gaps=(gap, len(silences)),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _fit(
|
|
273
|
+
frame: pd.DataFrame,
|
|
274
|
+
base: HeaterParams,
|
|
275
|
+
notes: list[str],
|
|
276
|
+
air: Sequence[Reading] | None,
|
|
277
|
+
ambient: float | None,
|
|
278
|
+
humidity: Sequence[Reading] | None,
|
|
279
|
+
record: RecordInputs | None,
|
|
280
|
+
gaps: tuple[timedelta, int] | None = None,
|
|
281
|
+
) -> Calibrated:
|
|
282
|
+
"""The fits on ``frame`` (``history``'s or ``change_history``'s),
|
|
283
|
+
with the air, the humidity, the record's spans and the gaps added:
|
|
284
|
+
``gaps``' longest spacing taken as observed and how many spans are
|
|
285
|
+
longer, else the frame's own."""
|
|
218
286
|
index = cast("pd.DatetimeIndex", frame.index)
|
|
219
287
|
# Timestamps, which are datetimes.
|
|
220
288
|
first, last = cast(datetime, index[0]), cast(datetime, index[-1])
|
|
@@ -254,8 +322,11 @@ def calibrate(
|
|
|
254
322
|
# The fits' own default, given explicitly: an absent humidity
|
|
255
323
|
# reaches the evaporator's dew point as NaN, which reads as 100 %.
|
|
256
324
|
humidity = [(first, DEFAULT_HUMIDITY_PCT), (last, DEFAULT_HUMIDITY_PCT)]
|
|
257
|
-
|
|
258
|
-
|
|
325
|
+
if gaps is None:
|
|
326
|
+
gap = max_gap(index)
|
|
327
|
+
n_gaps = int((_steps(index) > gap.total_seconds() * 1e9).sum())
|
|
328
|
+
else:
|
|
329
|
+
gap, n_gaps = gaps
|
|
259
330
|
sa.add_inputs(
|
|
260
331
|
frame,
|
|
261
332
|
air=series,
|