nwp500-python 9.11.0__tar.gz → 9.12.1__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.11.0 → nwp500_python-9.12.1}/CHANGELOG.rst +194 -0
  2. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/CONTRIBUTING.rst +1 -1
  3. {nwp500_python-9.11.0/src/nwp500_python.egg-info → nwp500_python-9.12.1}/PKG-INFO +6 -17
  4. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/conf.py +1 -2
  5. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/calibrate-simulator.rst +45 -23
  6. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/simulate-heater.rst +173 -18
  7. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/python_api/sim.rst +6 -3
  8. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/simulator-inputs.rst +44 -17
  9. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/requirements.txt +1 -1
  10. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/pyproject.toml +72 -2
  11. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/scripts/README.md +1 -1
  12. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/scripts/validate_version.py +21 -13
  13. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/setup.cfg +0 -65
  14. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/cli/__main__.py +16 -3
  15. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/cli/calibration.py +2 -0
  16. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/cli/simulate.py +122 -7
  17. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/api.py +50 -13
  18. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/book.py +38 -5
  19. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/calibration.py +140 -33
  20. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/circulation.py +96 -7
  21. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/defaults.py +26 -9
  22. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/delivery.py +30 -12
  23. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/inputs.py +3 -0
  24. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/params.py +17 -3
  25. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/ready.py +18 -7
  26. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim_adapters.py +443 -51
  27. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim_calibrate.py +250 -51
  28. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim_home_assistant.py +969 -203
  29. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim_live.py +30 -2
  30. {nwp500_python-9.11.0 → nwp500_python-9.12.1/src/nwp500_python.egg-info}/PKG-INFO +6 -17
  31. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500_python.egg-info/SOURCES.txt +0 -2
  32. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500_python.egg-info/requires.txt +2 -11
  33. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500_python.egg-info/scm_file_list.json +1 -3
  34. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500_python.egg-info/scm_version.json +2 -2
  35. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_cli_simulate.py +136 -1
  36. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_adapters.py +8 -3
  37. nwp500_python-9.12.1/tests/test_sim_book_history.py +954 -0
  38. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_bottom_loss.py +11 -0
  39. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_calibrate.py +136 -0
  40. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_calibration.py +9 -0
  41. nwp500_python-9.12.1/tests/test_sim_change_history.py +1373 -0
  42. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_circulation.py +122 -6
  43. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_defaults.py +16 -4
  44. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_fit_book.py +139 -72
  45. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_home_assistant.py +440 -10
  46. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_hot_water.py +10 -2
  47. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_live.py +23 -0
  48. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_overlay.py +51 -0
  49. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_ready_api.py +165 -0
  50. nwp500_python-9.11.0/setup.py +0 -22
  51. nwp500_python-9.11.0/tests/test_sim_book_history.py +0 -435
  52. nwp500_python-9.11.0/tests/test_sim_change_history.py +0 -572
  53. nwp500_python-9.11.0/uv.lock +0 -3
  54. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/.coveragerc +0 -0
  55. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  56. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/.github/copilot-instructions.md +0 -0
  57. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/.github/workflows/ci.yml +0 -0
  58. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/.github/workflows/release.yml +0 -0
  59. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/.gitignore +0 -0
  60. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/.pre-commit-config.yaml +0 -0
  61. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/.readthedocs.yml +0 -0
  62. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/AUTHORS.rst +0 -0
  63. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/LICENSE.txt +0 -0
  64. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/Makefile +0 -0
  65. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/README.rst +0 -0
  66. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/RELEASE.md +0 -0
  67. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/Makefile +0 -0
  68. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/_static/.gitignore +0 -0
  69. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/explanation/architecture.rst +0 -0
  70. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/explanation/draws.rst +0 -0
  71. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/explanation/heating-elements.rst +0 -0
  72. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/explanation/index.rst +0 -0
  73. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/explanation/recovery-speed.rst +0 -0
  74. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/explanation/simulator.rst +0 -0
  75. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/explanation/standby-loss.rst +0 -0
  76. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/explanation/tank-energy.rst +0 -0
  77. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/explanation/tou-recovery-cap.rst +0 -0
  78. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/explanation/unknown-values.rst +0 -0
  79. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/explanation/what-starts-a-recovery.rst +0 -0
  80. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/authenticate.rst +0 -0
  81. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/auto-recovery.rst +0 -0
  82. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/bluetooth.rst +0 -0
  83. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/diagnose-mqtt.rst +0 -0
  84. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/home-assistant.rst +0 -0
  85. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/index.rst +0 -0
  86. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/maintenance.rst +0 -0
  87. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/manage-units.rst +0 -0
  88. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/monitor-status.rst +0 -0
  89. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/optimize-tou.rst +0 -0
  90. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/queue-commands.rst +0 -0
  91. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/schedule-operation.rst +0 -0
  92. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/how-to/track-energy.rst +0 -0
  93. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/index.rst +0 -0
  94. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/openapi.yaml +0 -0
  95. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/project/authors.rst +0 -0
  96. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/project/changelog.rst +0 -0
  97. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/project/contributing.rst +0 -0
  98. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/project/history.rst +0 -0
  99. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/project/license.rst +0 -0
  100. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/configuration.rst +0 -0
  101. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/enumerations.rst +0 -0
  102. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/index.rst +0 -0
  103. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/installation.rst +0 -0
  104. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/protocol/data_conversions.rst +0 -0
  105. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/protocol/device_features.rst +0 -0
  106. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/protocol/device_status.rst +0 -0
  107. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/protocol/error_codes.rst +0 -0
  108. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  109. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/protocol/quick_reference.rst +0 -0
  110. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/protocol/rest_api.rst +0 -0
  111. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/python_api/api_client.rst +0 -0
  112. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/python_api/auth_client.rst +0 -0
  113. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/python_api/cli.rst +0 -0
  114. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/python_api/events.rst +0 -0
  115. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/python_api/exceptions.rst +0 -0
  116. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/python_api/models.rst +0 -0
  117. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/reference/python_api/mqtt_client.rst +0 -0
  118. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/docs/tutorials/getting-started.rst +0 -0
  119. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/.ruff.toml +0 -0
  120. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/README.md +0 -0
  121. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/air_filter_reset.py +0 -0
  122. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/anti_legionella.py +0 -0
  123. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/auto_recovery.py +0 -0
  124. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/combined_callbacks.py +0 -0
  125. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/demand_response.py +0 -0
  126. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/device_capabilities.py +0 -0
  127. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/device_status_debug.py +0 -0
  128. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/energy_analytics.py +0 -0
  129. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/error_code_demo.py +0 -0
  130. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/firmware_payload_capture.py +0 -0
  131. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/mqtt_diagnostics.py +0 -0
  132. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/power_control.py +0 -0
  133. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/recirculation_control.py +0 -0
  134. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/reconnection_demo.py +0 -0
  135. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/reservation_schedule.py +0 -0
  136. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/simple_auto_recovery.py +0 -0
  137. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/token_restoration.py +0 -0
  138. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/tou_openei.py +0 -0
  139. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/tou_schedule.py +0 -0
  140. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/advanced/water_reservation.py +0 -0
  141. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/beginner/01_authentication.py +0 -0
  142. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/beginner/02_list_devices.py +0 -0
  143. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/beginner/03_get_status.py +0 -0
  144. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/beginner/04_set_temperature.py +0 -0
  145. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/intermediate/advanced_auth_patterns.py +0 -0
  146. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/intermediate/command_queue.py +0 -0
  147. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/intermediate/device_status_callback.py +0 -0
  148. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/intermediate/error_handling.py +0 -0
  149. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/intermediate/event_driven_control.py +0 -0
  150. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/intermediate/improved_auth.py +0 -0
  151. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/intermediate/legacy_auth_constructor.py +0 -0
  152. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  153. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/intermediate/periodic_requests.py +0 -0
  154. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/intermediate/set_mode.py +0 -0
  155. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/intermediate/vacation_mode.py +0 -0
  156. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/mask.py +0 -0
  157. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/simulator/lower-setpoint.json +0 -0
  158. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/simulator/plan.json +0 -0
  159. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/simulator/scenario.json +0 -0
  160. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/testing/periodic_device_info.py +0 -0
  161. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/testing/simple_periodic_info.py +0 -0
  162. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/testing/test_api_client.py +0 -0
  163. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/testing/test_mqtt_connection.py +0 -0
  164. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/testing/test_mqtt_messaging.py +0 -0
  165. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/examples/testing/test_periodic_minimal.py +0 -0
  166. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/scripts/bump_version.py +0 -0
  167. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/scripts/diagnose_mqtt_connection.py +0 -0
  168. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/scripts/extract_changelog.py +0 -0
  169. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/scripts/format.py +0 -0
  170. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/scripts/lint.py +0 -0
  171. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/scripts/setup-dev.py +0 -0
  172. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/__init__.py +0 -0
  173. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/_base.py +0 -0
  174. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/api_client.py +0 -0
  175. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/auth.py +0 -0
  176. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/ble/__init__.py +0 -0
  177. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/ble/__main__.py +0 -0
  178. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/ble/client.py +0 -0
  179. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/ble/mqtt_adapter.py +0 -0
  180. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/ble/protocol.py +0 -0
  181. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/cli/__init__.py +0 -0
  182. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/cli/ble_backend.py +0 -0
  183. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/cli/handlers.py +0 -0
  184. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/cli/monitoring.py +0 -0
  185. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/cli/output_formatters.py +0 -0
  186. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/cli/presentation.py +0 -0
  187. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/cli/recording.py +0 -0
  188. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/cli/rich_output.py +0 -0
  189. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/cli/token_storage.py +0 -0
  190. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/command_decorators.py +0 -0
  191. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/config.py +0 -0
  192. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/converters.py +0 -0
  193. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/device_capabilities.py +0 -0
  194. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/device_info_cache.py +0 -0
  195. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/encoding.py +0 -0
  196. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/enums.py +0 -0
  197. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/events.py +0 -0
  198. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/exceptions.py +0 -0
  199. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/factory.py +0 -0
  200. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/field_factory.py +0 -0
  201. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/models/__init__.py +0 -0
  202. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/models/_converters.py +0 -0
  203. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/models/account.py +0 -0
  204. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/models/device.py +0 -0
  205. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/models/diagnostics.py +0 -0
  206. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/models/energy.py +0 -0
  207. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/models/feature.py +0 -0
  208. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/models/firmware.py +0 -0
  209. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/models/maintenance.py +0 -0
  210. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/models/mqtt_models.py +0 -0
  211. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/models/schedule.py +0 -0
  212. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/models/status.py +0 -0
  213. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/models/tou.py +0 -0
  214. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/__init__.py +0 -0
  215. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/_control_commands.py +0 -0
  216. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
  217. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/client.py +0 -0
  218. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/command_queue.py +0 -0
  219. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/connection.py +0 -0
  220. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/control.py +0 -0
  221. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/diagnostics.py +0 -0
  222. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/periodic.py +0 -0
  223. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/reconnection.py +0 -0
  224. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/state_tracker.py +0 -0
  225. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/subscriptions.py +0 -0
  226. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/types.py +0 -0
  227. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt/utils.py +0 -0
  228. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/mqtt_events.py +0 -0
  229. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/openei.py +0 -0
  230. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/py.typed +0 -0
  231. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/reservations.py +0 -0
  232. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/__init__.py +0 -0
  233. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/attribution.py +0 -0
  234. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/codec.py +0 -0
  235. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/cold_supply.py +0 -0
  236. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/controller.py +0 -0
  237. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/decay.py +0 -0
  238. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/draw_forecast.py +0 -0
  239. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/engine.py +0 -0
  240. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/extensions.py +0 -0
  241. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/heat_pump_recovery.py +0 -0
  242. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/heating_trajectory.py +0 -0
  243. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/location_air.py +0 -0
  244. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/observed.py +0 -0
  245. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/physics/__init__.py +0 -0
  246. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/physics/compressor.py +0 -0
  247. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/physics/draw.py +0 -0
  248. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/physics/element.py +0 -0
  249. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/physics/refrigerant.py +0 -0
  250. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/physics/tank.py +0 -0
  251. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/profile.py +0 -0
  252. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/record.py +0 -0
  253. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/result.py +0 -0
  254. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/schedules.py +0 -0
  255. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/tank_physics.py +0 -0
  256. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/tou_triggers.py +0 -0
  257. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/sim/units.py +0 -0
  258. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/status_history.py +0 -0
  259. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/temperature.py +0 -0
  260. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/topic_builder.py +0 -0
  261. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/tou_schedule.py +0 -0
  262. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/unit_system.py +0 -0
  263. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500/utils.py +0 -0
  264. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  265. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  266. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  267. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/src/nwp500_python.egg-info/top_level.txt +0 -0
  268. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/_sim_site.py +0 -0
  269. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/conftest.py +0 -0
  270. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/fixtures/sim/engine_golden.json +0 -0
  271. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/fixtures/sim/heater_params.json +0 -0
  272. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/fixtures/sim/heater_params_fitted.json +0 -0
  273. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_api_endpoints_201.py +0 -0
  274. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_api_helpers.py +0 -0
  275. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_app_queries.py +0 -0
  276. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_auth.py +0 -0
  277. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_auth_session_lifecycle.py +0 -0
  278. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_auth_session_refresh.py +0 -0
  279. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_ble_mqtt_adapter.py +0 -0
  280. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_ble_protocol.py +0 -0
  281. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_bug_fixes.py +0 -0
  282. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_canonical_schedule.py +0 -0
  283. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_cli_basic.py +0 -0
  284. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_cli_calibrate.py +0 -0
  285. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_cli_commands.py +0 -0
  286. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_cli_tristate_presentation.py +0 -0
  287. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_command_decorators.py +0 -0
  288. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_command_queue.py +0 -0
  289. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_device_capabilities.py +0 -0
  290. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_device_info_cache.py +0 -0
  291. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_device_rest_models.py +0 -0
  292. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_events.py +0 -0
  293. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_exceptions.py +0 -0
  294. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_model_converters.py +0 -0
  295. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_models.py +0 -0
  296. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_mqtt_clean_session_resume.py +0 -0
  297. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_mqtt_client_init.py +0 -0
  298. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_mqtt_control_fail.py +0 -0
  299. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_mqtt_events.py +0 -0
  300. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_mqtt_hypothesis.py +0 -0
  301. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_mqtt_last_will.py +0 -0
  302. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_mqtt_reconnection.py +0 -0
  303. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_mqtt_reconnection_storm.py +0 -0
  304. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_mqtt_reliability.py +0 -0
  305. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_multi_device.py +0 -0
  306. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_navilink_201_enums.py +0 -0
  307. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_openei.py +0 -0
  308. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_protocol_correctness.py +0 -0
  309. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_public_api.py +0 -0
  310. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_reservation_validation.py +0 -0
  311. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_reservations.py +0 -0
  312. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_api.py +0 -0
  313. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_book.py +0 -0
  314. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_codec.py +0 -0
  315. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_cold_supply.py +0 -0
  316. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_controller.py +0 -0
  317. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_documents.py +0 -0
  318. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_draw_forecast.py +0 -0
  319. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_draw_loss.py +0 -0
  320. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_element.py +0 -0
  321. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_element_fit.py +0 -0
  322. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_forecast.py +0 -0
  323. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_golden.py +0 -0
  324. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_import_contract.py +0 -0
  325. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_location_air.py +0 -0
  326. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_no_clock_no_state.py +0 -0
  327. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_observed.py +0 -0
  328. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_profile.py +0 -0
  329. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_readiness.py +0 -0
  330. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_readiness_fit.py +0 -0
  331. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_ready.py +0 -0
  332. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_replay.py +0 -0
  333. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_schedules.py +0 -0
  334. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_sim_tank_physics.py +0 -0
  335. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_status_history.py +0 -0
  336. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_temperature_converters.py +0 -0
  337. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_threading_model.py +0 -0
  338. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_token_storage.py +0 -0
  339. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_tou_api.py +0 -0
  340. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_tou_schedule.py +0 -0
  341. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_unit_switching.py +0 -0
  342. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_unit_system_process_wide.py +0 -0
  343. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_utility_modules.py +0 -0
  344. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tests/test_utils.py +0 -0
  345. {nwp500_python-9.11.0 → nwp500_python-9.12.1}/tox.ini +0 -0
