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