nwp500-python 9.10.0__tar.gz → 9.12.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 (346) hide show
  1. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/CHANGELOG.rst +199 -0
  2. {nwp500_python-9.10.0/src/nwp500_python.egg-info → nwp500_python-9.12.0}/PKG-INFO +1 -1
  3. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/calibrate-simulator.rst +45 -23
  4. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/simulate-heater.rst +167 -19
  5. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/python_api/sim.rst +10 -5
  6. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/simulator-inputs.rst +44 -17
  7. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/cli/calibration.py +2 -0
  8. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/cli/simulate.py +1 -0
  9. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/api.py +50 -13
  10. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/book.py +38 -5
  11. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/calibration.py +140 -33
  12. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/circulation.py +96 -7
  13. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/defaults.py +26 -9
  14. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/delivery.py +30 -12
  15. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/inputs.py +3 -0
  16. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/params.py +67 -9
  17. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/ready.py +18 -7
  18. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim_adapters.py +443 -51
  19. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim_calibrate.py +250 -51
  20. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim_home_assistant.py +1159 -90
  21. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim_live.py +30 -2
  22. {nwp500_python-9.10.0 → nwp500_python-9.12.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  23. nwp500_python-9.12.0/src/nwp500_python.egg-info/scm_version.json +8 -0
  24. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_adapters.py +8 -3
  25. nwp500_python-9.12.0/tests/test_sim_book_history.py +954 -0
  26. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_bottom_loss.py +11 -0
  27. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_calibrate.py +136 -0
  28. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_calibration.py +9 -0
  29. nwp500_python-9.12.0/tests/test_sim_change_history.py +1373 -0
  30. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_circulation.py +122 -6
  31. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_defaults.py +16 -4
  32. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_fit_book.py +139 -72
  33. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_home_assistant.py +863 -10
  34. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_hot_water.py +57 -2
  35. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_live.py +23 -0
  36. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_overlay.py +66 -0
  37. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_ready_api.py +165 -0
  38. nwp500_python-9.10.0/src/nwp500_python.egg-info/scm_version.json +0 -8
  39. nwp500_python-9.10.0/tests/test_sim_book_history.py +0 -435
  40. nwp500_python-9.10.0/tests/test_sim_change_history.py +0 -572
  41. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/.coveragerc +0 -0
  42. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  43. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/.github/copilot-instructions.md +0 -0
  44. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/.github/workflows/ci.yml +0 -0
  45. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/.github/workflows/release.yml +0 -0
  46. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/.gitignore +0 -0
  47. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/.pre-commit-config.yaml +0 -0
  48. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/.readthedocs.yml +0 -0
  49. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/AUTHORS.rst +0 -0
  50. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/CONTRIBUTING.rst +0 -0
  51. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/LICENSE.txt +0 -0
  52. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/Makefile +0 -0
  53. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/README.rst +0 -0
  54. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/RELEASE.md +0 -0
  55. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/Makefile +0 -0
  56. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/_static/.gitignore +0 -0
  57. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/conf.py +0 -0
  58. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/explanation/architecture.rst +0 -0
  59. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/explanation/draws.rst +0 -0
  60. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/explanation/heating-elements.rst +0 -0
  61. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/explanation/index.rst +0 -0
  62. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/explanation/recovery-speed.rst +0 -0
  63. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/explanation/simulator.rst +0 -0
  64. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/explanation/standby-loss.rst +0 -0
  65. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/explanation/tank-energy.rst +0 -0
  66. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/explanation/tou-recovery-cap.rst +0 -0
  67. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/explanation/unknown-values.rst +0 -0
  68. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/explanation/what-starts-a-recovery.rst +0 -0
  69. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/authenticate.rst +0 -0
  70. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/auto-recovery.rst +0 -0
  71. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/bluetooth.rst +0 -0
  72. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/diagnose-mqtt.rst +0 -0
  73. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/home-assistant.rst +0 -0
  74. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/index.rst +0 -0
  75. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/maintenance.rst +0 -0
  76. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/manage-units.rst +0 -0
  77. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/monitor-status.rst +0 -0
  78. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/optimize-tou.rst +0 -0
  79. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/queue-commands.rst +0 -0
  80. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/schedule-operation.rst +0 -0
  81. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/how-to/track-energy.rst +0 -0
  82. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/index.rst +0 -0
  83. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/openapi.yaml +0 -0
  84. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/project/authors.rst +0 -0
  85. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/project/changelog.rst +0 -0
  86. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/project/contributing.rst +0 -0
  87. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/project/history.rst +0 -0
  88. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/project/license.rst +0 -0
  89. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/configuration.rst +0 -0
  90. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/enumerations.rst +0 -0
  91. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/index.rst +0 -0
  92. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/installation.rst +0 -0
  93. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/protocol/data_conversions.rst +0 -0
  94. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/protocol/device_features.rst +0 -0
  95. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/protocol/device_status.rst +0 -0
  96. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/protocol/error_codes.rst +0 -0
  97. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  98. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/protocol/quick_reference.rst +0 -0
  99. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/protocol/rest_api.rst +0 -0
  100. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/python_api/api_client.rst +0 -0
  101. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/python_api/auth_client.rst +0 -0
  102. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/python_api/cli.rst +0 -0
  103. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/python_api/events.rst +0 -0
  104. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/python_api/exceptions.rst +0 -0
  105. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/python_api/models.rst +0 -0
  106. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/reference/python_api/mqtt_client.rst +0 -0
  107. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/requirements.txt +0 -0
  108. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/docs/tutorials/getting-started.rst +0 -0
  109. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/.ruff.toml +0 -0
  110. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/README.md +0 -0
  111. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/air_filter_reset.py +0 -0
  112. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/anti_legionella.py +0 -0
  113. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/auto_recovery.py +0 -0
  114. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/combined_callbacks.py +0 -0
  115. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/demand_response.py +0 -0
  116. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/device_capabilities.py +0 -0
  117. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/device_status_debug.py +0 -0
  118. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/energy_analytics.py +0 -0
  119. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/error_code_demo.py +0 -0
  120. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/firmware_payload_capture.py +0 -0
  121. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/mqtt_diagnostics.py +0 -0
  122. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/power_control.py +0 -0
  123. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/recirculation_control.py +0 -0
  124. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/reconnection_demo.py +0 -0
  125. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/reservation_schedule.py +0 -0
  126. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/simple_auto_recovery.py +0 -0
  127. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/token_restoration.py +0 -0
  128. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/tou_openei.py +0 -0
  129. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/tou_schedule.py +0 -0
  130. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/advanced/water_reservation.py +0 -0
  131. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/beginner/01_authentication.py +0 -0
  132. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/beginner/02_list_devices.py +0 -0
  133. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/beginner/03_get_status.py +0 -0
  134. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/beginner/04_set_temperature.py +0 -0
  135. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
  136. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/intermediate/command_queue.py +0 -0
  137. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/intermediate/device_status_callback.py +0 -0
  138. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/intermediate/error_handling.py +0 -0
  139. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/intermediate/event_driven_control.py +0 -0
  140. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/intermediate/improved_auth.py +0 -0
  141. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
  142. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  143. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/intermediate/periodic_requests.py +0 -0
  144. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/intermediate/set_mode.py +0 -0
  145. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/intermediate/vacation_mode.py +0 -0
  146. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/mask.py +0 -0
  147. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/simulator/lower-setpoint.json +0 -0
  148. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/simulator/plan.json +0 -0
  149. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/simulator/scenario.json +0 -0
  150. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/testing/periodic_device_info.py +0 -0
  151. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/testing/simple_periodic_info.py +0 -0
  152. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/testing/test_api_client.py +0 -0
  153. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/testing/test_mqtt_connection.py +0 -0
  154. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/testing/test_mqtt_messaging.py +0 -0
  155. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/examples/testing/test_periodic_minimal.py +0 -0
  156. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/pyproject.toml +0 -0
  157. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/scripts/README.md +0 -0
  158. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/scripts/bump_version.py +0 -0
  159. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/scripts/diagnose_mqtt_connection.py +0 -0
  160. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/scripts/extract_changelog.py +0 -0
  161. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/scripts/format.py +0 -0
  162. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/scripts/lint.py +0 -0
  163. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/scripts/setup-dev.py +0 -0
  164. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/scripts/validate_version.py +0 -0
  165. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/setup.cfg +0 -0
  166. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/setup.py +0 -0
  167. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/__init__.py +0 -0
  168. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/_base.py +0 -0
  169. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/api_client.py +0 -0
  170. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/auth.py +0 -0
  171. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/ble/__init__.py +0 -0
  172. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/ble/__main__.py +0 -0
  173. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/ble/client.py +0 -0
  174. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/ble/mqtt_adapter.py +0 -0
  175. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/ble/protocol.py +0 -0
  176. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/cli/__init__.py +0 -0
  177. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/cli/__main__.py +0 -0
  178. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/cli/ble_backend.py +0 -0
  179. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/cli/handlers.py +0 -0
  180. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/cli/monitoring.py +0 -0
  181. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/cli/output_formatters.py +0 -0
  182. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/cli/presentation.py +0 -0
  183. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/cli/recording.py +0 -0
  184. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/cli/rich_output.py +0 -0
  185. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/cli/token_storage.py +0 -0
  186. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/command_decorators.py +0 -0
  187. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/config.py +0 -0
  188. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/converters.py +0 -0
  189. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/device_capabilities.py +0 -0
  190. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/device_info_cache.py +0 -0
  191. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/encoding.py +0 -0
  192. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/enums.py +0 -0
  193. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/events.py +0 -0
  194. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/exceptions.py +0 -0
  195. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/factory.py +0 -0
  196. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/field_factory.py +0 -0
  197. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/models/__init__.py +0 -0
  198. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/models/_converters.py +0 -0
  199. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/models/account.py +0 -0
  200. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/models/device.py +0 -0
  201. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/models/diagnostics.py +0 -0
  202. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/models/energy.py +0 -0
  203. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/models/feature.py +0 -0
  204. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/models/firmware.py +0 -0
  205. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/models/maintenance.py +0 -0
  206. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/models/mqtt_models.py +0 -0
  207. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/models/schedule.py +0 -0
  208. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/models/status.py +0 -0
  209. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/models/tou.py +0 -0
  210. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/__init__.py +0 -0
  211. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/_control_commands.py +0 -0
  212. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
  213. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/client.py +0 -0
  214. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/command_queue.py +0 -0
  215. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/connection.py +0 -0
  216. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/control.py +0 -0
  217. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/diagnostics.py +0 -0
  218. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/periodic.py +0 -0
  219. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/reconnection.py +0 -0
  220. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/state_tracker.py +0 -0
  221. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/subscriptions.py +0 -0
  222. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/types.py +0 -0
  223. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt/utils.py +0 -0
  224. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/mqtt_events.py +0 -0
  225. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/openei.py +0 -0
  226. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/py.typed +0 -0
  227. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/reservations.py +0 -0
  228. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/__init__.py +0 -0
  229. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/attribution.py +0 -0
  230. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/codec.py +0 -0
  231. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/cold_supply.py +0 -0
  232. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/controller.py +0 -0
  233. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/decay.py +0 -0
  234. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/draw_forecast.py +0 -0
  235. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/engine.py +0 -0
  236. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/extensions.py +0 -0
  237. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/heat_pump_recovery.py +0 -0
  238. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/heating_trajectory.py +0 -0
  239. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/location_air.py +0 -0
  240. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/observed.py +0 -0
  241. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/physics/__init__.py +0 -0
  242. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/physics/compressor.py +0 -0
  243. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/physics/draw.py +0 -0
  244. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/physics/element.py +0 -0
  245. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/physics/refrigerant.py +0 -0
  246. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/physics/tank.py +0 -0
  247. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/profile.py +0 -0
  248. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/record.py +0 -0
  249. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/result.py +0 -0
  250. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/schedules.py +0 -0
  251. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/tank_physics.py +0 -0
  252. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/tou_triggers.py +0 -0
  253. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/sim/units.py +0 -0
  254. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/status_history.py +0 -0
  255. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/temperature.py +0 -0
  256. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/topic_builder.py +0 -0
  257. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/tou_schedule.py +0 -0
  258. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/unit_system.py +0 -0
  259. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500/utils.py +0 -0
  260. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500_python.egg-info/SOURCES.txt +0 -0
  261. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  262. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  263. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  264. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500_python.egg-info/requires.txt +0 -0
  265. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500_python.egg-info/scm_file_list.json +0 -0
  266. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
  267. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/_sim_site.py +0 -0
  268. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/conftest.py +0 -0
  269. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/fixtures/sim/engine_golden.json +0 -0
  270. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/fixtures/sim/heater_params.json +0 -0
  271. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/fixtures/sim/heater_params_fitted.json +0 -0
  272. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_api_endpoints_201.py +0 -0
  273. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_api_helpers.py +0 -0
  274. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_app_queries.py +0 -0
  275. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_auth.py +0 -0
  276. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_auth_session_lifecycle.py +0 -0
  277. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_auth_session_refresh.py +0 -0
  278. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_ble_mqtt_adapter.py +0 -0
  279. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_ble_protocol.py +0 -0
  280. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_bug_fixes.py +0 -0
  281. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_canonical_schedule.py +0 -0
  282. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_cli_basic.py +0 -0
  283. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_cli_calibrate.py +0 -0
  284. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_cli_commands.py +0 -0
  285. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_cli_simulate.py +0 -0
  286. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_cli_tristate_presentation.py +0 -0
  287. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_command_decorators.py +0 -0
  288. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_command_queue.py +0 -0
  289. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_device_capabilities.py +0 -0
  290. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_device_info_cache.py +0 -0
  291. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_device_rest_models.py +0 -0
  292. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_events.py +0 -0
  293. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_exceptions.py +0 -0
  294. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_model_converters.py +0 -0
  295. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_models.py +0 -0
  296. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_mqtt_clean_session_resume.py +0 -0
  297. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_mqtt_client_init.py +0 -0
  298. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_mqtt_control_fail.py +0 -0
  299. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_mqtt_events.py +0 -0
  300. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_mqtt_hypothesis.py +0 -0
  301. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_mqtt_last_will.py +0 -0
  302. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_mqtt_reconnection.py +0 -0
  303. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_mqtt_reconnection_storm.py +0 -0
  304. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_mqtt_reliability.py +0 -0
  305. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_multi_device.py +0 -0
  306. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_navilink_201_enums.py +0 -0
  307. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_openei.py +0 -0
  308. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_protocol_correctness.py +0 -0
  309. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_public_api.py +0 -0
  310. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_reservation_validation.py +0 -0
  311. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_reservations.py +0 -0
  312. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_api.py +0 -0
  313. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_book.py +0 -0
  314. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_codec.py +0 -0
  315. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_cold_supply.py +0 -0
  316. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_controller.py +0 -0
  317. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_documents.py +0 -0
  318. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_draw_forecast.py +0 -0
  319. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_draw_loss.py +0 -0
  320. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_element.py +0 -0
  321. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_element_fit.py +0 -0
  322. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_forecast.py +0 -0
  323. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_golden.py +0 -0
  324. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_import_contract.py +0 -0
  325. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_location_air.py +0 -0
  326. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_no_clock_no_state.py +0 -0
  327. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_observed.py +0 -0
  328. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_profile.py +0 -0
  329. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_readiness.py +0 -0
  330. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_readiness_fit.py +0 -0
  331. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_ready.py +0 -0
  332. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_replay.py +0 -0
  333. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_schedules.py +0 -0
  334. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_sim_tank_physics.py +0 -0
  335. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_status_history.py +0 -0
  336. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_temperature_converters.py +0 -0
  337. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_threading_model.py +0 -0
  338. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_token_storage.py +0 -0
  339. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_tou_api.py +0 -0
  340. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_tou_schedule.py +0 -0
  341. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_unit_switching.py +0 -0
  342. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_unit_system_process_wide.py +0 -0
  343. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_utility_modules.py +0 -0
  344. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tests/test_utils.py +0 -0
  345. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/tox.ini +0 -0
  346. {nwp500_python-9.10.0 → nwp500_python-9.12.0}/uv.lock +0 -0
@@ -2,6 +2,205 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version 9.12.0 (2026-10-23)
6
+ ===========================
7
+
8
+ Added
9
+ -----
10
+
11
+ - **The heater's own entities in the sources file** (#232).
12
+ ``[home_assistant.heater]`` names them (``heard``, ``upper`` and
13
+ ``lower`` required), so the CLI fits and anchors the projected air to
14
+ the heater's reading while its compressor runs where no air entity
15
+ does.
16
+ - **The idle cabinet air, said** (#232). ``Environment.ambient_cabinet``
17
+ marks air taken from the heater's own reading while the compressor
18
+ was idle, about 6 °F warm, and answers give the reason
19
+ ``ambient_from_idle_cabinet``.
20
+ - **The heater's own air in the book** (#236). With no air reading,
21
+ ``book_from_history``, ``book_from_changes`` and
22
+ ``HomeAssistantBook`` take the heater's own air reading once its
23
+ compressor has run 5 minutes (``sim_adapters.settled_air``,
24
+ ``HEATER_AIR_SETTLE``), held until the next run, rather than 65 °F
25
+ throughout. ``HomeAssistantBook`` notes it.
26
+ - **The standby loss from the heater's own air** (#236). With no air
27
+ given, calibration fits the standby loss on the heater's own reading
28
+ once its compressor has run 5 minutes, interpolated from one run to
29
+ the next, over the quiet windows with a run within
30
+ ``HEATER_AIR_BRACKET`` (6 h) of both ends: the law's level only, on
31
+ the base's exponent. ``Calibrated.air`` is ``"heater"``, and a note
32
+ says how many windows qualified and their share of the quiet hours.
33
+ It was left unfitted. ``fit_ua_law``, ``fit_loss``, ``fit_standby``
34
+ and ``calibration.calibrate`` take a fixed exponent.
35
+ - **The book prices a circulation run by a circuit volume.**
36
+ ``CirculationParams.book_circuit_gal``: with it, the heat book
37
+ charges a run its first gallons up to that volume, runs closer than
38
+ ``run_gap_min`` one run, each at the tank's upper probe less the air;
39
+ without it, ``loss_btu_per_gal`` per gallon as before. ``fit_book``
40
+ fits the volume in place of the loss per gallon (``book_circuit_gal``
41
+ on its result), and ``fit_book_terms`` with ``circuit=True``. At one
42
+ installation, over 284 re-anchors, the book's error on pairs with a
43
+ run fell 14 % against a refitted loss per gallon (1741 against 2023
44
+ BTU), and on held-out months from 3184 to 2454 BTU, the volume
45
+ steady at 1.1 to 1.5 gal where the loss per gallon moved from 59 to
46
+ 120. It is an effective volume, apart from ``displaced_gal``.
47
+ ``SITE_CIRCULATION`` carries one installation's, and the book's
48
+ heat-pump rate and slope are refitted with it (4338 BTU/h, 66.0
49
+ BTU/h/°F; the UA stays 2.88).
50
+
51
+ - **The heater's heard spans, given** (#230). ``book_from_changes``
52
+ takes ``heard=``, the spans the heater was heard over, for a record
53
+ that keeps no instant for each response. A gap between spans breaks
54
+ the book as a silence between instants does, and the heater is heard
55
+ at ``at`` only inside a span.
56
+ - **The air fit carried across reads** (#221).
57
+ ``sim_home_assistant.fit_location_air`` is the fit
58
+ ``HomeAssistantEnvironment`` makes where it is given no model, with
59
+ its notes, as a call of its own. ``HomeAssistantEnvironment`` takes
60
+ it back as ``fitted_air``, projects through it, and notes its r and
61
+ lag on every ``read`` without fitting again. A caller that reads
62
+ every few minutes fits on its own cadence.
63
+ - **The heat book kept current from Home Assistant** (#227).
64
+ ``sim_home_assistant.HomeAssistantBook`` holds the heater's and the
65
+ household flow's history, reading only what is new on each ``read``,
66
+ and books from it with no request (``book``, for an executor). The
67
+ flow is the household's draws as delivered through the mixing valve.
68
+ The circulation's speed, an inlet probe and the demand entity, each
69
+ optional, give the runs, the inlet and the conversion at the mains
70
+ (``BookAnswer.inlet_measured``). ``mains_plateau`` and ``known_runs``
71
+ are the inlet's plateau and the pump's runs from a ``History``, as
72
+ ``HomeAssistantEnvironment`` reads them. ``book_from_changes`` and
73
+ ``book_steps_from_changes`` take ``air_outages``: while the air is
74
+ unknown the book takes the default air, not the last reading.
75
+ - **The CPU work off the event loop** (#225). ``fit_location_air`` and
76
+ ``HomeAssistantEnvironment`` take ``run=`` (``Run``), which runs a
77
+ piece of work and awaits its result, such as Home Assistant's
78
+ ``hass.async_add_executor_job``. The fit, the samples of the heater's
79
+ air and the inlet's plateau go through it. Without it they run
80
+ inline (``inline``), as before.
81
+ - **A history kept current** (#223). ``sim_home_assistant.HeaterHistory``,
82
+ ``sim_home_assistant.History`` and ``sim_adapters.ChangeRows`` take
83
+ ``extended(newer)``, which merges a later read in by instant (the
84
+ later read's row winning at a shared instant) and joins the outages,
85
+ and ``since(start)``, which trims to ``start`` and keeps each value in
86
+ force there. A caller that books every few minutes reads only what is
87
+ new and books from what it holds. ``sim_adapters.merge_changes``,
88
+ ``changes_since``, ``join_spans`` and ``spans_since`` do it for one
89
+ series.
90
+
91
+ Changed
92
+ -------
93
+
94
+ - ``SITE_CIRCULATION.meter_share`` is 0. The book's flow is the
95
+ household's draws alone, which no circulation run passes, so the
96
+ example terms take nothing off it. A caller whose meter carries the
97
+ loop's water still sets its own share.
98
+
99
+ - **Readiness with no evidence and no book** (#234). A tank whose probes
100
+ are within the book's ``uniform_band_f`` reads its full-tank floor
101
+ from its probes (``Deliverable.basis`` ``probes``), at any
102
+ temperature. Any other tank reads 0 with the reason
103
+ ``no_evidence_for_state``, which says the gallons are unknown, not
104
+ none, in place of ``colder_than_all_evidence``.
105
+ - The 50 and 80 gal defaults scale the book's empty point
106
+ (``BookParams.empty_btu``) and the full tank's hidden cold water
107
+ (``ReadinessParams.full_hidden_gal``) by tank size, as they scale its
108
+ volume (#234). Both were the 65 gal tank's at every size.
109
+ - Circulation runs given with params that have no circulation terms are
110
+ priced at one installation's (``SITE_CIRCULATION``), and the answer
111
+ says so in ``defaults_used`` (#232). They raised in every walk, and a
112
+ book left them unpriced. ``sim_adapters.with_circulation_terms`` does
113
+ this for the books.
114
+ - A status in Standby is Power Off, as the NaviLink app counts it (#232).
115
+ It raised.
116
+ - ``book_from_changes`` books from the flow's first reading when its
117
+ history starts later than the window, rather than raising (#232).
118
+ - ``known_runs`` returns the pump's runs from its first reading and the
119
+ span before it as unknown, rather than nothing for a history shorter
120
+ than the span (#232). The inlet's plateau skips a draw in that span,
121
+ and ``HomeAssistantBook`` takes no anchor before it.
122
+ - ``read_heater_history`` leaves out a heating flag entity with no
123
+ history at all, with a note, rather than leaving the whole record
124
+ unobserved (#232).
125
+
126
+ - The simulate how-to's ``book_from_changes`` example passes the
127
+ circulation's runs and flow, and says to pass the household's draws,
128
+ separated from any circulation in the meter, with ``meter_share`` 0.
129
+ - With a model, given or fitted, and no fresh reading of the air beside
130
+ the heater, ``HomeAssistantEnvironment`` anchors the projection to
131
+ ``heater``'s newest air reading from the last hour while its
132
+ compressor ran, on every ``read``. It anchored to one only on the
133
+ read that fitted the model (#221).
134
+
135
+ Fixed
136
+ -----
137
+
138
+ - ``simulate`` and ``time_to_ready`` convert a book's heat to gallons at
139
+ the scenario's inlet (``Environment.inlet_f``) where one is given, as
140
+ the walk's draws take it (#234). The gallons ignored a measured inlet,
141
+ and with a book a colder inlet gave warmer delivered water.
142
+ - ``fit_location_air`` fits to the heater's own air when the air entity
143
+ can't be read or gives no fit (#232).
144
+ - Calibration's recoveries outside a partial air record read the
145
+ heater's own air, rather than being dropped (#232).
146
+ - The simulate how-to's and ``sim_live``'s ``compose`` example falls
147
+ back to the 65 gal defaults when the feature info doesn't arrive
148
+ (#232). It passed None to ``simulate``.
149
+ - ``HomeAssistantBook`` says when the flow is unknown now (#232).
150
+
151
+ - **A heater read missing a heating flag is unobserved** (#227).
152
+ ``read_heater_history`` reads a day at a time; a window with nothing
153
+ of the compressor, an element or the heat source (each opens with
154
+ the value in force) now leaves the heater unobserved over it, with a
155
+ note, rather than holding the flag from before.
156
+ - ``sim_adapters.book_from_changes`` read its flow as a sampled meter:
157
+ a reading held 60 seconds (``FLOW_HOLD``) and the inlet's an hour,
158
+ and flow rows more than an hour apart refused the book as a silent
159
+ meter. A history of changes writes a row only when a value changes,
160
+ so a steady draw counted for a minute and an hour at 0 gal/min was
161
+ refused. Its flows, inlet and air now hold until the next change. It
162
+ takes ``flow_outages``, a span with no anchor before it counting, as
163
+ for a heater unheard, and ``inlet_outages``. No flow reading in force
164
+ at the window's start is a ``ValueError`` (#219).
165
+ - ``sim_home_assistant.pump_runs`` ended a run 5 minutes after its last
166
+ speed reading. A pump at a steady speed writes no row, so a long run
167
+ read as 5 minutes. A speed now holds until it changes; ``stale``
168
+ (``PUMP_STALE``) keeps the old rule for a sampled speed (#219).
169
+
170
+ Version 9.11.0 (2026-10-22)
171
+ ===========================
172
+
173
+ Added
174
+ -----
175
+ - **The air from a weather entity without a model** (#214). With a
176
+ weather entity and no ``location_air`` model,
177
+ ``sim_home_assistant.HomeAssistantEnvironment`` fits one
178
+ (``sim.location_air.fit``) on the last 3 days
179
+ (``LOCATION_AIR_WINDOW``) of the air entity's readings, or with
180
+ ``heater=`` the heater's own air reading while its compressor runs,
181
+ against the weather entity's outdoor temperature, each reading from
182
+ the unit it was recorded in (``HomeAssistantClient.outdoor_history``,
183
+ ``parse_outdoor_history``). It projects the forecast through the fit
184
+ when the readings the outdoor history covers span a day and follow
185
+ the outdoor air at r 0.5 or more, and keeps it as ``fitted_air``.
186
+ ``nwp-cli simulate --sources`` fits one the same way when the sources
187
+ file has no ``[location_air]``.
188
+
189
+ Fixed
190
+ -----
191
+ - ``sim.params.params_from_document`` raised ``TypeError`` for a list
192
+ field given as something else (``unfitted``, the draw domain ranges,
193
+ readiness ``evidence``), past the ``ValueError`` it documents and
194
+ that ``sim_live.overlay`` and ``nwp-cli simulate`` handle. It read a
195
+ string, null or bool in a number field without complaint. Each is
196
+ now a ``ValueError`` naming the field (#212).
197
+ - ``HomeAssistantEnvironment`` found no inlet plateau for a probe that
198
+ held one value through a long draw's tail. Home Assistant's history
199
+ writes a row only when a value changes, so the tail had one reading
200
+ or none, under the plateau's 3. The probe's history is now read as
201
+ the value in force every 10 seconds (``held``, ``PROBE_STEP``)
202
+ (#216).
203
+
5
204
  Version 9.10.0 (2026-10-22)
6
205
  ===========================
7
206
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.10.0
3
+ Version: 9.12.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
@@ -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),
@@ -142,6 +142,13 @@ pass it with ``--sources`` (or set ``NWP500_SOURCES``):
142
142
  circulation_speed = "sensor.circulation_pump_speed"
143
143
  weather = "weather.forecast_home"
144
144
 
145
+ [home_assistant.heater] # optional: the heater's own entities
146
+ heard = "sensor.water_heater_last_status"
147
+ upper = "sensor.water_heater_upper_tank_temperature"
148
+ lower = "sensor.water_heater_lower_tank_temperature"
149
+ compressor = "binary_sensor.water_heater_compressor"
150
+ ambient = "sensor.water_heater_ambient_temperature"
151
+
145
152
  [location_air] # optional: air = damping x outdoor(t - lag) + offset
146
153
  damping = 0.638
147
154
  lag_hours = 2.0
@@ -159,16 +166,24 @@ give:
159
166
 
160
167
  - **the air:** the hourly forecast projected through the
161
168
  ``[location_air]`` model, shifted to pass through a reading beside the
162
- heater that is at most an hour old. Without a model, that reading is
163
- held over the span; without a fresh one, the projection is
164
- unshifted. Fit a model for your own heater with
165
- ``sim.location_air.fit``;
169
+ heater that is at most an hour old; without a fresh one, the
170
+ projection is unshifted. Without a ``[location_air]`` model, one is
171
+ fitted on the last 3 days of the ``air`` entity's readings against
172
+ the ``weather`` entity's outdoor temperature. Without an ``air``
173
+ entity, or one that gives no fit, the ``[home_assistant.heater]``
174
+ table's ``ambient`` reading while its ``compressor`` runs is fitted
175
+ instead, and the projection is shifted to pass through its newest
176
+ such reading. It is used when the
177
+ readings span a day and follow the outdoor air at r 0.5 or more; a
178
+ note gives the fit. Otherwise, and without a ``weather`` entity, the
179
+ reading beside the heater is held over the span;
166
180
  - **the humidity:** a reading at most an hour old;
167
181
  - **the inlet:** the plateau of the long draws (11 minutes or more,
168
182
  circulation off) in the last 72 hours, from the inlet probe and the
169
183
  ``demand`` entity: a binary sensor that is on while the household
170
184
  draws hot water, with the circulation's water not among its draws. A
171
- circulation pump with no speed reading leaves no estimate.
185
+ circulation pump with no speed reading leaves no estimate, and a draw
186
+ before its first reading is skipped.
172
187
 
173
188
  A reading's age is from when Home Assistant last heard the sensor
174
189
  report (``last_reported``). Over the REST API, Home Assistant serves
@@ -182,6 +197,41 @@ in rpm or Hz. A part that can't be read, or is stale, keeps its
182
197
  default, and a warning on stderr says why. ``--ambient``, ``--inlet``
183
198
  and ``--environment`` override what the sources give.
184
199
 
200
+ From Python, ``sim_home_assistant.HomeAssistantEnvironment`` reads the
201
+ same parts. Given ``heater=``, a ``HeaterEntities`` with ``ambient`` and
202
+ ``compressor``, it fits the model without an ``air`` entity: on the
203
+ heater's own air reading every 10 minutes while the compressor runs,
204
+ where the reading was taken 2 minutes or more into the run (earlier, or
205
+ idle, it reads the cabinet), and not while the sensor was unavailable.
206
+ With a model, given or fitted, and no fresh reading beside the heater,
207
+ each ``read`` anchors the projection to the heater's newest such
208
+ reading from the last hour. ``fitted_air`` holds the fit once ``read``
209
+ has run.
210
+
211
+ A caller that builds a new environment every few minutes needn't refit
212
+ each time. Fit on your own cadence, and pass the fit on:
213
+
214
+ .. code-block:: python
215
+
216
+ fitted, notes = await ha.fit_location_air(
217
+ client, entities, now=now, heater=heater_entities
218
+ ) # hourly, say; None (and notes why) without enough history
219
+
220
+ environment = ha.HomeAssistantEnvironment(
221
+ client, entities, start=now, until=now + horizon, now=now,
222
+ heater=heater_entities, fitted_air=fitted,
223
+ )
224
+
225
+ Given ``fitted_air``, the environment projects through it, notes its r
226
+ and lag on every ``read``, and doesn't fit again.
227
+
228
+ The fit, the samples of the heater's air and the inlet's plateau are
229
+ CPU work: the fit alone takes about 100 ms on 3 days of 30-second
230
+ readings. To keep it off an event loop, pass ``run=``, a callable that
231
+ runs a function and awaits its result, to ``fit_location_air`` and
232
+ ``HomeAssistantEnvironment``. In Home Assistant that is
233
+ ``hass.async_add_executor_job``. Without it, the work runs inline.
234
+
185
235
 
186
236
  Fetch what the heater reports
187
237
  =============================
@@ -214,7 +264,10 @@ A scenario starts from four things the library already reads:
214
264
  heater=sim_live.LiveHeaterSource(mqtt, device),
215
265
  schedules=sim_live.LiveScheduleSource(mqtt, api, device),
216
266
  )
217
- return sim.simulate(composed.scenario, composed.params)
267
+ # Without the feature info the tank size is unknown: the 65 gal
268
+ # defaults stand in.
269
+ params = composed.params or sim.default_params()
270
+ return sim.simulate(composed.scenario, params)
218
271
 
219
272
  ``environment=`` and ``loads=`` take any source with an ``async
220
273
  read()``; ``sim_live.Given(value)`` is one for a value in hand. Without
@@ -222,7 +275,7 @@ read()``; ``sim_live.Given(value)`` is one for a value in hand. Without
222
275
  gets no reply raises ``sim_live.LiveReadError``; so does the schedule
223
276
  source when TOU is on and its periods cannot be read. A missing
224
277
  feature info leaves ``composed.params`` None (the tank size is
225
- unknown). Change a part with ``composed.scenario.model_copy(update=...)``.
278
+ unknown), so the example falls back to ``sim.default_params()``. Change a part with ``composed.scenario.model_copy(update=...)``.
226
279
 
227
280
  With the status, the feature info, the reservations and the TOU
228
281
  periods already read some other way, build the same parts from them:
@@ -875,14 +928,106 @@ with the instants the heater was heard from:
875
928
  params,
876
929
  at=now,
877
930
  flow=draws_gpm, # gal/min, whatever the preferred units
931
+ flow_outages=flow_down, # when the flow entity was unavailable
932
+ circulation=runs, # [(start, end), ...], if you circulate
933
+ circulation_flow=pump_flow, # the loop's gal/min while it runs
878
934
  )
879
935
 
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.
936
+ ``flow`` is the household's draws. If your meter also carries the
937
+ circulation's water, separate the household's draws from it yourself
938
+ and pass those, with ``CirculationParams.meter_share`` 0, so the book
939
+ takes nothing off them again. Clean up the loop's flow before passing it
940
+ too (drop readings below the pump's floor, or one taken mid-ramp): while
941
+ a run is on, each step takes the last reading in force (a run before
942
+ any reading takes ``CirculationParams.flow_gpm``).
943
+
944
+ Like the history's rows, its ``flow``, ``circulation_flow``, ``inlet``
945
+ and ``air`` are changes: each value holds until the next, so a steady
946
+ draw is one row and an hour at 0 gal/min none. They are gal/min and °F,
947
+ whatever the preferred units. A span the heater wasn't heard from (an
948
+ outage, or a silence much longer than its usual reporting), or the flow
949
+ was unknown (``flow_outages``, from the history's ``outages``), breaks
950
+ the book: it is walked only from an anchor after the last one, and is
951
+ None while the heater isn't heard now. A record that keeps no instant
952
+ for each of the heater's responses passes ``heard=``, the spans the
953
+ heater was heard over, and an empty ``observed``: a gap between two
954
+ spans breaks the book the same way, and the heater is heard now only
955
+ inside a span. With no flow reading in force
956
+ at the start of the window it books over, it raises ``ValueError``.
957
+ An inlet reading holds except through ``inlet_outages``.
958
+
959
+ To keep a book current as you walk every few minutes, keep the history
960
+ rather than the book. Read only what is new since your last read,
961
+ extend what you hold with it, trim it to the book's window, and book
962
+ again from memory:
963
+
964
+ .. code-block:: python
965
+
966
+ newer = await ha.read_heater_history(client, entities, last_read, now)
967
+ history = history.extended(newer).since(now - timedelta(hours=72))
968
+ flows = flows.extended(
969
+ await client.read_history([FLOW], last_read, now)
970
+ ).since(now - timedelta(hours=72))
971
+ last_read = now
972
+
973
+ ``HeaterHistory`` and ``History`` both take ``extended`` and ``since``.
974
+ A read opens with the value in force at its start; as each value holds
975
+ until the next change, that row changes nothing. Where both reads have
976
+ a row at one instant, the later read's is kept. ``since`` keeps each
977
+ value in force at the cut. 72 hours is ``BookParams.history_h``, the
978
+ oldest anchor a book reads.
979
+
980
+ ``sim_home_assistant.HomeAssistantBook`` does all of that from Home
981
+ Assistant's entities:
982
+
983
+ .. code-block:: python
984
+
985
+ keeper = ha.HomeAssistantBook(
986
+ client,
987
+ heater_entities, # ha.HeaterEntities
988
+ "sensor.hot_water_draw_flow", # the household's draws
989
+ ha.Entities(
990
+ circulation_speed="sensor.circulation_pump_speed",
991
+ inlet="sensor.cold_supply_temperature",
992
+ demand="binary_sensor.hot_water_demand",
993
+ ),
994
+ )
995
+
996
+ await keeper.read(now) # all 72 h the first time, then what's new
997
+ answer = await hass.async_add_executor_job(keeper.book, now, params)
998
+ readiness = sim.time_to_ready(
999
+ scenario, params, book=answer.booked,
1000
+ inlet_measured=answer.inlet_measured,
1001
+ )
1002
+
1003
+ ``read`` reads the history since the last read and holds it; a read
1004
+ that fails is noted, and the next one reads from where it would have
1005
+ started. Until then, ``book`` takes the flow as unknown since the last
1006
+ read that worked, rather than holding its last value, and after a
1007
+ failed read of the heater it gives no book. An optional
1008
+ entity whose unit can't be read is left out of the book. With no air
1009
+ entity, or one that gives no reading, the book takes the heater's own
1010
+ air reading (``HeaterEntities.ambient``) once its compressor has run 5
1011
+ minutes, and holds it until the next run, and a note says so. Idle, that
1012
+ sensor reads the cabinet, about 6 °F warm, so an idle reading is never
1013
+ taken. Before the first run, and while an air entity that gives readings
1014
+ is unavailable or reads in a unit the book doesn't read, the book takes
1015
+ the default air (65 °F). ``book`` books from what it holds, with no
1016
+ request, so it can run in an executor. ``answer.notes`` say what the book rests on, or why
1017
+ there is none.
1018
+
1019
+ The flow is the household's hot water draws, without the circulation's
1020
+ water, in gal/min or any unit ``to_gpm`` reads. It is the water
1021
+ delivered at the taps, through the mixing valve: the book prices a
1022
+ delivered gallon at the valve's delivery line
1023
+ (``DrawParams.delivery_intercept_f`` and ``delivery_slope``), not as
1024
+ water withdrawn from the tank. As none of the flow is circulation,
1025
+ ``book`` takes ``CirculationParams.meter_share`` as 0. A flow entity that reports a draw late
1026
+ reaches the book late: the book holds what the history holds at
1027
+ ``read``. Each of the other entities is optional:
1028
+ ``circulation_speed`` gives the circulation's runs, ``inlet`` the inlet
1029
+ the book converts at, ``inlet`` with ``demand`` the mains, and ``air``
1030
+ the air beside the heater.
886
1031
 
887
1032
  Without a meter, start one only when you know the tank is full and
888
1033
  quiet (``sa.full_tank_book``).
@@ -1023,15 +1168,18 @@ to a day apart. Each pair's booked heat is checked against the probes':
1023
1168
  circulation=runs, circulation_flow=pump_flow, params=params,
1024
1169
  )
1025
1170
  book = calibration.RecordedBook(steps, p, params.circulation, start=since)
1026
- fit = calibration.fit_book_terms(book, book.reanchors(), p)
1171
+ fit = calibration.fit_book_terms(book, book.reanchors(), p, circuit=True)
1027
1172
  p = replace(p, hp_btu_per_h=fit.hp_btu_per_h,
1028
1173
  hp_btu_per_h_per_f=fit.hp_btu_per_h_per_f,
1029
1174
  ua_btu_per_h_f=fit.ua_btu_per_h_f)
1030
- bands = calibration.book_error_bands(book, p, fit.loss_btu_per_gal)
1031
- empty, crossings = calibration.empty_point(book, p, fit.loss_btu_per_gal)
1032
- zone = calibration.zone_empty_point(book, p, fit.loss_btu_per_gal)
1033
-
1034
- ``book_error_btu(book, p, fit.loss_btu_per_gal)`` gives the book's
1175
+ v = fit.book_circuit_gal
1176
+ bands = calibration.book_error_bands(book, p, 0.0, v)
1177
+ empty, crossings = calibration.empty_point(book, p, 0.0, circuit_gal=v)
1178
+ zone = calibration.zone_empty_point(book, p, 0.0, circuit_gal=v)
1179
+
1180
+ With ``circuit=True`` the runs are priced at a circuit volume
1181
+ (``CirculationParams.book_circuit_gal``) fitted in place of a loss per
1182
+ gallon. ``book_error_btu(book, p, 0.0, v)`` gives the book's
1035
1183
  error bands: the 90th percentile of its error pooled over re-anchors an
1036
1184
  hour to ``max_anchor_age_h`` apart, and the 18 to 24 h band's past it,
1037
1185
  each rounded up to 100 BTU. ``cold_lower_cap(draws,
@@ -154,7 +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
+ fit_terms_l1, BOOK_TERMS, BOOK_TERM_BOUNDS, BOOK_CIRCUIT_GAL_BOUNDS,
158
158
  book_error_bands, book_error_btu, empty_point,
159
159
  zone_empty_point, withheld_btu, book_walk, long_draws,
160
160
  front_frame, fit_front, FrontFit, settle_tails, fit_front_settle,
@@ -305,17 +305,22 @@ Recorded history
305
305
  fit_book, BookTermsFit, MIN_BOOK_PAIRS, MIN_RUN_PAIRS,
306
306
  score, Judged, HeldOut, GROUPS, air_unread_note,
307
307
  CalibrationInputError, DEFAULT_HUMIDITY_PCT,
308
- DEFAULT_BASE_NOTE, NO_AIR_NOTE
308
+ DEFAULT_BASE_NOTE, NO_AIR_NOTE, HEATER_AIR_BRACKET
309
309
 
310
310
 
311
311
  Home Assistant sources
312
312
  ======================
313
313
 
314
314
  .. automodule:: nwp500.sim_home_assistant
315
- :members: HomeAssistantEnvironment, HomeAssistantClient, record_inputs,
316
- RecordInputs, INLET_LOOKBACK, Sources, Entities, sources_from, State, parse_state,
315
+ :members: HomeAssistantEnvironment, HomeAssistantClient, fit_location_air,
316
+ Run, inline, HomeAssistantBook, BookAnswer, BOOK_WINDOW,
317
+ mains_plateau, known_runs,
318
+ record_inputs,
319
+ RecordInputs, INLET_LOOKBACK, PROBE_STEP, held, LOCATION_AIR_WINDOW,
320
+ LOCATION_AIR_MIN_SPAN, LOCATION_AIR_MIN_R, RUN_SETTLE, Sources,
321
+ Entities, sources_from, State, parse_state,
317
322
  parse_history, parse_outages, parse_states, History, history_for,
318
- parse_forecast, read_heater_history, HeaterEntities, HeaterHistory,
323
+ parse_forecast, parse_outdoor_history, read_heater_history, HeaterEntities, HeaterHistory,
319
324
  HISTORY_WINDOW, to_w, to_wh,
320
325
  pump_runs, demand_spans, busy, covers_start, percent,
321
326
  fresh, to_f, to_gpm, to_gal, to_rpm, HomeAssistantError