@@ -2,6 +2,200 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version 9.12.1 (2026-10-23)
6
+ ===========================
7
+
8
+ Fixed
9
+ -----
10
+
11
+ - **simulate run books the tank from Home Assistant.** Given the
12
+ sources' ``[home_assistant.heater]`` table and the new
13
+ ``draw_flow`` entity, the run walks from the tank's booked heat
14
+ (``HomeAssistantBook``), and "deliverable now" reads it: a tank with
15
+ its probes apart read 0 gal from the probes alone, the history
16
+ unused.
17
+
18
+ Added
19
+ -----
20
+
21
+ - **simulate run's bath and showers.** The table gives the bath and
22
+ the whole showers the tank is ready for (``sim.time_to_ready``), and
23
+ when the heat pump has the bath ready; ``--bath`` and ``--shower``
24
+ give the volumes.
25
+
26
+ Changed
27
+ -------
28
+
29
+ - **The package's metadata is in pyproject.toml.** Name, dependencies,
30
+ extras, classifiers and the ``nwp-cli`` entry point moved from
31
+ ``setup.cfg`` to the standard ``[project]`` table, and ``setup.py`` is
32
+ gone. The built package installs the same files and requirements.
33
+
34
+ Version 9.12.0 (2026-10-23)
35
+ ===========================
36
+
37
+ Added
38
+ -----
39
+
40
+ - **The heater's own entities in the sources file** (#232).
41
+ ``[home_assistant.heater]`` names them (``heard``, ``upper`` and
42
+ ``lower`` required), so the CLI fits and anchors the projected air to
43
+ the heater's reading while its compressor runs where no air entity
44
+ does.
45
+ - **The idle cabinet air, said** (#232). ``Environment.ambient_cabinet``
46
+ marks air taken from the heater's own reading while the compressor
47
+ was idle, about 6 °F warm, and answers give the reason
48
+ ``ambient_from_idle_cabinet``.
49
+ - **The heater's own air in the book** (#236). With no air reading,
50
+ ``book_from_history``, ``book_from_changes`` and
51
+ ``HomeAssistantBook`` take the heater's own air reading once its
52
+ compressor has run 5 minutes (``sim_adapters.settled_air``,
53
+ ``HEATER_AIR_SETTLE``), held until the next run, rather than 65 °F
54
+ throughout. ``HomeAssistantBook`` notes it.
55
+ - **The standby loss from the heater's own air** (#236). With no air
56
+ given, calibration fits the standby loss on the heater's own reading
57
+ once its compressor has run 5 minutes, interpolated from one run to
58
+ the next, over the quiet windows with a run within
59
+ ``HEATER_AIR_BRACKET`` (6 h) of both ends: the law's level only, on
60
+ the base's exponent. ``Calibrated.air`` is ``"heater"``, and a note
61
+ says how many windows qualified and their share of the quiet hours.
62
+ It was left unfitted. ``fit_ua_law``, ``fit_loss``, ``fit_standby``
63
+ and ``calibration.calibrate`` take a fixed exponent.
64
+ - **The book prices a circulation run by a circuit volume.**
65
+ ``CirculationParams.book_circuit_gal``: with it, the heat book
66
+ charges a run its first gallons up to that volume, runs closer than
67
+ ``run_gap_min`` one run, each at the tank's upper probe less the air;
68
+ without it, ``loss_btu_per_gal`` per gallon as before. ``fit_book``
69
+ fits the volume in place of the loss per gallon (``book_circuit_gal``
70
+ on its result), and ``fit_book_terms`` with ``circuit=True``. At one
71
+ installation, over 284 re-anchors, the book's error on pairs with a
72
+ run fell 14 % against a refitted loss per gallon (1741 against 2023
73
+ BTU), and on held-out months from 3184 to 2454 BTU, the volume
74
+ steady at 1.1 to 1.5 gal where the loss per gallon moved from 59 to
75
+ 120. It is an effective volume, apart from ``displaced_gal``.
76
+ ``SITE_CIRCULATION`` carries one installation's, and the book's
77
+ heat-pump rate and slope are refitted with it (4338 BTU/h, 66.0
78
+ BTU/h/°F; the UA stays 2.88).
79
+
80
+ - **The heater's heard spans, given** (#230). ``book_from_changes``
81
+ takes ``heard=``, the spans the heater was heard over, for a record
82
+ that keeps no instant for each response. A gap between spans breaks
83
+ the book as a silence between instants does, and the heater is heard
84
+ at ``at`` only inside a span.
85
+ - **The air fit carried across reads** (#221).
86
+ ``sim_home_assistant.fit_location_air`` is the fit
87
+ ``HomeAssistantEnvironment`` makes where it is given no model, with
88
+ its notes, as a call of its own. ``HomeAssistantEnvironment`` takes
89
+ it back as ``fitted_air``, projects through it, and notes its r and
90
+ lag on every ``read`` without fitting again. A caller that reads
91
+ every few minutes fits on its own cadence.
92
+ - **The heat book kept current from Home Assistant** (#227).
93
+ ``sim_home_assistant.HomeAssistantBook`` holds the heater's and the
94
+ household flow's history, reading only what is new on each ``read``,
95
+ and books from it with no request (``book``, for an executor). The
96
+ flow is the household's draws as delivered through the mixing valve.
97
+ The circulation's speed, an inlet probe and the demand entity, each
98
+ optional, give the runs, the inlet and the conversion at the mains
99
+ (``BookAnswer.inlet_measured``). ``mains_plateau`` and ``known_runs``
100
+ are the inlet's plateau and the pump's runs from a ``History``, as
101
+ ``HomeAssistantEnvironment`` reads them. ``book_from_changes`` and
102
+ ``book_steps_from_changes`` take ``air_outages``: while the air is
103
+ unknown the book takes the default air, not the last reading.
104
+ - **The CPU work off the event loop** (#225). ``fit_location_air`` and
105
+ ``HomeAssistantEnvironment`` take ``run=`` (``Run``), which runs a
106
+ piece of work and awaits its result, such as Home Assistant's
107
+ ``hass.async_add_executor_job``. The fit, the samples of the heater's
108
+ air and the inlet's plateau go through it. Without it they run
109
+ inline (``inline``), as before.
110
+ - **A history kept current** (#223). ``sim_home_assistant.HeaterHistory``,
111
+ ``sim_home_assistant.History`` and ``sim_adapters.ChangeRows`` take
112
+ ``extended(newer)``, which merges a later read in by instant (the
113
+ later read's row winning at a shared instant) and joins the outages,
114
+ and ``since(start)``, which trims to ``start`` and keeps each value in
115
+ force there. A caller that books every few minutes reads only what is
116
+ new and books from what it holds. ``sim_adapters.merge_changes``,
117
+ ``changes_since``, ``join_spans`` and ``spans_since`` do it for one
118
+ series.
119
+
120
+ Changed
121
+ -------
122
+
123
+ - ``SITE_CIRCULATION.meter_share`` is 0. The book's flow is the
124
+ household's draws alone, which no circulation run passes, so the
125
+ example terms take nothing off it. A caller whose meter carries the
126
+ loop's water still sets its own share.
127
+
128
+ - **Readiness with no evidence and no book** (#234). A tank whose probes
129
+ are within the book's ``uniform_band_f`` reads its full-tank floor
130
+ from its probes (``Deliverable.basis`` ``probes``), at any
131
+ temperature. Any other tank reads 0 with the reason
132
+ ``no_evidence_for_state``, which says the gallons are unknown, not
133
+ none, in place of ``colder_than_all_evidence``.
134
+ - The 50 and 80 gal defaults scale the book's empty point
135
+ (``BookParams.empty_btu``) and the full tank's hidden cold water
136
+ (``ReadinessParams.full_hidden_gal``) by tank size, as they scale its
137
+ volume (#234). Both were the 65 gal tank's at every size.
138
+ - Circulation runs given with params that have no circulation terms are
139
+ priced at one installation's (``SITE_CIRCULATION``), and the answer
140
+ says so in ``defaults_used`` (#232). They raised in every walk, and a
141
+ book left them unpriced. ``sim_adapters.with_circulation_terms`` does
142
+ this for the books.
143
+ - A status in Standby is Power Off, as the NaviLink app counts it (#232).
144
+ It raised.
145
+ - ``book_from_changes`` books from the flow's first reading when its
146
+ history starts later than the window, rather than raising (#232).
147
+ - ``known_runs`` returns the pump's runs from its first reading and the
148
+ span before it as unknown, rather than nothing for a history shorter
149
+ than the span (#232). The inlet's plateau skips a draw in that span,
150
+ and ``HomeAssistantBook`` takes no anchor before it.
151
+ - ``read_heater_history`` leaves out a heating flag entity with no
152
+ history at all, with a note, rather than leaving the whole record
153
+ unobserved (#232).
154
+
155
+ - The simulate how-to's ``book_from_changes`` example passes the
156
+ circulation's runs and flow, and says to pass the household's draws,
157
+ separated from any circulation in the meter, with ``meter_share`` 0.
158
+ - With a model, given or fitted, and no fresh reading of the air beside
159
+ the heater, ``HomeAssistantEnvironment`` anchors the projection to
160
+ ``heater``'s newest air reading from the last hour while its
161
+ compressor ran, on every ``read``. It anchored to one only on the
162
+ read that fitted the model (#221).
163
+
164
+ Fixed
165
+ -----
166
+
167
+ - ``simulate`` and ``time_to_ready`` convert a book's heat to gallons at
168
+ the scenario's inlet (``Environment.inlet_f``) where one is given, as
169
+ the walk's draws take it (#234). The gallons ignored a measured inlet,
170
+ and with a book a colder inlet gave warmer delivered water.
171
+ - ``fit_location_air`` fits to the heater's own air when the air entity
172
+ can't be read or gives no fit (#232).
173
+ - Calibration's recoveries outside a partial air record read the
174
+ heater's own air, rather than being dropped (#232).
175
+ - The simulate how-to's and ``sim_live``'s ``compose`` example falls
176
+ back to the 65 gal defaults when the feature info doesn't arrive
177
+ (#232). It passed None to ``simulate``.
178
+ - ``HomeAssistantBook`` says when the flow is unknown now (#232).
179
+
180
+ - **A heater read missing a heating flag is unobserved** (#227).
181
+ ``read_heater_history`` reads a day at a time; a window with nothing
182
+ of the compressor, an element or the heat source (each opens with
183
+ the value in force) now leaves the heater unobserved over it, with a
184
+ note, rather than holding the flag from before.
185
+ - ``sim_adapters.book_from_changes`` read its flow as a sampled meter:
186
+ a reading held 60 seconds (``FLOW_HOLD``) and the inlet's an hour,
187
+ and flow rows more than an hour apart refused the book as a silent
188
+ meter. A history of changes writes a row only when a value changes,
189
+ so a steady draw counted for a minute and an hour at 0 gal/min was
190
+ refused. Its flows, inlet and air now hold until the next change. It
191
+ takes ``flow_outages``, a span with no anchor before it counting, as
192
+ for a heater unheard, and ``inlet_outages``. No flow reading in force
193
+ at the window's start is a ``ValueError`` (#219).
194
+ - ``sim_home_assistant.pump_runs`` ended a run 5 minutes after its last
195
+ speed reading. A pump at a steady speed writes no row, so a long run
196
+ read as 5 minutes. A speed now holds until it changes; ``stale``
197
+ (``PUMP_STALE``) keeps the old rule for a sampled speed (#219).
198
+
5
199
  Version 9.11.0 (2026-10-22)
6
200
  ===========================
7
201
 
@@ -265,7 +265,7 @@ package:
265
265
  potentially in the root of your project.
266
266
 
267
267
  #. Sometimes |tox|_ misses out when new dependencies are added, especially to
268
- ``setup.cfg`` and ``docs/requirements.txt``. If you find any problems with
268
+ ``pyproject.toml`` and ``docs/requirements.txt``. If you find any problems with
269
269
  missing dependencies when running a command with |tox|_, try to recreate the
270
270
  ``tox`` environment using the ``-r`` flag. For example, instead of::
271
271
 
@@ -1,15 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.11.0
3
+ Version: 9.12.1
4
4
  Summary: A library for controlling Navien NWP500 Water Heaters via NaviLink
5
- Home-page: https://github.com/eman/nwp500-python
6
- Author: Emmanuel Levijarvi
7
- Author-email: emansl@gmail.com
8
- License: MIT
5
+ Author-email: Emmanuel Levijarvi <emansl@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/eman/nwp500-python
9
8
  Project-URL: Documentation, https://nwp500-python.readthedocs.io/en/latest/
10
9
  Project-URL: Source, https://github.com/eman/nwp500-python
11
10
  Project-URL: Tracker, https://github.com/eman/nwp500-python/issues
12
- Platform: any
13
11
  Classifier: Development Status :: 4 - Beta
14
12
  Classifier: Programming Language :: Python
15
13
  Classifier: Programming Language :: Python :: 3
@@ -31,9 +29,7 @@ Requires-Dist: numpy>=1.24.0; extra == "sim"
31
29
  Requires-Dist: pandas>=2.0.0; extra == "sim"
32
30
  Requires-Dist: scipy>=1.11.0; extra == "sim"
33
31
  Provides-Extra: testing
34
- Requires-Dist: numpy>=1.24.0; extra == "testing"
35
- Requires-Dist: pandas>=2.0.0; extra == "testing"
36
- Requires-Dist: scipy>=1.11.0; extra == "testing"
32
+ Requires-Dist: nwp500-python[sim]; extra == "testing"
37
33
  Requires-Dist: setuptools; extra == "testing"
38
34
  Requires-Dist: pytest; extra == "testing"
39
35
  Requires-Dist: pytest-cov; extra == "testing"
@@ -42,14 +38,7 @@ Requires-Dist: hypothesis; extra == "testing"
42
38
  Provides-Extra: dev
43
39
  Requires-Dist: ruff>=0.1.0; extra == "dev"
44
40
  Requires-Dist: pyright>=1.1.0; extra == "dev"
45
- Requires-Dist: numpy>=1.24.0; extra == "dev"
46
- Requires-Dist: pandas>=2.0.0; extra == "dev"
47
- Requires-Dist: scipy>=1.11.0; extra == "dev"
48
- Requires-Dist: setuptools; extra == "dev"
49
- Requires-Dist: pytest; extra == "dev"
50
- Requires-Dist: pytest-cov; extra == "dev"
51
- Requires-Dist: pytest-asyncio; extra == "dev"
52
- Requires-Dist: hypothesis; extra == "dev"
41
+ Requires-Dist: nwp500-python[testing]; extra == "dev"
53
42
  Dynamic: license-file
54
43
 
55
44
  =============
@@ -24,8 +24,7 @@ sys.path.insert(0, os.path.join(__location__, "../src"))
24
24
  # This hack is necessary since RTD does not issue `sphinx-apidoc` before running
25
25
  # `sphinx-build -b html . _build/html`. See Issue:
26
26
  # https://github.com/readthedocs/readthedocs.org/issues/1139
27
- # DON'T FORGET: Check the box "Install your project inside a virtualenv using
28
- # setup.py install" in the RTD Advanced Settings.
27
+ # RTD installs the project itself (.readthedocs.yml, python.install).
29
28
  # Additionally it helps us to avoid running apidoc manually
30
29
 
31
30
  try: # for Sphinx >= 1.7
@@ -96,9 +96,23 @@ Idle, it reads the cabinet, about 6 °F warm. So give one of these:
96
96
  the heater's own flag is used instead. ``--air``, ``--ambient`` and
97
97
  ``--humidity`` override the sources.
98
98
 
99
- Without the air, the standby loss keeps the base's value and the report
100
- says so. When you give the air, the recoveries use it too; without it,
101
- they use the heater's own reading while the compressor runs.
99
+ Without the air, the command fits the standby loss on the heater's own
100
+ reading, taken once the compressor has run 5 minutes and drawn straight
101
+ from one run's reading to the next across the idle span between them.
102
+ Only quiet windows with a run within 6 hours of both ends are fitted:
103
+ they fall mostly overnight, and a reading held from the day's last run
104
+ would read the night warm. Those windows span too narrow a range of
105
+ tank-to-air difference to fit the loss law's shape, so only its level is
106
+ fitted, on the base's exponent. A note says how many windows qualified
107
+ and what share of the quiet hours they hold. Expect the loss to read
108
+ somewhat high: at one installation, against a sensor beside the heater,
109
+ it read about 12 % high, as the night air dips below the line between
110
+ an evening run and a morning one, and the running reading is itself a
111
+ little warm. A sensor beside the heater (``--air``) avoids both.
112
+ With no such reading at all, the standby loss keeps the base's value
113
+ and the report says so. When you give the air, the recoveries use it
114
+ too; without it, they use the heater's own reading while the compressor
115
+ runs.
102
116
 
103
117
  ``--humidity`` takes the same kind of file, in %RH, for the
104
118
  evaporator; without it the command gives the fits 50 %.
@@ -205,7 +219,8 @@ read in order without holding every status:
205
219
  print(term.name, term.value, "fitted" if term.fitted else term.why)
206
220
 
207
221
  Give ``ambient`` instead of ``air`` for a constant, or neither: the
208
- standby loss then keeps the base's value. Without ``humidity`` the fits
222
+ standby loss is then fitted on the heater's own reading between runs,
223
+ and ``done.air`` is ``"heater"``. Without ``humidity`` the fits
209
224
  get 50 %. An input it cannot use raises ``CalibrationInputError``.
210
225
 
211
226
  On a desktop computer, 60 days of statuses (about 540,000) took about
@@ -373,8 +388,11 @@ Fit the heat book
373
388
 
374
389
  The heat book (``book_from_changes``) counts the heat the tank holds
375
390
  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
391
+ UA, and, with a circulation pump, what each run takes: its first
392
+ gallons, up to the book's circuit volume
393
+ (``CirculationParams.book_circuit_gal``), each at the tank's upper
394
+ probe less the air, the loop having cooled to the air since the last
395
+ run. ``fit_book`` fits them
378
396
  together from the heater's history, the household's draws, and the
379
397
  pump's runs with the loop's flow. The draws and the loop's flow are
380
398
  yours to give, apart from each other: the fit takes every gallon of the
@@ -402,8 +420,8 @@ draws as hot water drawn, and each run at the loop's flow.
402
420
 
403
421
  ``draws`` and ``loop_flow`` are (instant, gal/min) readings and ``runs``
404
422
  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).
423
+ takes it. ``fit.book_circuit_gal`` is the circuit volume for your
424
+ ``CirculationParams`` (None without a loop).
407
425
 
408
426
  The fit reads the pairs of quiet full tanks up to
409
427
  ``book.max_anchor_age_h`` apart, and fits each term for the least mean
@@ -415,18 +433,22 @@ pair:
415
433
  the others (the slope, with the air never off one temperature) stays
416
434
  at ``params.book``. The rest of the book (its draw line, the anchors'
417
435
  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.
436
+ - **The book's circuit volume:** on 10 pairs with a run between, with
437
+ the book's terms fitted at each volume tried. Runs closer than
438
+ ``run_gap_min`` are one run. It is an effective volume, smaller than
439
+ the loop's own (``displaced_gal``): at one installation 1.4 gal
440
+ against 6.4. Fit and keep them apart. With fewer pairs, the loop is
441
+ priced as ``params.circulation`` prices it, and without one, the
442
+ pairs with a run are left out. A pair with a run that has no flow
443
+ reading is left out.
422
444
 
423
445
  The fit needs 10 pairs. It holds out the latest fifth and replaces the
424
446
  base only when it books them no worse. A term on its bound
425
447
  (``sim.calibration.BOOK_TERM_BOUNDS``) is no fit: the book misses
426
448
  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
449
+ circuit volume on its bound (``sim.calibration.BOOK_CIRCUIT_GAL_BOUNDS``)
450
+ leaves the loop at the base's; any other term on its bound keeps the
451
+ base. ``fit.kept`` says which it
430
452
  kept and ``fit.why`` why. A span the heater wasn't heard from, a probe
431
453
  read nothing, or the draws were silent over an hour, breaks the book.
432
454
 
@@ -435,10 +457,10 @@ come slowly. With a pump running about once a day, one installation had
435
457
  5 in 14 days and 19 in 42: fit on four weeks or more, and raise Home
436
458
  Assistant's ``purge_keep_days`` to keep them.
437
459
 
438
- Book with the same inputs. Runs are priced with the loop's loss, in a
460
+ Book with the same inputs. Runs are priced at the circuit volume, in a
439
461
  ``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
462
+ since the draws hold none of the loop's water; without one, they can't
463
+ be booked. ``sa.run_readings`` gives the book each run's flow as
442
464
  the fit takes it: its own readings, the first from its start, and none
443
465
  read outside the runs.
444
466
 
@@ -447,13 +469,13 @@ read outside the runs.
447
469
  from nwp500.sim.params import CirculationParams
448
470
 
449
471
  book = None
450
- loss = fit.loss_btu_per_gal
451
- if loss is not None or not runs:
472
+ volume = fit.book_circuit_gal
473
+ if volume is not None or not runs:
452
474
  loop = None
453
- if loss is not None:
475
+ if volume is not None:
454
476
  loop = CirculationParams(
455
- displaced_gal=..., flow_gpm=..., loss_btu_per_gal=loss,
456
- meter_share=0.0,
477
+ displaced_gal=..., flow_gpm=..., loss_btu_per_gal=...,
478
+ book_circuit_gal=volume, meter_share=0.0,
457
479
  )
458
480
  book = sa.book_from_changes(
459
481
  history.rows, history.observed, replace(params, circulation=loop),
@@ -141,6 +141,14 @@ pass it with ``--sources`` (or set ``NWP500_SOURCES``):
141
141
  demand = "binary_sensor.hot_water_demand" # on while hot water is drawn
142
142
  circulation_speed = "sensor.circulation_pump_speed"
143
143
  weather = "weather.forecast_home"
144
+ draw_flow = "sensor.hot_water_draw_flow" # the household's draws
145
+
146
+ [home_assistant.heater] # optional: the heater's own entities
147
+ heard = "sensor.water_heater_last_status"
148
+ upper = "sensor.water_heater_upper_tank_temperature"
149
+ lower = "sensor.water_heater_lower_tank_temperature"
150
+ compressor = "binary_sensor.water_heater_compressor"
151
+ ambient = "sensor.water_heater_ambient_temperature"
144
152
 
145
153
  [location_air] # optional: air = damping x outdoor(t - lag) + offset
146
154
  damping = 0.638
@@ -162,7 +170,11 @@ give:
162
170
  heater that is at most an hour old; without a fresh one, the
163
171
  projection is unshifted. Without a ``[location_air]`` model, one is
164
172
  fitted on the last 3 days of the ``air`` entity's readings against
165
- the ``weather`` entity's outdoor temperature. It is used when the
173
+ the ``weather`` entity's outdoor temperature. Without an ``air``
174
+ entity, or one that gives no fit, the ``[home_assistant.heater]``
175
+ table's ``ambient`` reading while its ``compressor`` runs is fitted
176
+ instead, and the projection is shifted to pass through its newest
177
+ such reading. It is used when the
166
178
  readings span a day and follow the outdoor air at r 0.5 or more; a
167
179
  note gives the fit. Otherwise, and without a ``weather`` entity, the
168
180
  reading beside the heater is held over the span;
@@ -171,7 +183,26 @@ give:
171
183
  circulation off) in the last 72 hours, from the inlet probe and the
172
184
  ``demand`` entity: a binary sensor that is on while the household
173
185
  draws hot water, with the circulation's water not among its draws. A
174
- circulation pump with no speed reading leaves no estimate.
186
+ circulation pump with no speed reading leaves no estimate, and a draw
187
+ before its first reading is skipped;
188
+ - **the heat book:** with the ``[home_assistant.heater]`` table and a
189
+ ``draw_flow`` entity (the household's hot water draw flow, the
190
+ circulation's water not in it), the tank's heat booked from the last
191
+ 72 hours of the heater's readings and the draws
192
+ (``sim_home_assistant.HomeAssistantBook``). The run walks from it,
193
+ and "deliverable now" reads it (``energy``), whatever the gap between
194
+ the two probes. Without it, the probes alone answer only a full tank,
195
+ and a tank with the probes apart reads 0 (``probes``). With no full
196
+ tank in the 72 hours, or a scenario that doesn't start now, the run
197
+ walks without the book, and a warning says why.
198
+
199
+ Under "deliverable now", the table gives the bath and the showers the
200
+ tank is ready for (``sim.time_to_ready``): the bath ready with its
201
+ margin, or how short it is and when the heat pump has it ready; and the
202
+ whole showers, as a shower the tank can't finish isn't counted. Both are
203
+ delivered volumes, past the mixing valve. ``--bath`` and ``--shower``
204
+ give them in the preferred unit; without them, the parameters'
205
+ (``ReadinessParams.bath_gal``, 28.7 gal, and ``shower_gal``, 12 gal).
175
206
 
176
207
  A reading's age is from when Home Assistant last heard the sensor
177
208
  report (``last_reported``). Over the REST API, Home Assistant serves
@@ -191,8 +222,34 @@ same parts. Given ``heater=``, a ``HeaterEntities`` with ``ambient`` and
191
222
  heater's own air reading every 10 minutes while the compressor runs,
192
223
  where the reading was taken 2 minutes or more into the run (earlier, or
193
224
  idle, it reads the cabinet), and not while the sensor was unavailable.
194
- The latest such reading, if under an hour old, anchors the projection.
195
- ``fitted_air`` holds the fit once ``read`` has run.
225
+ With a model, given or fitted, and no fresh reading beside the heater,
226
+ each ``read`` anchors the projection to the heater's newest such
227
+ reading from the last hour. ``fitted_air`` holds the fit once ``read``
228
+ has run.
229
+
230
+ A caller that builds a new environment every few minutes needn't refit
231
+ each time. Fit on your own cadence, and pass the fit on:
232
+
233
+ .. code-block:: python
234
+
235
+ fitted, notes = await ha.fit_location_air(
236
+ client, entities, now=now, heater=heater_entities
237
+ ) # hourly, say; None (and notes why) without enough history
238
+
239
+ environment = ha.HomeAssistantEnvironment(
240
+ client, entities, start=now, until=now + horizon, now=now,
241
+ heater=heater_entities, fitted_air=fitted,
242
+ )
243
+
244
+ Given ``fitted_air``, the environment projects through it, notes its r
245
+ and lag on every ``read``, and doesn't fit again.
246
+
247
+ The fit, the samples of the heater's air and the inlet's plateau are
248
+ CPU work: the fit alone takes about 100 ms on 3 days of 30-second
249
+ readings. To keep it off an event loop, pass ``run=``, a callable that
250
+ runs a function and awaits its result, to ``fit_location_air`` and
251
+ ``HomeAssistantEnvironment``. In Home Assistant that is
252
+ ``hass.async_add_executor_job``. Without it, the work runs inline.
196
253
 
197
254
 
198
255
  Fetch what the heater reports
@@ -226,7 +283,10 @@ A scenario starts from four things the library already reads:
226
283
  heater=sim_live.LiveHeaterSource(mqtt, device),
227
284
  schedules=sim_live.LiveScheduleSource(mqtt, api, device),
228
285
  )
229
- return sim.simulate(composed.scenario, composed.params)
286
+ # Without the feature info the tank size is unknown: the 65 gal
287
+ # defaults stand in.
288
+ params = composed.params or sim.default_params()
289
+ return sim.simulate(composed.scenario, params)
230
290
 
231
291
  ``environment=`` and ``loads=`` take any source with an ``async
232
292
  read()``; ``sim_live.Given(value)`` is one for a value in hand. Without
@@ -234,7 +294,7 @@ read()``; ``sim_live.Given(value)`` is one for a value in hand. Without
234
294
  gets no reply raises ``sim_live.LiveReadError``; so does the schedule
235
295
  source when TOU is on and its periods cannot be read. A missing
236
296
  feature info leaves ``composed.params`` None (the tank size is
237
- unknown). Change a part with ``composed.scenario.model_copy(update=...)``.
297
+ unknown), so the example falls back to ``sim.default_params()``. Change a part with ``composed.scenario.model_copy(update=...)``.
238
298
 
239
299
  With the status, the feature info, the reservations and the TOU
240
300
  periods already read some other way, build the same parts from them:
@@ -887,14 +947,106 @@ with the instants the heater was heard from:
887
947
  params,
888
948
  at=now,
889
949
  flow=draws_gpm, # gal/min, whatever the preferred units
950
+ flow_outages=flow_down, # when the flow entity was unavailable
951
+ circulation=runs, # [(start, end), ...], if you circulate
952
+ circulation_flow=pump_flow, # the loop's gal/min while it runs
953
+ )
954
+
955
+ ``flow`` is the household's draws. If your meter also carries the
956
+ circulation's water, separate the household's draws from it yourself
957
+ and pass those, with ``CirculationParams.meter_share`` 0, so the book
958
+ takes nothing off them again. Clean up the loop's flow before passing it
959
+ too (drop readings below the pump's floor, or one taken mid-ramp): while
960
+ a run is on, each step takes the last reading in force (a run before
961
+ any reading takes ``CirculationParams.flow_gpm``).
962
+
963
+ Like the history's rows, its ``flow``, ``circulation_flow``, ``inlet``
964
+ and ``air`` are changes: each value holds until the next, so a steady
965
+ draw is one row and an hour at 0 gal/min none. They are gal/min and °F,
966
+ whatever the preferred units. A span the heater wasn't heard from (an
967
+ outage, or a silence much longer than its usual reporting), or the flow
968
+ was unknown (``flow_outages``, from the history's ``outages``), breaks
969
+ the book: it is walked only from an anchor after the last one, and is
970
+ None while the heater isn't heard now. A record that keeps no instant
971
+ for each of the heater's responses passes ``heard=``, the spans the
972
+ heater was heard over, and an empty ``observed``: a gap between two
973
+ spans breaks the book the same way, and the heater is heard now only
974
+ inside a span. With no flow reading in force
975
+ at the start of the window it books over, it raises ``ValueError``.
976
+ An inlet reading holds except through ``inlet_outages``.
977
+
978
+ To keep a book current as you walk every few minutes, keep the history
979
+ rather than the book. Read only what is new since your last read,
980
+ extend what you hold with it, trim it to the book's window, and book
981
+ again from memory:
982
+
983
+ .. code-block:: python
984
+
985
+ newer = await ha.read_heater_history(client, entities, last_read, now)
986
+ history = history.extended(newer).since(now - timedelta(hours=72))
987
+ flows = flows.extended(
988
+ await client.read_history([FLOW], last_read, now)
989
+ ).since(now - timedelta(hours=72))
990
+ last_read = now
991
+
992
+ ``HeaterHistory`` and ``History`` both take ``extended`` and ``since``.
993
+ A read opens with the value in force at its start; as each value holds
994
+ until the next change, that row changes nothing. Where both reads have
995
+ a row at one instant, the later read's is kept. ``since`` keeps each
996
+ value in force at the cut. 72 hours is ``BookParams.history_h``, the
997
+ oldest anchor a book reads.
998
+
999
+ ``sim_home_assistant.HomeAssistantBook`` does all of that from Home
1000
+ Assistant's entities:
1001
+
1002
+ .. code-block:: python
1003
+
1004
+ keeper = ha.HomeAssistantBook(
1005
+ client,
1006
+ heater_entities, # ha.HeaterEntities
1007
+ "sensor.hot_water_draw_flow", # the household's draws
1008
+ ha.Entities(
1009
+ circulation_speed="sensor.circulation_pump_speed",
1010
+ inlet="sensor.cold_supply_temperature",
1011
+ demand="binary_sensor.hot_water_demand",
1012
+ ),
890
1013
  )
891
1014
 
892
- Like the history's rows, its ``flow`` and ``circulation_flow`` are
893
- gal/min and its ``inlet`` and ``air`` °F, whatever the preferred units.
894
- A span the heater wasn't heard from (an outage, or a silence much
895
- longer than its usual reporting) breaks the book: it is walked only
896
- from an anchor after the last one, and is None while the heater isn't
897
- heard now.
1015
+ await keeper.read(now) # all 72 h the first time, then what's new
1016
+ answer = await hass.async_add_executor_job(keeper.book, now, params)
1017
+ readiness = sim.time_to_ready(
1018
+ scenario, params, book=answer.booked,
1019
+ inlet_measured=answer.inlet_measured,
1020
+ )
1021
+
1022
+ ``read`` reads the history since the last read and holds it; a read
1023
+ that fails is noted, and the next one reads from where it would have
1024
+ started. Until then, ``book`` takes the flow as unknown since the last
1025
+ read that worked, rather than holding its last value, and after a
1026
+ failed read of the heater it gives no book. An optional
1027
+ entity whose unit can't be read is left out of the book. With no air
1028
+ entity, or one that gives no reading, the book takes the heater's own
1029
+ air reading (``HeaterEntities.ambient``) once its compressor has run 5
1030
+ minutes, and holds it until the next run, and a note says so. Idle, that
1031
+ sensor reads the cabinet, about 6 °F warm, so an idle reading is never
1032
+ taken. Before the first run, and while an air entity that gives readings
1033
+ is unavailable or reads in a unit the book doesn't read, the book takes
1034
+ the default air (65 °F). ``book`` books from what it holds, with no
1035
+ request, so it can run in an executor. ``answer.notes`` say what the book rests on, or why
1036
+ there is none.
1037
+
1038
+ The flow is the household's hot water draws, without the circulation's
1039
+ water, in gal/min or any unit ``to_gpm`` reads. It is the water
1040
+ delivered at the taps, through the mixing valve: the book prices a
1041
+ delivered gallon at the valve's delivery line
1042
+ (``DrawParams.delivery_intercept_f`` and ``delivery_slope``), not as
1043
+ water withdrawn from the tank. As none of the flow is circulation,
1044
+ ``book`` takes ``CirculationParams.meter_share`` as 0. A flow entity that reports a draw late
1045
+ reaches the book late: the book holds what the history holds at
1046
+ ``read``. Each of the other entities is optional:
1047
+ ``circulation_speed`` gives the circulation's runs, ``inlet`` the inlet
1048
+ the book converts at, ``inlet`` with ``demand`` the mains, and ``air``
1049
+ the air beside the heater.
898
1050
 
899
1051
  Without a meter, start one only when you know the tank is full and
900
1052
  quiet (``sa.full_tank_book``).
@@ -1035,15 +1187,18 @@ to a day apart. Each pair's booked heat is checked against the probes':
1035
1187
  circulation=runs, circulation_flow=pump_flow, params=params,
1036
1188
  )
1037
1189
  book = calibration.RecordedBook(steps, p, params.circulation, start=since)
1038
- fit = calibration.fit_book_terms(book, book.reanchors(), p)
1190
+ fit = calibration.fit_book_terms(book, book.reanchors(), p, circuit=True)
1039
1191
  p = replace(p, hp_btu_per_h=fit.hp_btu_per_h,
1040
1192
  hp_btu_per_h_per_f=fit.hp_btu_per_h_per_f,
1041
1193
  ua_btu_per_h_f=fit.ua_btu_per_h_f)
1042
- bands = calibration.book_error_bands(book, p, fit.loss_btu_per_gal)
1043
- empty, crossings = calibration.empty_point(book, p, fit.loss_btu_per_gal)
1044
- zone = calibration.zone_empty_point(book, p, fit.loss_btu_per_gal)
1045
-
1046
- ``book_error_btu(book, p, fit.loss_btu_per_gal)`` gives the book's
1194
+ v = fit.book_circuit_gal
1195
+ bands = calibration.book_error_bands(book, p, 0.0, v)
1196
+ empty, crossings = calibration.empty_point(book, p, 0.0, circuit_gal=v)
1197
+ zone = calibration.zone_empty_point(book, p, 0.0, circuit_gal=v)
1198
+
1199
+ With ``circuit=True`` the runs are priced at a circuit volume
1200
+ (``CirculationParams.book_circuit_gal``) fitted in place of a loss per
1201
+ gallon. ``book_error_btu(book, p, 0.0, v)`` gives the book's
1047
1202
  error bands: the 90th percentile of its error pooled over re-anchors an
1048
1203
  hour to ``max_anchor_age_h`` apart, and the 18 to 24 h band's past it,
1049
1204
  each rounded up to 100 BTU. ``cold_lower_cap(draws,