nwp500-python 9.9.0__tar.gz → 9.10.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.9.0 → nwp500_python-9.10.0}/CHANGELOG.rst +101 -0
- {nwp500_python-9.9.0/src/nwp500_python.egg-info → nwp500_python-9.10.0}/PKG-INFO +1 -1
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/calibrate-simulator.rst +100 -4
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/simulate-heater.rst +85 -5
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/sim.rst +9 -8
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/simulator-inputs.rst +18 -1
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/__main__.py +2 -2
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/simulate.py +46 -77
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/calibration.py +91 -11
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/codec.py +88 -2
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/cold_supply.py +13 -52
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/decay.py +3 -1
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/params.py +4 -1
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim_adapters.py +412 -84
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim_calibrate.py +444 -36
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim_home_assistant.py +105 -168
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim_live.py +149 -16
- {nwp500_python-9.9.0 → nwp500_python-9.10.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/SOURCES.txt +3 -2
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/scm_file_list.json +3 -2
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/scm_version.json +2 -2
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_cli_simulate.py +3 -1
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_book_history.py +185 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_cold_supply.py +41 -64
- nwp500_python-9.10.0/tests/test_sim_documents.py +127 -0
- nwp500_python-9.10.0/tests/test_sim_fit_book.py +695 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_home_assistant.py +179 -121
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_live.py +47 -0
- nwp500_python-9.10.0/tests/test_sim_overlay.py +150 -0
- nwp500_python-9.9.0/src/nwp500/sim/meter.py +0 -130
- nwp500_python-9.9.0/tests/test_sim_meter.py +0 -163
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.coveragerc +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.github/workflows/ci.yml +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.gitignore +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.readthedocs.yml +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/AUTHORS.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/LICENSE.txt +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/Makefile +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/README.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/RELEASE.md +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/Makefile +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/conf.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/draws.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/heating-elements.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/index.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/recovery-speed.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/simulator.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/standby-loss.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/tank-energy.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/tou-recovery-cap.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/unknown-values.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/what-starts-a-recovery.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/bluetooth.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/maintenance.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/manage-units.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/optimize-tou.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/schedule-operation.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/track-energy.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/index.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/openapi.yaml +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/project/authors.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/project/changelog.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/project/history.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/project/license.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/index.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/installation.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/data_conversions.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/device_status.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/quick_reference.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/cli.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/events.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/models.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/mqtt_client.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/requirements.txt +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/.ruff.toml +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/README.md +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/air_filter_reset.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/auto_recovery.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/combined_callbacks.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/demand_response.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/device_capabilities.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/device_status_debug.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/energy_analytics.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/firmware_payload_capture.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/mqtt_diagnostics.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/power_control.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/recirculation_control.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/reconnection_demo.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/simple_auto_recovery.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/token_restoration.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/tou_schedule.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/water_reservation.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/beginner/01_authentication.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/beginner/02_list_devices.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/beginner/04_set_temperature.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/command_queue.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/device_status_callback.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/event_driven_control.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/periodic_requests.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/set_mode.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/vacation_mode.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/mask.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/simulator/lower-setpoint.json +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/simulator/plan.json +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/simulator/scenario.json +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/testing/periodic_device_info.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/testing/test_api_client.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/testing/test_mqtt_connection.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/testing/test_mqtt_messaging.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/testing/test_periodic_minimal.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/pyproject.toml +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/README.md +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/bump_version.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/format.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/lint.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/validate_version.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/setup.cfg +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/setup.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/__init__.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/ble/__init__.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/ble/__main__.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/ble/client.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/ble/mqtt_adapter.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/ble/protocol.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/ble_backend.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/calibration.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/handlers.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/output_formatters.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/presentation.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/recording.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/rich_output.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/config.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/converters.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/device_capabilities.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/encoding.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/enums.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/events.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/__init__.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/account.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/device.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/diagnostics.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/energy.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/feature.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/firmware.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/maintenance.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/schedule.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/status.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/tou.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/__init__.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/_control_commands.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/client.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/command_queue.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/connection.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/control.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/state_tracker.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/subscriptions.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/types.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/utils.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt_events.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/reservations.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/__init__.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/api.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/attribution.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/book.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/circulation.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/controller.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/defaults.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/delivery.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/draw_forecast.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/engine.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/extensions.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/heat_pump_recovery.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/heating_trajectory.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/inputs.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/location_air.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/observed.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/__init__.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/compressor.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/draw.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/element.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/refrigerant.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/tank.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/profile.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/ready.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/record.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/result.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/schedules.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/tank_physics.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/tou_triggers.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/units.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/status_history.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/tou_schedule.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/_sim_site.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/conftest.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/fixtures/sim/engine_golden.json +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/fixtures/sim/heater_params.json +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/fixtures/sim/heater_params_fitted.json +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_api_endpoints_201.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_app_queries.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_auth.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_auth_session_refresh.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_ble_mqtt_adapter.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_ble_protocol.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_canonical_schedule.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_cli_calibrate.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_cli_commands.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_cli_tristate_presentation.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_command_queue.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_device_capabilities.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_device_rest_models.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_events.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_model_converters.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_models.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_control_fail.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_events.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_last_will.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_reliability.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_multi_device.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_navilink_201_enums.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_openei.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_protocol_correctness.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_public_api.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_reservation_validation.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_reservations.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_adapters.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_api.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_book.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_bottom_loss.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_calibrate.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_calibration.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_change_history.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_circulation.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_codec.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_controller.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_defaults.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_draw_forecast.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_draw_loss.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_element.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_element_fit.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_forecast.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_golden.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_hot_water.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_import_contract.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_location_air.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_no_clock_no_state.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_observed.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_profile.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_readiness.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_readiness_fit.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_ready.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_ready_api.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_replay.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_schedules.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_tank_physics.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_status_history.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_tou_schedule.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_unit_system_process_wide.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_utils.py +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tox.ini +0 -0
- {nwp500_python-9.9.0 → nwp500_python-9.10.0}/uv.lock +0 -0
|
@@ -2,6 +2,107 @@
|
|
|
2
2
|
Changelog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
Version 9.10.0 (2026-10-22)
|
|
6
|
+
===========================
|
|
7
|
+
|
|
8
|
+
Added
|
|
9
|
+
-----
|
|
10
|
+
- **The heat book from a heater's values' changes** (#191).
|
|
11
|
+
``sim_adapters.book_from_changes`` books the tank's heat from
|
|
12
|
+
``ChangeRows`` and the instants the heater was heard from (a
|
|
13
|
+
``HeaterHistory``'s ``rows`` and ``observed``, as
|
|
14
|
+
``calibrate_changes`` takes them), with the same steps, anchor search
|
|
15
|
+
and silent-meter refusal as ``book_from_history``. Like the rows, its
|
|
16
|
+
flows are gal/min and its temperatures °F, whatever the preferred
|
|
17
|
+
units. A span the heater
|
|
18
|
+
wasn't heard from (a silence longer than ``max_gap`` of the heard
|
|
19
|
+
instants, or an outage) breaks the book: it is read only from after
|
|
20
|
+
the last one, and is None while the heater isn't heard at ``at``.
|
|
21
|
+
``book_steps_from_changes`` gives the steps, and ``silences`` the
|
|
22
|
+
unheard spans.
|
|
23
|
+
- ``sim_adapters.cold_supply_f``: the mains' plateau with the probe in
|
|
24
|
+
°F, reading no unit preference. The live source's inlet reads through it,
|
|
25
|
+
so a preference changed while it runs changes nothing.
|
|
26
|
+
- ``sim_adapters.cold_supply(..., window=)``: how far back a long draw
|
|
27
|
+
counts (72 hours by default).
|
|
28
|
+
- **The heat book's terms and the circulation loop's loss, fitted
|
|
29
|
+
together** (#196). ``sim_calibrate.fit_book`` fits the book's
|
|
30
|
+
compressor heat rate and slope with the air, its standby UA and a
|
|
31
|
+
circulation loop's loss per gallon from a heater's values' changes,
|
|
32
|
+
the household's draws, and the pump's runs with the loop's flow, each
|
|
33
|
+
given apart from the other. The loss is fitted with the book's terms
|
|
34
|
+
on the pairs of quiet full tanks with a run between. It needs 10
|
|
35
|
+
pairs (10 with a run for the loss), and keeps the base unless the
|
|
36
|
+
refit books the held-out latest fifth no worse, and unless a term
|
|
37
|
+
reaches its bound (``BookTermsFit``). ``sim.calibration.fit_terms_l1``
|
|
38
|
+
fits the terms exactly on ``RecordedBook.pair_terms``, within
|
|
39
|
+
``BOOK_TERM_BOUNDS``. ``sim_adapters.run_readings`` gives the book a
|
|
40
|
+
loop's flow readings within its runs, each run's first held from its
|
|
41
|
+
start, as the fit takes them, and ``sim_adapters.run_flow`` gives runs
|
|
42
|
+
at a flow.
|
|
43
|
+
- **A scenario document over the live scenario** (#201).
|
|
44
|
+
``sim_live.overlay`` lays a scenario document over the heater's
|
|
45
|
+
composed scenario, as ``nwp-cli simulate --live`` does, and the CLI
|
|
46
|
+
now calls it: objects merge key by key (``sim.codec.merge``) and any
|
|
47
|
+
other value replaces. The start, horizon and zone take defaults, and
|
|
48
|
+
the parameters come from the caller, then the document, then the
|
|
49
|
+
heater, then the defaults (``Built``).
|
|
50
|
+
- **The simulator's answers as documents** (#205).
|
|
51
|
+
``sim.codec.to_document`` writes ``Readiness``, ``Recovery``,
|
|
52
|
+
``RecoveryResult``, ``Forecast``, ``ActiveCycle``,
|
|
53
|
+
``ElementModeResult``, ``Deliverable``, ``Trace`` and
|
|
54
|
+
``HeaterProfile`` as JSON documents with ``schema_version`` and a
|
|
55
|
+
``kind`` (``DOCUMENT_KINDS``), as ``result_to_document`` does for a
|
|
56
|
+
walk's result.
|
|
57
|
+
- **The live parts from values read** (#207).
|
|
58
|
+
``sim_live.heater_reading(status, feature)`` and
|
|
59
|
+
``live_schedules(status, reservations, tou)``: what
|
|
60
|
+
``LiveHeaterSource`` and ``LiveScheduleSource`` read, from a status,
|
|
61
|
+
a feature info, the reservations and the TOU periods read any other
|
|
62
|
+
way. Both sources now build their answers through them.
|
|
63
|
+
|
|
64
|
+
Fixed
|
|
65
|
+
-----
|
|
66
|
+
- The live source's inlet looked back 72 hours of wall time, an hour
|
|
67
|
+
more or less than 72 hours across a clock change. It now looks back
|
|
68
|
+
72 elapsed hours.
|
|
69
|
+
- ``book_from_history`` counted a missing meter reading (None or NaN)
|
|
70
|
+
as the meter reporting, so a gap filled with them passed the
|
|
71
|
+
``METER_MAX_GAP`` check and was booked as no flow. Missing readings
|
|
72
|
+
now count as silence.
|
|
73
|
+
|
|
74
|
+
Changed
|
|
75
|
+
-------
|
|
76
|
+
- ``parse_state`` and the simulate how-to say that over Home
|
|
77
|
+
Assistant's REST API an entity's ``last_reported`` comes from a copy
|
|
78
|
+
that a report of the same value doesn't renew, so a sensor holding
|
|
79
|
+
one value for over an hour can read as stale.
|
|
80
|
+
- ``sim.calibration.RecordedBook`` prices a draw against each step's
|
|
81
|
+
inlet, as the heat book does (``book.step_heat_btu``), where it priced
|
|
82
|
+
it against the book's reference. Without an inlet the steps take
|
|
83
|
+
``inlet_default_f``, the reference by default, and nothing changes.
|
|
84
|
+
- ``max_gap``, ``GAP_SAMPLES`` and ``MIN_GAP`` are defined in
|
|
85
|
+
``sim_adapters`` and documented there. ``sim_calibrate`` still exports
|
|
86
|
+
them.
|
|
87
|
+
- **The household's draws come from a demand entity** (#203). The
|
|
88
|
+
simulator no longer works out when hot water was drawn from a water
|
|
89
|
+
meter. ``sim_home_assistant.Entities.demand`` (``demand`` in a
|
|
90
|
+
sources file) is a binary sensor on while the household draws hot
|
|
91
|
+
water, with the circulation's water not among its draws; the live
|
|
92
|
+
inlet's long draws and ``record_inputs``' ``drawing`` come from it
|
|
93
|
+
(``demand_spans``). ``sim.cold_supply.plateau_t_cold``,
|
|
94
|
+
``sim_adapters.cold_supply`` and ``cold_supply_f`` take the draws as
|
|
95
|
+
(start, end) spans (``draws=``) in place of the meter's flow and draw
|
|
96
|
+
instants; ``ColdSupplyParams`` loses ``tick_s``, ``tick_gap_s`` and
|
|
97
|
+
``draw_gpm``.
|
|
98
|
+
|
|
99
|
+
Removed
|
|
100
|
+
-------
|
|
101
|
+
- ``Entities.meter_volume`` and ``meter_flow`` (and their sources-file
|
|
102
|
+
keys), ``sim.meter``, ``sim_home_assistant.draw_spans``,
|
|
103
|
+
``volume_spans`` and ``reports_after``: the simulator's own reading
|
|
104
|
+
of draws from a water meter.
|
|
105
|
+
|
|
5
106
|
Version 9.9.0 (2026-10-22)
|
|
6
107
|
==========================
|
|
7
108
|
|
|
@@ -81,7 +81,8 @@ Idle, it reads the cabinet, about 6 °F warm. So give one of these:
|
|
|
81
81
|
- **Home Assistant:** ``--sources FILE``, the same sources file as
|
|
82
82
|
``simulate run`` takes (:doc:`simulate-heater`). The command reads the
|
|
83
83
|
air beside the heater and its humidity over the record. It also reads
|
|
84
|
-
the
|
|
84
|
+
the household's draws (the ``demand`` entity) and the circulation's
|
|
85
|
+
runs, which make the quiet
|
|
85
86
|
test exact: the heater's own "hot water in use" flag also fires on
|
|
86
87
|
circulation. The air is used only over the span its history covers,
|
|
87
88
|
and not while its sensor was ``unavailable``; the humidity takes the
|
|
@@ -91,7 +92,7 @@ Idle, it reads the cabinet, about 6 °F warm. So give one of these:
|
|
|
91
92
|
loss is fitted from those alone. For the whole record, raise the
|
|
92
93
|
recorder's ``purge_keep_days`` to cover it before you start
|
|
93
94
|
recording, or give the air another way (``--air``). Where the
|
|
94
|
-
|
|
95
|
+
draws' or the circulation's history doesn't cover the whole record,
|
|
95
96
|
the heater's own flag is used instead. ``--air``, ``--ambient`` and
|
|
96
97
|
``--humidity`` override the sources.
|
|
97
98
|
|
|
@@ -221,7 +222,7 @@ Assistant integration, can calibrate the same way:
|
|
|
221
222
|
2. Read the air beside the heater and its humidity over the same span
|
|
222
223
|
with ``sim_home_assistant.record_inputs(client, entities, start,
|
|
223
224
|
end)``, and pass the result as ``record=``. It also brings the
|
|
224
|
-
|
|
225
|
+
household's draws and the circulation's runs. Any client with the same
|
|
225
226
|
``read_history`` as ``HomeAssistantClient`` works. The recorder's
|
|
226
227
|
retention limits the air, as above.
|
|
227
228
|
3. Call ``sim_calibrate.calibrate`` in a worker thread. It takes the
|
|
@@ -274,7 +275,7 @@ without ``keep_better``.
|
|
|
274
275
|
|
|
275
276
|
``sim.calibration.calibrate`` underneath takes the frame
|
|
276
277
|
``sim_adapters.history`` builds; ``sim_adapters.add_inputs`` adds the
|
|
277
|
-
air, the humidity,
|
|
278
|
+
air, the humidity, the household's draws and the circulation's runs to it.
|
|
278
279
|
From there you can also give the draws that price the heat each
|
|
279
280
|
cycle's draws took. The fits for the readiness terms, the heat book,
|
|
280
281
|
the draw response and the elements also run there: see "Improve
|
|
@@ -367,6 +368,101 @@ On a desktop computer, 60 days with a heartbeat every 10 seconds
|
|
|
367
368
|
Assistant's own queries, and 11 to fit, in about 180 MB.
|
|
368
369
|
|
|
369
370
|
|
|
371
|
+
Fit the heat book
|
|
372
|
+
=================
|
|
373
|
+
|
|
374
|
+
The heat book (``book_from_changes``) counts the heat the tank holds
|
|
375
|
+
from the compressor's heat rate and its slope with the air, the standby
|
|
376
|
+
UA, and, with a circulation pump, the loop's loss per gallon
|
|
377
|
+
(``CirculationParams.loss_btu_per_gal``). ``fit_book`` fits them
|
|
378
|
+
together from the heater's history, the household's draws, and the
|
|
379
|
+
pump's runs with the loop's flow. The draws and the loop's flow are
|
|
380
|
+
yours to give, apart from each other: the fit takes every gallon of the
|
|
381
|
+
draws as hot water drawn, and each run at the loop's flow.
|
|
382
|
+
|
|
383
|
+
.. code-block:: python
|
|
384
|
+
|
|
385
|
+
from dataclasses import replace
|
|
386
|
+
|
|
387
|
+
from nwp500 import sim_adapters as sa
|
|
388
|
+
|
|
389
|
+
async with ha.HomeAssistantClient(url, token) as client:
|
|
390
|
+
history = await ha.read_heater_history(client, entities, start, end)
|
|
391
|
+
record = await ha.record_inputs(client, ha.Entities(air=..., humidity=...),
|
|
392
|
+
start, end)
|
|
393
|
+
params = sim_calibrate.calibrate_changes(
|
|
394
|
+
history.rows, history.observed, record=record, notes=history.notes
|
|
395
|
+
).calibration.params
|
|
396
|
+
fit = sim_calibrate.fit_book(
|
|
397
|
+
history.rows, history.observed, params,
|
|
398
|
+
flow=draws, runs=runs, circulation_flow=loop_flow,
|
|
399
|
+
air=record.air or (),
|
|
400
|
+
)
|
|
401
|
+
params = replace(params, book=fit.book)
|
|
402
|
+
|
|
403
|
+
``draws`` and ``loop_flow`` are (instant, gal/min) readings and ``runs``
|
|
404
|
+
the pump's (start, end) spans; ``record_inputs``' air is °F, as the fit
|
|
405
|
+
takes it. ``fit.loss_btu_per_gal`` is the loop's loss for your
|
|
406
|
+
``CirculationParams`` (None without one).
|
|
407
|
+
|
|
408
|
+
The fit reads the pairs of quiet full tanks up to
|
|
409
|
+
``book.max_anchor_age_h`` apart, and fits each term for the least mean
|
|
410
|
+
absolute error between the book and the probes at the second of each
|
|
411
|
+
pair:
|
|
412
|
+
|
|
413
|
+
- **The book's terms:** the compressor's heat rate and slope, and the
|
|
414
|
+
standby UA, on every pair. A term the pairs can't tell apart from
|
|
415
|
+
the others (the slope, with the air never off one temperature) stays
|
|
416
|
+
at ``params.book``. The rest of the book (its draw line, the anchors'
|
|
417
|
+
age, the valve's line) is ``params``'.
|
|
418
|
+
- **The loop's loss per gallon:** on 10 pairs with a run between,
|
|
419
|
+
with the book's terms. With fewer, the loss is
|
|
420
|
+
``params.circulation``'s, and without one, the pairs with a run are
|
|
421
|
+
left out. A pair with a run that has no flow reading is left out.
|
|
422
|
+
|
|
423
|
+
The fit needs 10 pairs. It holds out the latest fifth and replaces the
|
|
424
|
+
base only when it books them no worse. A term on its bound
|
|
425
|
+
(``sim.calibration.BOOK_TERM_BOUNDS``) is no fit: the book misses
|
|
426
|
+
something between those anchors that the term is standing in for. The
|
|
427
|
+
loop's loss on its bound, or one the pairs can't tell from the book's
|
|
428
|
+
terms (runs that follow the recoveries), leaves the loss at the base's;
|
|
429
|
+
any other term on its bound keeps the base. ``fit.kept`` says which it
|
|
430
|
+
kept and ``fit.why`` why. A span the heater wasn't heard from, a probe
|
|
431
|
+
read nothing, or the draws were silent over an hour, breaks the book.
|
|
432
|
+
|
|
433
|
+
A pair with a run needs a quiet full tank on each side of it, so they
|
|
434
|
+
come slowly. With a pump running about once a day, one installation had
|
|
435
|
+
5 in 14 days and 19 in 42: fit on four weeks or more, and raise Home
|
|
436
|
+
Assistant's ``purge_keep_days`` to keep them.
|
|
437
|
+
|
|
438
|
+
Book with the same inputs. Runs are priced with the loop's loss, in a
|
|
439
|
+
``CirculationParams`` of your installation's with ``meter_share`` 0,
|
|
440
|
+
since the draws hold none of the loop's water; without a loss, they
|
|
441
|
+
can't be booked. ``sa.run_readings`` gives the book each run's flow as
|
|
442
|
+
the fit takes it: its own readings, the first from its start, and none
|
|
443
|
+
read outside the runs.
|
|
444
|
+
|
|
445
|
+
.. code-block:: python
|
|
446
|
+
|
|
447
|
+
from nwp500.sim.params import CirculationParams
|
|
448
|
+
|
|
449
|
+
book = None
|
|
450
|
+
loss = fit.loss_btu_per_gal
|
|
451
|
+
if loss is not None or not runs:
|
|
452
|
+
loop = None
|
|
453
|
+
if loss is not None:
|
|
454
|
+
loop = CirculationParams(
|
|
455
|
+
displaced_gal=..., flow_gpm=..., loss_btu_per_gal=loss,
|
|
456
|
+
meter_share=0.0,
|
|
457
|
+
)
|
|
458
|
+
book = sa.book_from_changes(
|
|
459
|
+
history.rows, history.observed, replace(params, circulation=loop),
|
|
460
|
+
at=end, flow=draws, circulation=runs,
|
|
461
|
+
circulation_flow=sa.run_readings(runs, loop_flow),
|
|
462
|
+
air=record.air or (),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
370
466
|
Limitations
|
|
371
467
|
===========
|
|
372
468
|
|
|
@@ -138,8 +138,7 @@ pass it with ``--sources`` (or set ``NWP500_SOURCES``):
|
|
|
138
138
|
air = "sensor.beside_the_heater_temperature"
|
|
139
139
|
humidity = "sensor.beside_the_heater_humidity"
|
|
140
140
|
inlet = "sensor.cold_supply_temperature"
|
|
141
|
-
|
|
142
|
-
meter_flow = "sensor.water_flow_rate"
|
|
141
|
+
demand = "binary_sensor.hot_water_demand" # on while hot water is drawn
|
|
143
142
|
circulation_speed = "sensor.circulation_pump_speed"
|
|
144
143
|
weather = "weather.forecast_home"
|
|
145
144
|
|
|
@@ -167,9 +166,17 @@ give:
|
|
|
167
166
|
- **the humidity:** a reading at most an hour old;
|
|
168
167
|
- **the inlet:** the plateau of the long draws (11 minutes or more,
|
|
169
168
|
circulation off) in the last 72 hours, from the inlet probe and the
|
|
170
|
-
|
|
169
|
+
``demand`` entity: a binary sensor that is on while the household
|
|
170
|
+
draws hot water, with the circulation's water not among its draws. A
|
|
171
171
|
circulation pump with no speed reading leaves no estimate.
|
|
172
172
|
|
|
173
|
+
A reading's age is from when Home Assistant last heard the sensor
|
|
174
|
+
report (``last_reported``). Over the REST API, Home Assistant serves
|
|
175
|
+
that from a copy of the state it keeps until the state changes, and a
|
|
176
|
+
report of the same value doesn't renew the copy. So a sensor that has
|
|
177
|
+
held one value for over an hour can read as over an hour old, and its part
|
|
178
|
+
keeps its default, even while it still reports.
|
|
179
|
+
|
|
173
180
|
Units come from each entity's ``unit_of_measurement``: the pump's speed
|
|
174
181
|
in rpm or Hz. A part that can't be read, or is stale, keeps its
|
|
175
182
|
default, and a warning on stderr says why. ``--ambient``, ``--inlet``
|
|
@@ -217,6 +224,35 @@ source when TOU is on and its periods cannot be read. A missing
|
|
|
217
224
|
feature info leaves ``composed.params`` None (the tank size is
|
|
218
225
|
unknown). Change a part with ``composed.scenario.model_copy(update=...)``.
|
|
219
226
|
|
|
227
|
+
With the status, the feature info, the reservations and the TOU
|
|
228
|
+
periods already read some other way, build the same parts from them:
|
|
229
|
+
``sim_live.heater_reading(status, feature)`` is what
|
|
230
|
+
``LiveHeaterSource`` reads, and ``sim_live.live_schedules(status,
|
|
231
|
+
reservations, tou)`` what ``LiveScheduleSource`` reads. Pass either to
|
|
232
|
+
``compose`` as ``sim_live.Given(...)``. ``tou`` is the ``TOUInfo`` the
|
|
233
|
+
TOU read gives, or the ``TOUPeriod`` list itself; the TOU switch is the
|
|
234
|
+
status's. ``live_schedules`` raises ``LiveReadError`` when the status
|
|
235
|
+
has TOU on and ``tou`` is None.
|
|
236
|
+
|
|
237
|
+
To lay a scenario document over the heater as it is now, as
|
|
238
|
+
``nwp-cli simulate --scenario ... --live`` does, use ``overlay``:
|
|
239
|
+
|
|
240
|
+
.. code-block:: python
|
|
241
|
+
|
|
242
|
+
built = sim_live.overlay(composed, doc, now=datetime.now(UTC))
|
|
243
|
+
result = sim.simulate(built.scenario, built.params)
|
|
244
|
+
|
|
245
|
+
Objects merge key by key, and any other value, a list included,
|
|
246
|
+
replaces the one under it, so ``{"schema_version": 1, "scenario":
|
|
247
|
+
{"settings": {"setpoint_f": 125}}}`` changes the setpoint and keeps
|
|
248
|
+
the rest of the heater's settings. ``composed`` or ``doc`` may be None.
|
|
249
|
+
Where neither gives one, the start is ``now``, the horizon 24 hours,
|
|
250
|
+
and the zone ``tz=``. The parameters are ``params=``, then the
|
|
251
|
+
document's, then the heater's, then the 65 gal defaults (with a note in
|
|
252
|
+
``built.notes``). ``replace=`` takes parts that stand in whole for the
|
|
253
|
+
scenario's, and ``over=`` parts laid on after them. A document that
|
|
254
|
+
doesn't make a scenario raises ``ValueError``.
|
|
255
|
+
|
|
220
256
|
To have the models themselves, read them directly. Each MQTT read is a
|
|
221
257
|
request answered on a subscription:
|
|
222
258
|
|
|
@@ -824,6 +860,30 @@ heater's statuses, book the tank's heat from the record and pass it as
|
|
|
824
860
|
)
|
|
825
861
|
readiness = sim.time_to_ready(scenario, params, book=book)
|
|
826
862
|
|
|
863
|
+
With the heater's values recorded as changes instead (Home Assistant's
|
|
864
|
+
entity history, which ``read_heater_history`` reads), book from those,
|
|
865
|
+
with the instants the heater was heard from:
|
|
866
|
+
|
|
867
|
+
.. code-block:: python
|
|
868
|
+
|
|
869
|
+
from nwp500 import sim_home_assistant as ha
|
|
870
|
+
|
|
871
|
+
history = await ha.read_heater_history(client, entities, start, now)
|
|
872
|
+
book = sa.book_from_changes(
|
|
873
|
+
history.rows,
|
|
874
|
+
history.observed,
|
|
875
|
+
params,
|
|
876
|
+
at=now,
|
|
877
|
+
flow=draws_gpm, # gal/min, whatever the preferred units
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
Like the history's rows, its ``flow`` and ``circulation_flow`` are
|
|
881
|
+
gal/min and its ``inlet`` and ``air`` °F, whatever the preferred units.
|
|
882
|
+
A span the heater wasn't heard from (an outage, or a silence much
|
|
883
|
+
longer than its usual reporting) breaks the book: it is walked only
|
|
884
|
+
from an anchor after the last one, and is None while the heater isn't
|
|
885
|
+
heard now.
|
|
886
|
+
|
|
827
887
|
Without a meter, start one only when you know the tank is full and
|
|
828
888
|
quiet (``sa.full_tank_book``).
|
|
829
889
|
|
|
@@ -835,12 +895,18 @@ the air around the pipe:
|
|
|
835
895
|
|
|
836
896
|
from nwp500.sim.cold_supply import on_the_mains
|
|
837
897
|
|
|
838
|
-
cold = sa.cold_supply(inlet_probe, at=now,
|
|
898
|
+
cold = sa.cold_supply(inlet_probe, at=now, draws=draws,
|
|
839
899
|
circulation=runs)
|
|
840
900
|
book, measured = on_the_mains(book, cold)
|
|
841
901
|
readiness = sim.time_to_ready(scenario, params, book=book,
|
|
842
902
|
inlet_measured=measured)
|
|
843
903
|
|
|
904
|
+
``draws`` are the (start, end) spans the household drew hot water,
|
|
905
|
+
without the circulation's water. ``sa.cold_supply`` takes the probe in
|
|
906
|
+
the preferred unit. ``sa.cold_supply_f`` takes it in °F and reads no
|
|
907
|
+
unit preference, for a caller whose preference may change while it
|
|
908
|
+
runs.
|
|
909
|
+
|
|
844
910
|
The mains is the median plateau of the long draws (11 minutes or more,
|
|
845
911
|
with circulation off) over the last 72 hours, read from minute 9 of each
|
|
846
912
|
draw, once the probe has passed the stagnant pipe. There is none
|
|
@@ -1162,7 +1228,21 @@ the ones you want to look at again, or attach one to a bug report:
|
|
|
1162
1228
|
scenario, params = codec.scenario_from_document(f.read())
|
|
1163
1229
|
|
|
1164
1230
|
``codec.result_to_document(result)`` writes a result the same
|
|
1165
|
-
way.
|
|
1231
|
+
way. The other answers (``time_to_ready``, ``next_recovery``,
|
|
1232
|
+
``recovery``, ``forecast``, ``active_cycle``, ``element_mode``,
|
|
1233
|
+
``readiness``, ``standby`` and ``heater_profile``) are documents through
|
|
1234
|
+
``codec.to_document``:
|
|
1235
|
+
|
|
1236
|
+
.. code-block:: python
|
|
1237
|
+
|
|
1238
|
+
doc = codec.to_document(sim.time_to_ready(scenario, params))
|
|
1239
|
+
doc["kind"] # "readiness"
|
|
1240
|
+
doc["deliverable_gal"] # the answer's fields, at the top level
|
|
1241
|
+
|
|
1242
|
+
Each document has ``schema_version`` and ``kind``, the answer's type
|
|
1243
|
+
in snake case (``codec.DOCUMENT_KINDS``). Instants are ISO 8601 strings,
|
|
1244
|
+
nested answers are objects, and a number that isn't finite is null, so
|
|
1245
|
+
the document is strict JSON.
|
|
1166
1246
|
|
|
1167
1247
|
|
|
1168
1248
|
See also
|
|
@@ -154,6 +154,7 @@ Hot water and the heat book
|
|
|
154
154
|
clean_draws, CleanDraw, crossing,
|
|
155
155
|
demonstrated, frontier, capacity, held_out, fit_evidence,
|
|
156
156
|
EvidenceFit, cold_lower_cap, RecordedBook, fit_book_terms, BookFit,
|
|
157
|
+
fit_terms_l1, BOOK_TERMS, BOOK_TERM_BOUNDS,
|
|
157
158
|
book_error_bands, book_error_btu, empty_point,
|
|
158
159
|
zone_empty_point, withheld_btu, book_walk, long_draws,
|
|
159
160
|
front_frame, fit_front, FrontFit, settle_tails, fit_front_settle,
|
|
@@ -184,9 +185,6 @@ Hot water and the heat book
|
|
|
184
185
|
.. automodule:: nwp500.sim.cold_supply
|
|
185
186
|
:members: plateau_t_cold, ColdSupply, ColdSupplyParams, on_the_mains
|
|
186
187
|
|
|
187
|
-
.. automodule:: nwp500.sim.meter
|
|
188
|
-
:members: flow_gpm
|
|
189
|
-
|
|
190
188
|
.. automodule:: nwp500.sim.record
|
|
191
189
|
:members: Record, RecordedWrite, Recorded, RecordedStarts, corroborator
|
|
192
190
|
|
|
@@ -291,7 +289,9 @@ Live sources
|
|
|
291
289
|
:members: compose, Composed, Source, Given, HeaterReading,
|
|
292
290
|
LiveHeaterSource, LiveScheduleSource, LiveReadError, read_status,
|
|
293
291
|
read_feature, read_reservations, HeaterSource, ScheduleSource,
|
|
294
|
-
EnvironmentSource, LoadSource, DEFAULT_TIMEOUT
|
|
292
|
+
EnvironmentSource, LoadSource, DEFAULT_TIMEOUT, overlay, Built,
|
|
293
|
+
DEFAULT_HORIZON, DEFAULT_PARAMS_NOTE, zone_name, heater_reading,
|
|
294
|
+
live_schedules
|
|
295
295
|
|
|
296
296
|
|
|
297
297
|
Recorded history
|
|
@@ -301,9 +301,10 @@ Recorded history
|
|
|
301
301
|
:members:
|
|
302
302
|
|
|
303
303
|
.. automodule:: nwp500.sim_calibrate
|
|
304
|
-
:members: calibrate, calibrate_changes, Calibrated, report, Term,
|
|
304
|
+
:members: calibrate, calibrate_changes, Calibrated, report, Term,
|
|
305
|
+
fit_book, BookTermsFit, MIN_BOOK_PAIRS, MIN_RUN_PAIRS,
|
|
305
306
|
score, Judged, HeldOut, GROUPS, air_unread_note,
|
|
306
|
-
CalibrationInputError, DEFAULT_HUMIDITY_PCT,
|
|
307
|
+
CalibrationInputError, DEFAULT_HUMIDITY_PCT,
|
|
307
308
|
DEFAULT_BASE_NOTE, NO_AIR_NOTE
|
|
308
309
|
|
|
309
310
|
|
|
@@ -312,9 +313,9 @@ Home Assistant sources
|
|
|
312
313
|
|
|
313
314
|
.. automodule:: nwp500.sim_home_assistant
|
|
314
315
|
:members: HomeAssistantEnvironment, HomeAssistantClient, record_inputs,
|
|
315
|
-
RecordInputs, Sources, Entities, sources_from, State, parse_state,
|
|
316
|
+
RecordInputs, INLET_LOOKBACK, Sources, Entities, sources_from, State, parse_state,
|
|
316
317
|
parse_history, parse_outages, parse_states, History, history_for,
|
|
317
318
|
parse_forecast, read_heater_history, HeaterEntities, HeaterHistory,
|
|
318
319
|
HISTORY_WINDOW, to_w, to_wh,
|
|
319
|
-
pump_runs,
|
|
320
|
+
pump_runs, demand_spans, busy, covers_start, percent,
|
|
320
321
|
fresh, to_f, to_gpm, to_gal, to_rpm, HomeAssistantError
|
|
@@ -519,6 +519,13 @@ tank over the cold water of a recent draw. Start one only from what you know:
|
|
|
519
519
|
- You record the heater's statuses and meter the hot water: the
|
|
520
520
|
book is walked from the last time the record shows the tank
|
|
521
521
|
full and quiet after a recovery (below).
|
|
522
|
+
* - ``sim_adapters.book_from_changes(rows, observed, params, at=...,
|
|
523
|
+
flow=...)``
|
|
524
|
+
- The same, from the heater's values' changes (``ChangeRows``) and
|
|
525
|
+
the instants it was heard from, as Home Assistant records them,
|
|
526
|
+
with the flows in gal/min and the temperatures in °F.
|
|
527
|
+
Only an anchor after the last span the heater wasn't heard from
|
|
528
|
+
counts.
|
|
522
529
|
* - A ``Booked`` from your own record
|
|
523
530
|
- You keep a book of the tank's heat some other way.
|
|
524
531
|
|
|
@@ -540,7 +547,17 @@ median of the probe's readings over the last 3 hours
|
|
|
540
547
|
(``book.inlet_window_s``), the default without one. The meter must
|
|
541
548
|
report at rest too: a silence over an hour in the window is refused,
|
|
542
549
|
since it would read as a quiet full tank. A missing reading (None or
|
|
543
|
-
NaN) is skipped, and the value before it holds
|
|
550
|
+
NaN) is skipped, and the value before it holds; it doesn't count as the
|
|
551
|
+
meter reporting.
|
|
552
|
+
|
|
553
|
+
``book_from_changes`` grids the changes the same way, each value held
|
|
554
|
+
until its next change; without a compressor flag, the heat source says
|
|
555
|
+
when the compressor runs. A value that holds writes no change, so only
|
|
556
|
+
``observed`` says the heater was heard. A silence between those
|
|
557
|
+
instants longer than ``max_gap`` of them (4 times their usual spacing,
|
|
558
|
+
and at least 5 minutes), or one of ``rows.outages``, is a span nobody
|
|
559
|
+
saw. The book is read only from after the last such span, and is None
|
|
560
|
+
when the heater isn't heard at ``at``.
|
|
544
561
|
|
|
545
562
|
To build the steps yourself, pass them to ``book.book_steps``.
|
|
546
563
|
``BookStep.circulation_in_meter_gpm`` is
|
|
@@ -1276,8 +1276,8 @@ async def simulate_record(
|
|
|
1276
1276
|
"--sources",
|
|
1277
1277
|
type=click.Path(exists=True, dir_okay=False),
|
|
1278
1278
|
envvar="NWP500_SOURCES",
|
|
1279
|
-
help="A TOML file naming where the air, its humidity, the
|
|
1280
|
-
"the circulation are recorded (Home Assistant)",
|
|
1279
|
+
help="A TOML file naming where the air, its humidity, the hot water "
|
|
1280
|
+
"demand and the circulation are recorded (Home Assistant)",
|
|
1281
1281
|
)
|
|
1282
1282
|
@click.pass_context
|
|
1283
1283
|
def simulate_calibrate(
|
|
@@ -32,6 +32,9 @@ from nwp500.sim.codec import (
|
|
|
32
32
|
result_to_document,
|
|
33
33
|
scenario_to_document,
|
|
34
34
|
)
|
|
35
|
+
from nwp500.sim.codec import (
|
|
36
|
+
merge as merge,
|
|
37
|
+
)
|
|
35
38
|
from nwp500.sim.engine import SimulationError
|
|
36
39
|
from nwp500.sim.params import params_from_document
|
|
37
40
|
|
|
@@ -47,7 +50,7 @@ __license__ = "MIT"
|
|
|
47
50
|
_logger = logging.getLogger(__name__)
|
|
48
51
|
|
|
49
52
|
#: How far a run looks ahead when nothing else says.
|
|
50
|
-
DEFAULT_HOURS =
|
|
53
|
+
DEFAULT_HOURS = sim_live.DEFAULT_HORIZON / timedelta(hours=1)
|
|
51
54
|
|
|
52
55
|
|
|
53
56
|
class InputError(ValueError):
|
|
@@ -69,23 +72,9 @@ def read_document(path: str | Path, what: str) -> dict[str, Any]:
|
|
|
69
72
|
def zone_name(name: str) -> str:
|
|
70
73
|
"""``name``, when it is an IANA zone this machine knows."""
|
|
71
74
|
try:
|
|
72
|
-
|
|
73
|
-
except
|
|
74
|
-
raise InputError(
|
|
75
|
-
return name
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def merge(base: Mapping[str, Any], over: Mapping[str, Any]) -> dict[str, Any]:
|
|
79
|
-
"""``base`` with ``over`` laid on it: objects merge key by key; any
|
|
80
|
-
other value, a list included, replaces the one under it."""
|
|
81
|
-
out = dict(base)
|
|
82
|
-
for key, value in over.items():
|
|
83
|
-
under = out.get(key)
|
|
84
|
-
if isinstance(value, Mapping) and isinstance(under, Mapping):
|
|
85
|
-
out[key] = merge(under, value) # pyright: ignore[reportUnknownArgumentType]
|
|
86
|
-
else:
|
|
87
|
-
out[key] = value
|
|
88
|
-
return out
|
|
75
|
+
return sim_live.zone_name(name)
|
|
76
|
+
except ValueError as e:
|
|
77
|
+
raise InputError(str(e)) from None
|
|
89
78
|
|
|
90
79
|
|
|
91
80
|
def local_zone() -> str | None:
|
|
@@ -110,6 +99,16 @@ def local_zone() -> str | None:
|
|
|
110
99
|
return None
|
|
111
100
|
|
|
112
101
|
|
|
102
|
+
#: A scenario ready to walk, with its parameters.
|
|
103
|
+
Built = sim_live.Built
|
|
104
|
+
|
|
105
|
+
#: The library's notes, naming the command's options.
|
|
106
|
+
_OPTION_NOTES = {
|
|
107
|
+
sim_live.DEFAULT_PARAMS_NOTE: "the tank size is unknown: using the 65 "
|
|
108
|
+
"gal defaults (give --params)",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
113
112
|
@dataclass(frozen=True)
|
|
114
113
|
class Inputs:
|
|
115
114
|
"""What the command was given. Files are read already; ``ambient``
|
|
@@ -169,78 +168,48 @@ class Inputs:
|
|
|
169
168
|
)
|
|
170
169
|
|
|
171
170
|
|
|
172
|
-
@dataclass(frozen=True)
|
|
173
|
-
class Built:
|
|
174
|
-
"""A scenario ready to walk, with its parameters."""
|
|
175
|
-
|
|
176
|
-
scenario: sim.Scenario
|
|
177
|
-
params: sim.HeaterParams
|
|
178
|
-
#: What the driver assumed, for the user to see.
|
|
179
|
-
notes: tuple[str, ...] = ()
|
|
180
|
-
|
|
181
|
-
|
|
182
171
|
def build(
|
|
183
172
|
inputs: Inputs, live: sim_live.Composed | None, now: datetime
|
|
184
173
|
) -> Built:
|
|
185
174
|
"""The scenario: the device's parts (``live``), the scenario
|
|
186
|
-
document's laid over them, then the flags'.
|
|
187
|
-
when nothing gives one."""
|
|
188
|
-
|
|
189
|
-
if live is not None:
|
|
190
|
-
data = scenario_to_document(live.scenario)["scenario"]
|
|
191
|
-
if inputs.scenario is not None:
|
|
192
|
-
data = merge(data, inputs.scenario["scenario"])
|
|
175
|
+
document's laid over them (``sim_live.overlay``), then the flags'.
|
|
176
|
+
``now`` is the start when nothing gives one."""
|
|
177
|
+
replace: dict[str, Any] = {}
|
|
193
178
|
if inputs.start is not None:
|
|
194
|
-
|
|
195
|
-
data.setdefault("start", now)
|
|
179
|
+
replace["start"] = inputs.start
|
|
196
180
|
if inputs.hours is not None:
|
|
197
|
-
|
|
198
|
-
data.setdefault("horizon", timedelta(hours=DEFAULT_HOURS))
|
|
181
|
+
replace["horizon"] = timedelta(hours=inputs.hours)
|
|
199
182
|
if inputs.tz is not None:
|
|
200
|
-
|
|
201
|
-
elif "tz" not in data:
|
|
202
|
-
data["tz"] = inputs.zone()
|
|
203
|
-
elif isinstance(data["tz"], str):
|
|
204
|
-
# The model's own check raises a KeyError for an unknown zone.
|
|
205
|
-
zone_name(data["tz"])
|
|
183
|
+
replace["tz"] = inputs.tz
|
|
206
184
|
if inputs.environment is not None:
|
|
207
|
-
|
|
208
|
-
if inputs.ambient is not None:
|
|
209
|
-
data["environment"] = {
|
|
210
|
-
**data.get("environment", {}),
|
|
211
|
-
"ambient_f": sa.to_sim_temperature(inputs.ambient),
|
|
212
|
-
}
|
|
213
|
-
if inputs.inlet is not None:
|
|
214
|
-
data["environment"] = {
|
|
215
|
-
**data.get("environment", {}),
|
|
216
|
-
"inlet_f": sa.to_sim_temperature(inputs.inlet),
|
|
217
|
-
}
|
|
185
|
+
replace["environment"] = dict(inputs.environment)
|
|
218
186
|
if inputs.loads is not None:
|
|
219
|
-
|
|
187
|
+
replace["loads"] = dict(inputs.loads)
|
|
220
188
|
if inputs.commands is not None:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
doc_params = (
|
|
229
|
-
None if inputs.scenario is None else inputs.scenario.get("params")
|
|
230
|
-
)
|
|
189
|
+
replace["commands"] = dict(inputs.commands)
|
|
190
|
+
environment: dict[str, float] = {}
|
|
191
|
+
if inputs.ambient is not None:
|
|
192
|
+
environment["ambient_f"] = sa.to_sim_temperature(inputs.ambient)
|
|
193
|
+
if inputs.inlet is not None:
|
|
194
|
+
environment["inlet_f"] = sa.to_sim_temperature(inputs.inlet)
|
|
195
|
+
params = None
|
|
231
196
|
if inputs.params is not None:
|
|
232
197
|
params = read_params(inputs.params, "--params")
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
198
|
+
try:
|
|
199
|
+
built = sim_live.overlay(
|
|
200
|
+
live,
|
|
201
|
+
inputs.scenario,
|
|
202
|
+
now=now,
|
|
203
|
+
# A live scenario carries its zone.
|
|
204
|
+
tz=None if live is not None else inputs.zone(),
|
|
205
|
+
params=params,
|
|
206
|
+
replace=replace,
|
|
207
|
+
over={"environment": environment} if environment else None,
|
|
242
208
|
)
|
|
243
|
-
|
|
209
|
+
except ValueError as e:
|
|
210
|
+
raise InputError(str(e)) from None
|
|
211
|
+
notes = tuple(_OPTION_NOTES.get(n, n) for n in built.notes)
|
|
212
|
+
return Built(built.scenario, built.params, notes)
|
|
244
213
|
|
|
245
214
|
|
|
246
215
|
def read_params(doc: Any, what: str) -> sim.HeaterParams:
|