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.
Files changed (345) hide show
  1. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/CHANGELOG.rst +101 -0
  2. {nwp500_python-9.9.0/src/nwp500_python.egg-info → nwp500_python-9.10.0}/PKG-INFO +1 -1
  3. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/calibrate-simulator.rst +100 -4
  4. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/simulate-heater.rst +85 -5
  5. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/sim.rst +9 -8
  6. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/simulator-inputs.rst +18 -1
  7. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/__main__.py +2 -2
  8. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/simulate.py +46 -77
  9. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/calibration.py +91 -11
  10. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/codec.py +88 -2
  11. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/cold_supply.py +13 -52
  12. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/decay.py +3 -1
  13. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/params.py +4 -1
  14. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim_adapters.py +412 -84
  15. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim_calibrate.py +444 -36
  16. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim_home_assistant.py +105 -168
  17. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim_live.py +149 -16
  18. {nwp500_python-9.9.0 → nwp500_python-9.10.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  19. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/SOURCES.txt +3 -2
  20. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/scm_file_list.json +3 -2
  21. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/scm_version.json +2 -2
  22. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_cli_simulate.py +3 -1
  23. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_book_history.py +185 -0
  24. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_cold_supply.py +41 -64
  25. nwp500_python-9.10.0/tests/test_sim_documents.py +127 -0
  26. nwp500_python-9.10.0/tests/test_sim_fit_book.py +695 -0
  27. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_home_assistant.py +179 -121
  28. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_live.py +47 -0
  29. nwp500_python-9.10.0/tests/test_sim_overlay.py +150 -0
  30. nwp500_python-9.9.0/src/nwp500/sim/meter.py +0 -130
  31. nwp500_python-9.9.0/tests/test_sim_meter.py +0 -163
  32. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.coveragerc +0 -0
  33. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  34. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.github/copilot-instructions.md +0 -0
  35. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.github/workflows/ci.yml +0 -0
  36. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.github/workflows/release.yml +0 -0
  37. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.gitignore +0 -0
  38. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.pre-commit-config.yaml +0 -0
  39. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/.readthedocs.yml +0 -0
  40. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/AUTHORS.rst +0 -0
  41. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/CONTRIBUTING.rst +0 -0
  42. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/LICENSE.txt +0 -0
  43. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/Makefile +0 -0
  44. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/README.rst +0 -0
  45. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/RELEASE.md +0 -0
  46. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/Makefile +0 -0
  47. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/_static/.gitignore +0 -0
  48. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/conf.py +0 -0
  49. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/architecture.rst +0 -0
  50. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/draws.rst +0 -0
  51. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/heating-elements.rst +0 -0
  52. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/index.rst +0 -0
  53. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/recovery-speed.rst +0 -0
  54. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/simulator.rst +0 -0
  55. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/standby-loss.rst +0 -0
  56. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/tank-energy.rst +0 -0
  57. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/tou-recovery-cap.rst +0 -0
  58. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/unknown-values.rst +0 -0
  59. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/explanation/what-starts-a-recovery.rst +0 -0
  60. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/authenticate.rst +0 -0
  61. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/auto-recovery.rst +0 -0
  62. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/bluetooth.rst +0 -0
  63. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/diagnose-mqtt.rst +0 -0
  64. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/home-assistant.rst +0 -0
  65. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/index.rst +0 -0
  66. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/maintenance.rst +0 -0
  67. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/manage-units.rst +0 -0
  68. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/monitor-status.rst +0 -0
  69. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/optimize-tou.rst +0 -0
  70. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/queue-commands.rst +0 -0
  71. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/schedule-operation.rst +0 -0
  72. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/how-to/track-energy.rst +0 -0
  73. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/index.rst +0 -0
  74. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/openapi.yaml +0 -0
  75. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/project/authors.rst +0 -0
  76. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/project/changelog.rst +0 -0
  77. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/project/contributing.rst +0 -0
  78. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/project/history.rst +0 -0
  79. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/project/license.rst +0 -0
  80. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/configuration.rst +0 -0
  81. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/enumerations.rst +0 -0
  82. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/index.rst +0 -0
  83. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/installation.rst +0 -0
  84. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/data_conversions.rst +0 -0
  85. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/device_features.rst +0 -0
  86. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/device_status.rst +0 -0
  87. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/error_codes.rst +0 -0
  88. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  89. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/quick_reference.rst +0 -0
  90. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/protocol/rest_api.rst +0 -0
  91. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/api_client.rst +0 -0
  92. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/auth_client.rst +0 -0
  93. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/cli.rst +0 -0
  94. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/events.rst +0 -0
  95. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/exceptions.rst +0 -0
  96. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/models.rst +0 -0
  97. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/reference/python_api/mqtt_client.rst +0 -0
  98. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/requirements.txt +0 -0
  99. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/docs/tutorials/getting-started.rst +0 -0
  100. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/.ruff.toml +0 -0
  101. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/README.md +0 -0
  102. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/air_filter_reset.py +0 -0
  103. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/anti_legionella.py +0 -0
  104. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/auto_recovery.py +0 -0
  105. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/combined_callbacks.py +0 -0
  106. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/demand_response.py +0 -0
  107. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/device_capabilities.py +0 -0
  108. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/device_status_debug.py +0 -0
  109. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/energy_analytics.py +0 -0
  110. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/error_code_demo.py +0 -0
  111. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/firmware_payload_capture.py +0 -0
  112. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/mqtt_diagnostics.py +0 -0
  113. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/power_control.py +0 -0
  114. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/recirculation_control.py +0 -0
  115. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/reconnection_demo.py +0 -0
  116. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/reservation_schedule.py +0 -0
  117. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/simple_auto_recovery.py +0 -0
  118. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/token_restoration.py +0 -0
  119. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/tou_openei.py +0 -0
  120. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/tou_schedule.py +0 -0
  121. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/advanced/water_reservation.py +0 -0
  122. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/beginner/01_authentication.py +0 -0
  123. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/beginner/02_list_devices.py +0 -0
  124. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/beginner/03_get_status.py +0 -0
  125. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/beginner/04_set_temperature.py +0 -0
  126. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
  127. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/command_queue.py +0 -0
  128. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/device_status_callback.py +0 -0
  129. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/error_handling.py +0 -0
  130. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/event_driven_control.py +0 -0
  131. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/improved_auth.py +0 -0
  132. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
  133. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  134. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/periodic_requests.py +0 -0
  135. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/set_mode.py +0 -0
  136. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/intermediate/vacation_mode.py +0 -0
  137. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/mask.py +0 -0
  138. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/simulator/lower-setpoint.json +0 -0
  139. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/simulator/plan.json +0 -0
  140. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/simulator/scenario.json +0 -0
  141. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/testing/periodic_device_info.py +0 -0
  142. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/testing/simple_periodic_info.py +0 -0
  143. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/testing/test_api_client.py +0 -0
  144. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/testing/test_mqtt_connection.py +0 -0
  145. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/testing/test_mqtt_messaging.py +0 -0
  146. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/examples/testing/test_periodic_minimal.py +0 -0
  147. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/pyproject.toml +0 -0
  148. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/README.md +0 -0
  149. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/bump_version.py +0 -0
  150. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/diagnose_mqtt_connection.py +0 -0
  151. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/extract_changelog.py +0 -0
  152. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/format.py +0 -0
  153. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/lint.py +0 -0
  154. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/setup-dev.py +0 -0
  155. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/scripts/validate_version.py +0 -0
  156. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/setup.cfg +0 -0
  157. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/setup.py +0 -0
  158. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/__init__.py +0 -0
  159. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/_base.py +0 -0
  160. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/api_client.py +0 -0
  161. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/auth.py +0 -0
  162. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/ble/__init__.py +0 -0
  163. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/ble/__main__.py +0 -0
  164. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/ble/client.py +0 -0
  165. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/ble/mqtt_adapter.py +0 -0
  166. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/ble/protocol.py +0 -0
  167. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/__init__.py +0 -0
  168. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/ble_backend.py +0 -0
  169. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/calibration.py +0 -0
  170. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/handlers.py +0 -0
  171. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/monitoring.py +0 -0
  172. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/output_formatters.py +0 -0
  173. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/presentation.py +0 -0
  174. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/recording.py +0 -0
  175. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/rich_output.py +0 -0
  176. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/cli/token_storage.py +0 -0
  177. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/command_decorators.py +0 -0
  178. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/config.py +0 -0
  179. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/converters.py +0 -0
  180. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/device_capabilities.py +0 -0
  181. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/device_info_cache.py +0 -0
  182. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/encoding.py +0 -0
  183. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/enums.py +0 -0
  184. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/events.py +0 -0
  185. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/exceptions.py +0 -0
  186. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/factory.py +0 -0
  187. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/field_factory.py +0 -0
  188. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/__init__.py +0 -0
  189. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/_converters.py +0 -0
  190. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/account.py +0 -0
  191. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/device.py +0 -0
  192. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/diagnostics.py +0 -0
  193. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/energy.py +0 -0
  194. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/feature.py +0 -0
  195. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/firmware.py +0 -0
  196. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/maintenance.py +0 -0
  197. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/mqtt_models.py +0 -0
  198. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/schedule.py +0 -0
  199. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/status.py +0 -0
  200. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/models/tou.py +0 -0
  201. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/__init__.py +0 -0
  202. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/_control_commands.py +0 -0
  203. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
  204. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/client.py +0 -0
  205. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/command_queue.py +0 -0
  206. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/connection.py +0 -0
  207. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/control.py +0 -0
  208. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/diagnostics.py +0 -0
  209. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/periodic.py +0 -0
  210. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/reconnection.py +0 -0
  211. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/state_tracker.py +0 -0
  212. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/subscriptions.py +0 -0
  213. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/types.py +0 -0
  214. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/utils.py +0 -0
  215. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/mqtt_events.py +0 -0
  216. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/openei.py +0 -0
  217. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/py.typed +0 -0
  218. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/reservations.py +0 -0
  219. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/__init__.py +0 -0
  220. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/api.py +0 -0
  221. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/attribution.py +0 -0
  222. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/book.py +0 -0
  223. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/circulation.py +0 -0
  224. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/controller.py +0 -0
  225. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/defaults.py +0 -0
  226. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/delivery.py +0 -0
  227. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/draw_forecast.py +0 -0
  228. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/engine.py +0 -0
  229. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/extensions.py +0 -0
  230. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/heat_pump_recovery.py +0 -0
  231. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/heating_trajectory.py +0 -0
  232. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/inputs.py +0 -0
  233. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/location_air.py +0 -0
  234. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/observed.py +0 -0
  235. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/__init__.py +0 -0
  236. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/compressor.py +0 -0
  237. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/draw.py +0 -0
  238. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/element.py +0 -0
  239. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/refrigerant.py +0 -0
  240. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/tank.py +0 -0
  241. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/profile.py +0 -0
  242. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/ready.py +0 -0
  243. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/record.py +0 -0
  244. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/result.py +0 -0
  245. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/schedules.py +0 -0
  246. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/tank_physics.py +0 -0
  247. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/tou_triggers.py +0 -0
  248. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/sim/units.py +0 -0
  249. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/status_history.py +0 -0
  250. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/temperature.py +0 -0
  251. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/topic_builder.py +0 -0
  252. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/tou_schedule.py +0 -0
  253. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/unit_system.py +0 -0
  254. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500/utils.py +0 -0
  255. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  256. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  257. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  258. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/requires.txt +0 -0
  259. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
  260. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/_sim_site.py +0 -0
  261. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/conftest.py +0 -0
  262. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/fixtures/sim/engine_golden.json +0 -0
  263. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/fixtures/sim/heater_params.json +0 -0
  264. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/fixtures/sim/heater_params_fitted.json +0 -0
  265. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_api_endpoints_201.py +0 -0
  266. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_api_helpers.py +0 -0
  267. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_app_queries.py +0 -0
  268. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_auth.py +0 -0
  269. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_auth_session_lifecycle.py +0 -0
  270. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_auth_session_refresh.py +0 -0
  271. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_ble_mqtt_adapter.py +0 -0
  272. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_ble_protocol.py +0 -0
  273. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_bug_fixes.py +0 -0
  274. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_canonical_schedule.py +0 -0
  275. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_cli_basic.py +0 -0
  276. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_cli_calibrate.py +0 -0
  277. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_cli_commands.py +0 -0
  278. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_cli_tristate_presentation.py +0 -0
  279. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_command_decorators.py +0 -0
  280. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_command_queue.py +0 -0
  281. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_device_capabilities.py +0 -0
  282. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_device_info_cache.py +0 -0
  283. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_device_rest_models.py +0 -0
  284. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_events.py +0 -0
  285. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_exceptions.py +0 -0
  286. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_model_converters.py +0 -0
  287. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_models.py +0 -0
  288. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_clean_session_resume.py +0 -0
  289. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_client_init.py +0 -0
  290. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_control_fail.py +0 -0
  291. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_events.py +0 -0
  292. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_hypothesis.py +0 -0
  293. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_last_will.py +0 -0
  294. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_reconnection.py +0 -0
  295. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_reconnection_storm.py +0 -0
  296. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_mqtt_reliability.py +0 -0
  297. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_multi_device.py +0 -0
  298. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_navilink_201_enums.py +0 -0
  299. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_openei.py +0 -0
  300. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_protocol_correctness.py +0 -0
  301. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_public_api.py +0 -0
  302. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_reservation_validation.py +0 -0
  303. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_reservations.py +0 -0
  304. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_adapters.py +0 -0
  305. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_api.py +0 -0
  306. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_book.py +0 -0
  307. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_bottom_loss.py +0 -0
  308. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_calibrate.py +0 -0
  309. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_calibration.py +0 -0
  310. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_change_history.py +0 -0
  311. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_circulation.py +0 -0
  312. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_codec.py +0 -0
  313. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_controller.py +0 -0
  314. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_defaults.py +0 -0
  315. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_draw_forecast.py +0 -0
  316. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_draw_loss.py +0 -0
  317. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_element.py +0 -0
  318. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_element_fit.py +0 -0
  319. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_forecast.py +0 -0
  320. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_golden.py +0 -0
  321. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_hot_water.py +0 -0
  322. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_import_contract.py +0 -0
  323. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_location_air.py +0 -0
  324. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_no_clock_no_state.py +0 -0
  325. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_observed.py +0 -0
  326. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_profile.py +0 -0
  327. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_readiness.py +0 -0
  328. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_readiness_fit.py +0 -0
  329. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_ready.py +0 -0
  330. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_ready_api.py +0 -0
  331. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_replay.py +0 -0
  332. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_schedules.py +0 -0
  333. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_sim_tank_physics.py +0 -0
  334. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_status_history.py +0 -0
  335. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_temperature_converters.py +0 -0
  336. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_threading_model.py +0 -0
  337. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_token_storage.py +0 -0
  338. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_tou_api.py +0 -0
  339. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_tou_schedule.py +0 -0
  340. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_unit_switching.py +0 -0
  341. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_unit_system_process_wide.py +0 -0
  342. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_utility_modules.py +0 -0
  343. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tests/test_utils.py +0 -0
  344. {nwp500_python-9.9.0 → nwp500_python-9.10.0}/tox.ini +0 -0
  345. {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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.9.0
3
+ Version: 9.10.0
4
4
  Summary: A library for controlling Navien NWP500 Water Heaters via NaviLink
5
5
  Home-page: https://github.com/eman/nwp500-python
6
6
  Author: Emmanuel Levijarvi
@@ -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 meter's draws and the circulation's runs, which make the quiet
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
- meter's or the circulation's history doesn't cover the whole record,
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
- meter's draws and the circulation's runs. Any client with the same
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, a meter's draws and the circulation's runs to it.
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
- meter_volume = "sensor.water_volume" # volume since the last report
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
- meter. Name the meter's flow rate as well as its volume. A
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, flow=meter_flow,
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, max_gap,
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, GAP_SAMPLES, MIN_GAP,
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, draw_spans, busy, covers_start, reports_after, percent,
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 meter and "
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 = 24.0
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
- ZoneInfo(name)
73
- except ZoneInfoNotFoundError, ValueError:
74
- raise InputError(f"unknown time zone {name!r}") from None
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'. ``now`` is the start
187
- when nothing gives one."""
188
- data: dict[str, Any] = {}
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
- data["start"] = inputs.start
195
- data.setdefault("start", now)
179
+ replace["start"] = inputs.start
196
180
  if inputs.hours is not None:
197
- data["horizon"] = timedelta(hours=inputs.hours)
198
- data.setdefault("horizon", timedelta(hours=DEFAULT_HOURS))
181
+ replace["horizon"] = timedelta(hours=inputs.hours)
199
182
  if inputs.tz is not None:
200
- data["tz"] = inputs.tz
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
- data["environment"] = dict(inputs.environment)
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
- data["loads"] = dict(inputs.loads)
187
+ replace["loads"] = dict(inputs.loads)
220
188
  if inputs.commands is not None:
221
- data["commands"] = dict(inputs.commands)
222
- try:
223
- scenario = sim.Scenario.model_validate(data)
224
- except ValueError as e:
225
- raise InputError(f"the inputs do not make a scenario: {e}") from None
226
-
227
- notes: list[str] = []
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
- elif doc_params is not None:
234
- params = read_params(doc_params, "the scenario's params")
235
- elif live is not None and live.params is not None:
236
- params = live.params
237
- else:
238
- params = sim.default_params()
239
- notes.append(
240
- "the tank size is unknown: using the 65 gal defaults "
241
- "(give --params)"
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
- return Built(scenario, params, tuple(notes))
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: