nwp500-python 9.8.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.8.0 → nwp500_python-9.10.0}/CHANGELOG.rst +147 -0
  2. {nwp500_python-9.8.0/src/nwp500_python.egg-info → nwp500_python-9.10.0}/PKG-INFO +1 -1
  3. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/explanation/simulator.rst +5 -1
  4. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/calibrate-simulator.rst +119 -12
  5. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/simulate-heater.rst +85 -5
  6. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/python_api/sim.rst +12 -9
  7. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/simulator-inputs.rst +18 -1
  8. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/cli/__main__.py +2 -2
  9. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/cli/simulate.py +46 -77
  10. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/api.py +1 -5
  11. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/calibration.py +457 -33
  12. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/codec.py +88 -2
  13. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/cold_supply.py +13 -52
  14. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/decay.py +32 -2
  15. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/params.py +21 -1
  16. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/compressor.py +12 -2
  17. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/tank.py +68 -0
  18. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/tank_physics.py +2 -0
  19. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim_adapters.py +412 -84
  20. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim_calibrate.py +615 -164
  21. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim_home_assistant.py +105 -168
  22. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim_live.py +149 -16
  23. {nwp500_python-9.8.0 → nwp500_python-9.10.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  24. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/SOURCES.txt +4 -2
  25. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/scm_file_list.json +4 -2
  26. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/scm_version.json +2 -2
  27. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_cli_simulate.py +3 -1
  28. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_book_history.py +185 -0
  29. nwp500_python-9.10.0/tests/test_sim_bottom_loss.py +665 -0
  30. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_calibrate.py +20 -12
  31. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_cold_supply.py +41 -64
  32. nwp500_python-9.10.0/tests/test_sim_documents.py +127 -0
  33. nwp500_python-9.10.0/tests/test_sim_fit_book.py +695 -0
  34. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_home_assistant.py +179 -121
  35. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_live.py +47 -0
  36. nwp500_python-9.10.0/tests/test_sim_overlay.py +150 -0
  37. nwp500_python-9.8.0/src/nwp500/sim/meter.py +0 -130
  38. nwp500_python-9.8.0/tests/test_sim_meter.py +0 -163
  39. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/.coveragerc +0 -0
  40. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  41. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/.github/copilot-instructions.md +0 -0
  42. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/.github/workflows/ci.yml +0 -0
  43. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/.github/workflows/release.yml +0 -0
  44. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/.gitignore +0 -0
  45. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/.pre-commit-config.yaml +0 -0
  46. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/.readthedocs.yml +0 -0
  47. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/AUTHORS.rst +0 -0
  48. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/CONTRIBUTING.rst +0 -0
  49. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/LICENSE.txt +0 -0
  50. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/Makefile +0 -0
  51. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/README.rst +0 -0
  52. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/RELEASE.md +0 -0
  53. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/Makefile +0 -0
  54. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/_static/.gitignore +0 -0
  55. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/conf.py +0 -0
  56. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/explanation/architecture.rst +0 -0
  57. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/explanation/draws.rst +0 -0
  58. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/explanation/heating-elements.rst +0 -0
  59. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/explanation/index.rst +0 -0
  60. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/explanation/recovery-speed.rst +0 -0
  61. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/explanation/standby-loss.rst +0 -0
  62. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/explanation/tank-energy.rst +0 -0
  63. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/explanation/tou-recovery-cap.rst +0 -0
  64. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/explanation/unknown-values.rst +0 -0
  65. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/explanation/what-starts-a-recovery.rst +0 -0
  66. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/authenticate.rst +0 -0
  67. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/auto-recovery.rst +0 -0
  68. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/bluetooth.rst +0 -0
  69. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/diagnose-mqtt.rst +0 -0
  70. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/home-assistant.rst +0 -0
  71. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/index.rst +0 -0
  72. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/maintenance.rst +0 -0
  73. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/manage-units.rst +0 -0
  74. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/monitor-status.rst +0 -0
  75. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/optimize-tou.rst +0 -0
  76. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/queue-commands.rst +0 -0
  77. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/schedule-operation.rst +0 -0
  78. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/how-to/track-energy.rst +0 -0
  79. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/index.rst +0 -0
  80. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/openapi.yaml +0 -0
  81. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/project/authors.rst +0 -0
  82. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/project/changelog.rst +0 -0
  83. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/project/contributing.rst +0 -0
  84. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/project/history.rst +0 -0
  85. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/project/license.rst +0 -0
  86. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/configuration.rst +0 -0
  87. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/enumerations.rst +0 -0
  88. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/index.rst +0 -0
  89. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/installation.rst +0 -0
  90. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/protocol/data_conversions.rst +0 -0
  91. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/protocol/device_features.rst +0 -0
  92. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/protocol/device_status.rst +0 -0
  93. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/protocol/error_codes.rst +0 -0
  94. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  95. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/protocol/quick_reference.rst +0 -0
  96. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/protocol/rest_api.rst +0 -0
  97. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/python_api/api_client.rst +0 -0
  98. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/python_api/auth_client.rst +0 -0
  99. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/python_api/cli.rst +0 -0
  100. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/python_api/events.rst +0 -0
  101. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/python_api/exceptions.rst +0 -0
  102. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/python_api/models.rst +0 -0
  103. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/reference/python_api/mqtt_client.rst +0 -0
  104. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/requirements.txt +0 -0
  105. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/docs/tutorials/getting-started.rst +0 -0
  106. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/.ruff.toml +0 -0
  107. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/README.md +0 -0
  108. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/air_filter_reset.py +0 -0
  109. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/anti_legionella.py +0 -0
  110. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/auto_recovery.py +0 -0
  111. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/combined_callbacks.py +0 -0
  112. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/demand_response.py +0 -0
  113. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/device_capabilities.py +0 -0
  114. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/device_status_debug.py +0 -0
  115. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/energy_analytics.py +0 -0
  116. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/error_code_demo.py +0 -0
  117. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/firmware_payload_capture.py +0 -0
  118. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/mqtt_diagnostics.py +0 -0
  119. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/power_control.py +0 -0
  120. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/recirculation_control.py +0 -0
  121. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/reconnection_demo.py +0 -0
  122. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/reservation_schedule.py +0 -0
  123. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/simple_auto_recovery.py +0 -0
  124. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/token_restoration.py +0 -0
  125. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/tou_openei.py +0 -0
  126. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/tou_schedule.py +0 -0
  127. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/advanced/water_reservation.py +0 -0
  128. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/beginner/01_authentication.py +0 -0
  129. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/beginner/02_list_devices.py +0 -0
  130. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/beginner/03_get_status.py +0 -0
  131. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/beginner/04_set_temperature.py +0 -0
  132. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
  133. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/intermediate/command_queue.py +0 -0
  134. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/intermediate/device_status_callback.py +0 -0
  135. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/intermediate/error_handling.py +0 -0
  136. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/intermediate/event_driven_control.py +0 -0
  137. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/intermediate/improved_auth.py +0 -0
  138. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
  139. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  140. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/intermediate/periodic_requests.py +0 -0
  141. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/intermediate/set_mode.py +0 -0
  142. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/intermediate/vacation_mode.py +0 -0
  143. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/mask.py +0 -0
  144. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/simulator/lower-setpoint.json +0 -0
  145. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/simulator/plan.json +0 -0
  146. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/simulator/scenario.json +0 -0
  147. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/testing/periodic_device_info.py +0 -0
  148. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/testing/simple_periodic_info.py +0 -0
  149. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/testing/test_api_client.py +0 -0
  150. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/testing/test_mqtt_connection.py +0 -0
  151. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/testing/test_mqtt_messaging.py +0 -0
  152. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/examples/testing/test_periodic_minimal.py +0 -0
  153. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/pyproject.toml +0 -0
  154. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/scripts/README.md +0 -0
  155. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/scripts/bump_version.py +0 -0
  156. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/scripts/diagnose_mqtt_connection.py +0 -0
  157. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/scripts/extract_changelog.py +0 -0
  158. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/scripts/format.py +0 -0
  159. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/scripts/lint.py +0 -0
  160. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/scripts/setup-dev.py +0 -0
  161. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/scripts/validate_version.py +0 -0
  162. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/setup.cfg +0 -0
  163. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/setup.py +0 -0
  164. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/__init__.py +0 -0
  165. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/_base.py +0 -0
  166. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/api_client.py +0 -0
  167. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/auth.py +0 -0
  168. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/ble/__init__.py +0 -0
  169. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/ble/__main__.py +0 -0
  170. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/ble/client.py +0 -0
  171. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/ble/mqtt_adapter.py +0 -0
  172. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/ble/protocol.py +0 -0
  173. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/cli/__init__.py +0 -0
  174. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/cli/ble_backend.py +0 -0
  175. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/cli/calibration.py +0 -0
  176. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/cli/handlers.py +0 -0
  177. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/cli/monitoring.py +0 -0
  178. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/cli/output_formatters.py +0 -0
  179. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/cli/presentation.py +0 -0
  180. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/cli/recording.py +0 -0
  181. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/cli/rich_output.py +0 -0
  182. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/cli/token_storage.py +0 -0
  183. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/command_decorators.py +0 -0
  184. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/config.py +0 -0
  185. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/converters.py +0 -0
  186. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/device_capabilities.py +0 -0
  187. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/device_info_cache.py +0 -0
  188. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/encoding.py +0 -0
  189. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/enums.py +0 -0
  190. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/events.py +0 -0
  191. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/exceptions.py +0 -0
  192. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/factory.py +0 -0
  193. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/field_factory.py +0 -0
  194. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/models/__init__.py +0 -0
  195. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/models/_converters.py +0 -0
  196. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/models/account.py +0 -0
  197. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/models/device.py +0 -0
  198. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/models/diagnostics.py +0 -0
  199. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/models/energy.py +0 -0
  200. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/models/feature.py +0 -0
  201. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/models/firmware.py +0 -0
  202. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/models/maintenance.py +0 -0
  203. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/models/mqtt_models.py +0 -0
  204. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/models/schedule.py +0 -0
  205. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/models/status.py +0 -0
  206. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/models/tou.py +0 -0
  207. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/__init__.py +0 -0
  208. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/_control_commands.py +0 -0
  209. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
  210. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/client.py +0 -0
  211. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/command_queue.py +0 -0
  212. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/connection.py +0 -0
  213. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/control.py +0 -0
  214. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/diagnostics.py +0 -0
  215. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/periodic.py +0 -0
  216. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/reconnection.py +0 -0
  217. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/state_tracker.py +0 -0
  218. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/subscriptions.py +0 -0
  219. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/types.py +0 -0
  220. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt/utils.py +0 -0
  221. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/mqtt_events.py +0 -0
  222. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/openei.py +0 -0
  223. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/py.typed +0 -0
  224. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/reservations.py +0 -0
  225. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/__init__.py +0 -0
  226. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/attribution.py +0 -0
  227. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/book.py +0 -0
  228. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/circulation.py +0 -0
  229. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/controller.py +0 -0
  230. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/defaults.py +0 -0
  231. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/delivery.py +0 -0
  232. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/draw_forecast.py +0 -0
  233. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/engine.py +0 -0
  234. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/extensions.py +0 -0
  235. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/heat_pump_recovery.py +0 -0
  236. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/heating_trajectory.py +0 -0
  237. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/inputs.py +0 -0
  238. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/location_air.py +0 -0
  239. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/observed.py +0 -0
  240. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/__init__.py +0 -0
  241. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/draw.py +0 -0
  242. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/element.py +0 -0
  243. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/physics/refrigerant.py +0 -0
  244. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/profile.py +0 -0
  245. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/ready.py +0 -0
  246. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/record.py +0 -0
  247. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/result.py +0 -0
  248. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/schedules.py +0 -0
  249. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/tou_triggers.py +0 -0
  250. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/sim/units.py +0 -0
  251. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/status_history.py +0 -0
  252. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/temperature.py +0 -0
  253. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/topic_builder.py +0 -0
  254. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/tou_schedule.py +0 -0
  255. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/unit_system.py +0 -0
  256. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500/utils.py +0 -0
  257. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  258. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  259. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  260. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/requires.txt +0 -0
  261. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
  262. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/_sim_site.py +0 -0
  263. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/conftest.py +0 -0
  264. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/fixtures/sim/engine_golden.json +0 -0
  265. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/fixtures/sim/heater_params.json +0 -0
  266. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/fixtures/sim/heater_params_fitted.json +0 -0
  267. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_api_endpoints_201.py +0 -0
  268. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_api_helpers.py +0 -0
  269. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_app_queries.py +0 -0
  270. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_auth.py +0 -0
  271. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_auth_session_lifecycle.py +0 -0
  272. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_auth_session_refresh.py +0 -0
  273. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_ble_mqtt_adapter.py +0 -0
  274. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_ble_protocol.py +0 -0
  275. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_bug_fixes.py +0 -0
  276. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_canonical_schedule.py +0 -0
  277. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_cli_basic.py +0 -0
  278. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_cli_calibrate.py +0 -0
  279. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_cli_commands.py +0 -0
  280. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_cli_tristate_presentation.py +0 -0
  281. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_command_decorators.py +0 -0
  282. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_command_queue.py +0 -0
  283. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_device_capabilities.py +0 -0
  284. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_device_info_cache.py +0 -0
  285. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_device_rest_models.py +0 -0
  286. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_events.py +0 -0
  287. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_exceptions.py +0 -0
  288. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_model_converters.py +0 -0
  289. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_models.py +0 -0
  290. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_mqtt_clean_session_resume.py +0 -0
  291. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_mqtt_client_init.py +0 -0
  292. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_mqtt_control_fail.py +0 -0
  293. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_mqtt_events.py +0 -0
  294. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_mqtt_hypothesis.py +0 -0
  295. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_mqtt_last_will.py +0 -0
  296. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_mqtt_reconnection.py +0 -0
  297. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_mqtt_reconnection_storm.py +0 -0
  298. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_mqtt_reliability.py +0 -0
  299. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_multi_device.py +0 -0
  300. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_navilink_201_enums.py +0 -0
  301. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_openei.py +0 -0
  302. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_protocol_correctness.py +0 -0
  303. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_public_api.py +0 -0
  304. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_reservation_validation.py +0 -0
  305. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_reservations.py +0 -0
  306. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_adapters.py +0 -0
  307. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_api.py +0 -0
  308. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_book.py +0 -0
  309. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_calibration.py +0 -0
  310. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_change_history.py +0 -0
  311. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_circulation.py +0 -0
  312. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_codec.py +0 -0
  313. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_controller.py +0 -0
  314. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_defaults.py +0 -0
  315. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_draw_forecast.py +0 -0
  316. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_draw_loss.py +0 -0
  317. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_element.py +0 -0
  318. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_element_fit.py +0 -0
  319. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_forecast.py +0 -0
  320. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_golden.py +0 -0
  321. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_hot_water.py +0 -0
  322. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_import_contract.py +0 -0
  323. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_location_air.py +0 -0
  324. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_no_clock_no_state.py +0 -0
  325. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_observed.py +0 -0
  326. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_profile.py +0 -0
  327. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_readiness.py +0 -0
  328. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_readiness_fit.py +0 -0
  329. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_ready.py +0 -0
  330. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_ready_api.py +0 -0
  331. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_replay.py +0 -0
  332. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_schedules.py +0 -0
  333. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_sim_tank_physics.py +0 -0
  334. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_status_history.py +0 -0
  335. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_temperature_converters.py +0 -0
  336. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_threading_model.py +0 -0
  337. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_token_storage.py +0 -0
  338. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_tou_api.py +0 -0
  339. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_tou_schedule.py +0 -0
  340. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_unit_switching.py +0 -0
  341. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_unit_system_process_wide.py +0 -0
  342. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_utility_modules.py +0 -0
  343. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tests/test_utils.py +0 -0
  344. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/tox.ini +0 -0
  345. {nwp500_python-9.8.0 → nwp500_python-9.10.0}/uv.lock +0 -0
@@ -2,6 +2,153 @@
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
+
106
+ Version 9.9.0 (2026-10-22)
107
+ ==========================
108
+
109
+ Added
110
+ -----
111
+ - **A bottom loss in the standby model** (#187). Over one installation's 407
112
+ quiet windows across ten months, a window that opens stratified loses
113
+ heat at the bottom: its gap widens, its lower probe falls about 40 %
114
+ faster than a mixed tank's, and its upper probe hardly moves. One UA
115
+ on the whole tank could not represent it, and the fitted UA was a
116
+ blend that cooled a mixed tank too fast, as after a cold season's full
117
+ recoveries. ``LossParams`` gains ``bottom_loss_btu_hr`` and
118
+ ``bottom_gate_f``: heat leaving the lower zone, of which the lower
119
+ probe sees ``1 - exp(-gap / gate)`` (``sim.physics.tank.
120
+ bottom_loss_reach``). The standby walk (``decay.project_decay``) and
121
+ the recovery integrator charge it to the lower zone. A bottom loss of
122
+ 0.0, the default and what stored parameter sets carry, reproduces the
123
+ model before it.
124
+ - ``sim.calibration.fit_loss`` fits the UA law and the bottom loss
125
+ together, through the standby walk, on the quiet windows opening with
126
+ a gap of 10 °F or less (``loss_windows``); ``fit_standby`` uses it.
127
+ On the installation above, the lower probe's median error over ten
128
+ hours fell from 1.25 to 0.77 °F from March to October and from 1.67
129
+ to 1.05 °F over the winter (1.15 °F trained without the winter).
130
+ ``StandbyWindow`` carries each window's probes and air, and
131
+ ``standby_ends`` and ``standby_error`` walk a loss over windows.
132
+ - The report has the bottom loss and its gate, and ``keep_better``
133
+ judges them with the standby loss, on the probes' error in each
134
+ held-out window's fall, °F/h.
135
+
136
+ Changed
137
+ -------
138
+ - The standby loss is fitted only on quiet windows opening with a gap of
139
+ 10 °F or less: a wider gap is a draw's front closing, which the gap's
140
+ time constant describes.
141
+ - ``keep_better`` scores the standby group on the probes' error in each
142
+ held-out window's fall, walked as the simulation walks it, in place of
143
+ the error in log UA, so a loss with a bottom term and one without are
144
+ scored alike.
145
+ - The zone gap time constant is fitted with the bottom loss opening the
146
+ gaps it relaxes (``fit_gap_tau``'s ``loss``, ``gap_paths``), in turn
147
+ with the loss until it settles: a gap the bottom loss widens read as a
148
+ pure relaxation pushed the constant long. ``keep_better`` judges the
149
+ constant before the standby loss and fits and scores the loss on the
150
+ constant kept.
151
+
5
152
  Version 9.8.0 (2026-10-21)
6
153
  ==========================
7
154
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.8.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
@@ -78,7 +78,11 @@ zone (about half the tank's mass). Once the gap between the zones
78
78
  closes, the tank heats as one body. In standby the gap relaxes on a
79
79
  fitted time constant. Standby loss follows a jacket law,
80
80
  ``UA(dT) = ua_ref * (max(dT, 5) / 50) ^ n``, with the ambient as the
81
- other side.
81
+ other side, on the whole tank. A bottom loss ``Q_b`` leaves the lower
82
+ zone alone: the lower probe sees the share ``1 - exp(-gap / gate)`` of
83
+ it, so a stratified tank's lower zone falls faster and its gap opens,
84
+ while a mixed tank hides it below the probe. With ``Q_b = 0`` the loss
85
+ is the jacket law alone.
82
86
 
83
87
  **The elements.** They follow the device's own rules
84
88
  (:doc:`heating-elements`): per-mode entry thresholds, a dead time,
@@ -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
 
@@ -139,16 +140,23 @@ fitted or kept the base's value, and why:
139
140
 
140
141
  * - Term
141
142
  - Fitted from
142
- * - Standby loss and its exponent
143
+ * - Standby loss, its exponent, the bottom loss and its gate
143
144
  - Quiet windows of 3 h or more, starting 45 minutes after any
144
- heating or hot water in use. A window counts only if its fit
145
- tracks the probes within 2 °F. The exponent is fitted only when
146
- the windows span two 20 °F bands of tank-to-air difference; it
147
- is held at 0 (a constant loss) otherwise.
145
+ heating or hot water in use, that open with a gap of 10 °F or
146
+ less between the probes. A window counts only if its fit tracks
147
+ the probes within 2 °F. Each window's upper and lower probe are
148
+ walked from its start as the simulation walks them, and the
149
+ terms are fitted to where the probes ended. The exponent is
150
+ fitted only when the windows span two 20 °F bands of tank-to-air
151
+ difference; it is held at 0 (a constant loss) otherwise. The
152
+ bottom loss needs 10 windows that open with a gap of 1 °F or
153
+ more; with fewer it is 0, and the loss is the jacket law
154
+ alone.
148
155
  * - Zone gap time constant
149
156
  - How fast the upper and lower probes' gap closes over quiet
150
157
  windows; it needs 20 windows that open with a gap of 2 °F or
151
- more.
158
+ more. The bottom loss opens the gaps it relaxes, so the two are
159
+ fitted in turn until the constant settles.
152
160
  * - Compressor constant, evaporator approach
153
161
  - Heat-pump recoveries that reached their target, against the loss
154
162
  just fitted. The error is the mean absolute error of the
@@ -214,7 +222,7 @@ Assistant integration, can calibrate the same way:
214
222
  2. Read the air beside the heater and its humidity over the same span
215
223
  with ``sim_home_assistant.record_inputs(client, entities, start,
216
224
  end)``, and pass the result as ``record=``. It also brings the
217
- meter's draws and the circulation's runs. Any client with the same
225
+ household's draws and the circulation's runs. Any client with the same
218
226
  ``read_history`` as ``HomeAssistantClient`` works. The recorder's
219
227
  retention limits the air, as above.
220
228
  3. Call ``sim_calibrate.calibrate`` in a worker thread. It takes the
@@ -240,8 +248,12 @@ fit as ``params`` with ``keep_better=True``:
240
248
 
241
249
  Each group of terms holds out its latest evidence from its refit:
242
250
 
243
- - the standby loss and its exponent: its quiet windows;
244
- - the zone gap time constant: its windows that open a gap;
251
+ - the standby loss, its exponent, the bottom loss and its gate: its
252
+ quiet windows, scored on the probes' error in each window's fall,
253
+ °F/h;
254
+ - the zone gap time constant: its windows that open a gap, judged
255
+ before the standby loss, which is then fitted and scored on the
256
+ constant kept;
245
257
  - the compressor constant and the evaporator approach: its reached
246
258
  recoveries;
247
259
  - the power scale: the metered compressor cycles it can use;
@@ -263,7 +275,7 @@ without ``keep_better``.
263
275
 
264
276
  ``sim.calibration.calibrate`` underneath takes the frame
265
277
  ``sim_adapters.history`` builds; ``sim_adapters.add_inputs`` adds the
266
- 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.
267
279
  From there you can also give the draws that price the heat each
268
280
  cycle's draws took. The fits for the readiness terms, the heat book,
269
281
  the draw response and the elements also run there: see "Improve
@@ -356,6 +368,101 @@ On a desktop computer, 60 days with a heartbeat every 10 seconds
356
368
  Assistant's own queries, and 11 to fit, in about 180 MB.
357
369
 
358
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
+
359
466
  Limitations
360
467
  ===========
361
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,
@@ -169,6 +170,8 @@ Hot water and the heat book
169
170
  metered_withdrawn, metered_lead,
170
171
  path_to_end, with_power, snapshots, recoveries, quiet_windows,
171
172
  busy_intervals, fit_window, fit_ua_law, fit_gap_tau, with_standby,
173
+ fit_loss, LossFit, loss_windows, standby_ends, standby_error,
174
+ walk_tau, gap_paths, gap_error,
172
175
  with_compressor, UA_SEARCH_BOUNDS, UA_EXPONENT_BOUNDS, DRAW_LEAD
173
176
 
174
177
  .. automodule:: nwp500.sim.profile
@@ -182,9 +185,6 @@ Hot water and the heat book
182
185
  .. automodule:: nwp500.sim.cold_supply
183
186
  :members: plateau_t_cold, ColdSupply, ColdSupplyParams, on_the_mains
184
187
 
185
- .. automodule:: nwp500.sim.meter
186
- :members: flow_gpm
187
-
188
188
  .. automodule:: nwp500.sim.record
189
189
  :members: Record, RecordedWrite, Recorded, RecordedStarts, corroborator
190
190
 
@@ -250,7 +250,7 @@ The pieces the entry points are built from.
250
250
  vapor_enthalpy, dew_point_f
251
251
 
252
252
  .. automodule:: nwp500.sim.physics.tank
253
- :members: ua_at
253
+ :members: ua_at, bottom_loss_reach, bottom_loss_drop_f
254
254
 
255
255
  .. automodule:: nwp500.sim.physics.element
256
256
  :members: mode_recovery, ModeRecovery, mode_plan, ModePlan,
@@ -289,7 +289,9 @@ Live sources
289
289
  :members: compose, Composed, Source, Given, HeaterReading,
290
290
  LiveHeaterSource, LiveScheduleSource, LiveReadError, read_status,
291
291
  read_feature, read_reservations, HeaterSource, ScheduleSource,
292
- EnvironmentSource, LoadSource, DEFAULT_TIMEOUT
292
+ EnvironmentSource, LoadSource, DEFAULT_TIMEOUT, overlay, Built,
293
+ DEFAULT_HORIZON, DEFAULT_PARAMS_NOTE, zone_name, heater_reading,
294
+ live_schedules
293
295
 
294
296
 
295
297
  Recorded history
@@ -299,9 +301,10 @@ Recorded history
299
301
  :members:
300
302
 
301
303
  .. automodule:: nwp500.sim_calibrate
302
- :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,
303
306
  score, Judged, HeldOut, GROUPS, air_unread_note,
304
- CalibrationInputError, DEFAULT_HUMIDITY_PCT, GAP_SAMPLES, MIN_GAP,
307
+ CalibrationInputError, DEFAULT_HUMIDITY_PCT,
305
308
  DEFAULT_BASE_NOTE, NO_AIR_NOTE
306
309
 
307
310
 
@@ -310,9 +313,9 @@ Home Assistant sources
310
313
 
311
314
  .. automodule:: nwp500.sim_home_assistant
312
315
  :members: HomeAssistantEnvironment, HomeAssistantClient, record_inputs,
313
- RecordInputs, Sources, Entities, sources_from, State, parse_state,
316
+ RecordInputs, INLET_LOOKBACK, Sources, Entities, sources_from, State, parse_state,
314
317
  parse_history, parse_outages, parse_states, History, history_for,
315
318
  parse_forecast, read_heater_history, HeaterEntities, HeaterHistory,
316
319
  HISTORY_WINDOW, to_w, to_wh,
317
- pump_runs, draw_spans, busy, covers_start, reports_after, percent,
320
+ pump_runs, demand_spans, busy, covers_start, percent,
318
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(