nwp500-python 9.7.0__tar.gz → 9.8.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 (344) hide show
  1. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/CHANGELOG.rst +38 -0
  2. {nwp500_python-9.7.0/src/nwp500_python.egg-info → nwp500_python-9.8.0}/PKG-INFO +1 -1
  3. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/calibrate-simulator.rst +44 -0
  4. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/sim.rst +4 -2
  5. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/calibration.py +205 -111
  6. nwp500_python-9.8.0/src/nwp500/sim_calibrate.py +1202 -0
  7. {nwp500_python-9.7.0 → nwp500_python-9.8.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  8. nwp500_python-9.8.0/src/nwp500_python.egg-info/scm_version.json +8 -0
  9. nwp500_python-9.8.0/tests/test_sim_calibrate.py +1380 -0
  10. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_change_history.py +45 -0
  11. nwp500_python-9.7.0/src/nwp500/sim_calibrate.py +0 -517
  12. nwp500_python-9.7.0/src/nwp500_python.egg-info/scm_version.json +0 -8
  13. nwp500_python-9.7.0/tests/test_sim_calibrate.py +0 -608
  14. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.coveragerc +0 -0
  15. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  16. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.github/copilot-instructions.md +0 -0
  17. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.github/workflows/ci.yml +0 -0
  18. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.github/workflows/release.yml +0 -0
  19. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.gitignore +0 -0
  20. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.pre-commit-config.yaml +0 -0
  21. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.readthedocs.yml +0 -0
  22. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/AUTHORS.rst +0 -0
  23. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/CONTRIBUTING.rst +0 -0
  24. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/LICENSE.txt +0 -0
  25. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/Makefile +0 -0
  26. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/README.rst +0 -0
  27. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/RELEASE.md +0 -0
  28. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/Makefile +0 -0
  29. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/_static/.gitignore +0 -0
  30. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/conf.py +0 -0
  31. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/architecture.rst +0 -0
  32. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/draws.rst +0 -0
  33. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/heating-elements.rst +0 -0
  34. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/index.rst +0 -0
  35. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/recovery-speed.rst +0 -0
  36. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/simulator.rst +0 -0
  37. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/standby-loss.rst +0 -0
  38. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/tank-energy.rst +0 -0
  39. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/tou-recovery-cap.rst +0 -0
  40. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/unknown-values.rst +0 -0
  41. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/what-starts-a-recovery.rst +0 -0
  42. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/authenticate.rst +0 -0
  43. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/auto-recovery.rst +0 -0
  44. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/bluetooth.rst +0 -0
  45. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/diagnose-mqtt.rst +0 -0
  46. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/home-assistant.rst +0 -0
  47. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/index.rst +0 -0
  48. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/maintenance.rst +0 -0
  49. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/manage-units.rst +0 -0
  50. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/monitor-status.rst +0 -0
  51. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/optimize-tou.rst +0 -0
  52. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/queue-commands.rst +0 -0
  53. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/schedule-operation.rst +0 -0
  54. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/simulate-heater.rst +0 -0
  55. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/track-energy.rst +0 -0
  56. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/index.rst +0 -0
  57. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/openapi.yaml +0 -0
  58. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/project/authors.rst +0 -0
  59. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/project/changelog.rst +0 -0
  60. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/project/contributing.rst +0 -0
  61. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/project/history.rst +0 -0
  62. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/project/license.rst +0 -0
  63. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/configuration.rst +0 -0
  64. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/enumerations.rst +0 -0
  65. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/index.rst +0 -0
  66. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/installation.rst +0 -0
  67. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/data_conversions.rst +0 -0
  68. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/device_features.rst +0 -0
  69. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/device_status.rst +0 -0
  70. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/error_codes.rst +0 -0
  71. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  72. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/quick_reference.rst +0 -0
  73. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/rest_api.rst +0 -0
  74. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/api_client.rst +0 -0
  75. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/auth_client.rst +0 -0
  76. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/cli.rst +0 -0
  77. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/events.rst +0 -0
  78. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/exceptions.rst +0 -0
  79. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/models.rst +0 -0
  80. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/mqtt_client.rst +0 -0
  81. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/simulator-inputs.rst +0 -0
  82. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/requirements.txt +0 -0
  83. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/tutorials/getting-started.rst +0 -0
  84. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/.ruff.toml +0 -0
  85. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/README.md +0 -0
  86. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/air_filter_reset.py +0 -0
  87. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/anti_legionella.py +0 -0
  88. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/auto_recovery.py +0 -0
  89. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/combined_callbacks.py +0 -0
  90. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/demand_response.py +0 -0
  91. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/device_capabilities.py +0 -0
  92. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/device_status_debug.py +0 -0
  93. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/energy_analytics.py +0 -0
  94. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/error_code_demo.py +0 -0
  95. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/firmware_payload_capture.py +0 -0
  96. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/mqtt_diagnostics.py +0 -0
  97. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/power_control.py +0 -0
  98. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/recirculation_control.py +0 -0
  99. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/reconnection_demo.py +0 -0
  100. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/reservation_schedule.py +0 -0
  101. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/simple_auto_recovery.py +0 -0
  102. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/token_restoration.py +0 -0
  103. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/tou_openei.py +0 -0
  104. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/tou_schedule.py +0 -0
  105. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/water_reservation.py +0 -0
  106. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/beginner/01_authentication.py +0 -0
  107. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/beginner/02_list_devices.py +0 -0
  108. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/beginner/03_get_status.py +0 -0
  109. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/beginner/04_set_temperature.py +0 -0
  110. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
  111. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/command_queue.py +0 -0
  112. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/device_status_callback.py +0 -0
  113. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/error_handling.py +0 -0
  114. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/event_driven_control.py +0 -0
  115. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/improved_auth.py +0 -0
  116. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
  117. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  118. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/periodic_requests.py +0 -0
  119. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/set_mode.py +0 -0
  120. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/vacation_mode.py +0 -0
  121. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/mask.py +0 -0
  122. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/simulator/lower-setpoint.json +0 -0
  123. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/simulator/plan.json +0 -0
  124. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/simulator/scenario.json +0 -0
  125. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/testing/periodic_device_info.py +0 -0
  126. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/testing/simple_periodic_info.py +0 -0
  127. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/testing/test_api_client.py +0 -0
  128. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/testing/test_mqtt_connection.py +0 -0
  129. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/testing/test_mqtt_messaging.py +0 -0
  130. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/testing/test_periodic_minimal.py +0 -0
  131. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/pyproject.toml +0 -0
  132. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/README.md +0 -0
  133. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/bump_version.py +0 -0
  134. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/diagnose_mqtt_connection.py +0 -0
  135. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/extract_changelog.py +0 -0
  136. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/format.py +0 -0
  137. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/lint.py +0 -0
  138. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/setup-dev.py +0 -0
  139. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/validate_version.py +0 -0
  140. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/setup.cfg +0 -0
  141. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/setup.py +0 -0
  142. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/__init__.py +0 -0
  143. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/_base.py +0 -0
  144. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/api_client.py +0 -0
  145. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/auth.py +0 -0
  146. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/ble/__init__.py +0 -0
  147. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/ble/__main__.py +0 -0
  148. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/ble/client.py +0 -0
  149. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/ble/mqtt_adapter.py +0 -0
  150. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/ble/protocol.py +0 -0
  151. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/__init__.py +0 -0
  152. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/__main__.py +0 -0
  153. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/ble_backend.py +0 -0
  154. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/calibration.py +0 -0
  155. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/handlers.py +0 -0
  156. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/monitoring.py +0 -0
  157. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/output_formatters.py +0 -0
  158. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/presentation.py +0 -0
  159. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/recording.py +0 -0
  160. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/rich_output.py +0 -0
  161. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/simulate.py +0 -0
  162. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/token_storage.py +0 -0
  163. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/command_decorators.py +0 -0
  164. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/config.py +0 -0
  165. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/converters.py +0 -0
  166. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/device_capabilities.py +0 -0
  167. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/device_info_cache.py +0 -0
  168. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/encoding.py +0 -0
  169. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/enums.py +0 -0
  170. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/events.py +0 -0
  171. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/exceptions.py +0 -0
  172. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/factory.py +0 -0
  173. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/field_factory.py +0 -0
  174. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/__init__.py +0 -0
  175. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/_converters.py +0 -0
  176. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/account.py +0 -0
  177. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/device.py +0 -0
  178. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/diagnostics.py +0 -0
  179. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/energy.py +0 -0
  180. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/feature.py +0 -0
  181. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/firmware.py +0 -0
  182. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/maintenance.py +0 -0
  183. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/mqtt_models.py +0 -0
  184. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/schedule.py +0 -0
  185. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/status.py +0 -0
  186. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/tou.py +0 -0
  187. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/__init__.py +0 -0
  188. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/_control_commands.py +0 -0
  189. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
  190. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/client.py +0 -0
  191. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/command_queue.py +0 -0
  192. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/connection.py +0 -0
  193. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/control.py +0 -0
  194. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/diagnostics.py +0 -0
  195. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/periodic.py +0 -0
  196. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/reconnection.py +0 -0
  197. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/state_tracker.py +0 -0
  198. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/subscriptions.py +0 -0
  199. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/types.py +0 -0
  200. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/utils.py +0 -0
  201. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt_events.py +0 -0
  202. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/openei.py +0 -0
  203. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/py.typed +0 -0
  204. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/reservations.py +0 -0
  205. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/__init__.py +0 -0
  206. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/api.py +0 -0
  207. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/attribution.py +0 -0
  208. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/book.py +0 -0
  209. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/circulation.py +0 -0
  210. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/codec.py +0 -0
  211. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/cold_supply.py +0 -0
  212. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/controller.py +0 -0
  213. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/decay.py +0 -0
  214. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/defaults.py +0 -0
  215. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/delivery.py +0 -0
  216. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/draw_forecast.py +0 -0
  217. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/engine.py +0 -0
  218. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/extensions.py +0 -0
  219. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/heat_pump_recovery.py +0 -0
  220. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/heating_trajectory.py +0 -0
  221. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/inputs.py +0 -0
  222. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/location_air.py +0 -0
  223. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/meter.py +0 -0
  224. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/observed.py +0 -0
  225. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/params.py +0 -0
  226. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/physics/__init__.py +0 -0
  227. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/physics/compressor.py +0 -0
  228. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/physics/draw.py +0 -0
  229. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/physics/element.py +0 -0
  230. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/physics/refrigerant.py +0 -0
  231. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/physics/tank.py +0 -0
  232. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/profile.py +0 -0
  233. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/ready.py +0 -0
  234. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/record.py +0 -0
  235. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/result.py +0 -0
  236. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/schedules.py +0 -0
  237. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/tank_physics.py +0 -0
  238. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/tou_triggers.py +0 -0
  239. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/units.py +0 -0
  240. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim_adapters.py +0 -0
  241. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim_home_assistant.py +0 -0
  242. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim_live.py +0 -0
  243. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/status_history.py +0 -0
  244. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/temperature.py +0 -0
  245. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/topic_builder.py +0 -0
  246. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/tou_schedule.py +0 -0
  247. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/unit_system.py +0 -0
  248. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/utils.py +0 -0
  249. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/SOURCES.txt +0 -0
  250. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  251. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  252. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  253. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/requires.txt +0 -0
  254. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/scm_file_list.json +0 -0
  255. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
  256. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/_sim_site.py +0 -0
  257. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/conftest.py +0 -0
  258. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/fixtures/sim/engine_golden.json +0 -0
  259. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/fixtures/sim/heater_params.json +0 -0
  260. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/fixtures/sim/heater_params_fitted.json +0 -0
  261. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_api_endpoints_201.py +0 -0
  262. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_api_helpers.py +0 -0
  263. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_app_queries.py +0 -0
  264. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_auth.py +0 -0
  265. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_auth_session_lifecycle.py +0 -0
  266. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_auth_session_refresh.py +0 -0
  267. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_ble_mqtt_adapter.py +0 -0
  268. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_ble_protocol.py +0 -0
  269. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_bug_fixes.py +0 -0
  270. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_canonical_schedule.py +0 -0
  271. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_cli_basic.py +0 -0
  272. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_cli_calibrate.py +0 -0
  273. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_cli_commands.py +0 -0
  274. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_cli_simulate.py +0 -0
  275. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_cli_tristate_presentation.py +0 -0
  276. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_command_decorators.py +0 -0
  277. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_command_queue.py +0 -0
  278. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_device_capabilities.py +0 -0
  279. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_device_info_cache.py +0 -0
  280. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_device_rest_models.py +0 -0
  281. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_events.py +0 -0
  282. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_exceptions.py +0 -0
  283. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_model_converters.py +0 -0
  284. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_models.py +0 -0
  285. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_clean_session_resume.py +0 -0
  286. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_client_init.py +0 -0
  287. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_control_fail.py +0 -0
  288. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_events.py +0 -0
  289. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_hypothesis.py +0 -0
  290. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_last_will.py +0 -0
  291. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_reconnection.py +0 -0
  292. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_reconnection_storm.py +0 -0
  293. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_reliability.py +0 -0
  294. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_multi_device.py +0 -0
  295. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_navilink_201_enums.py +0 -0
  296. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_openei.py +0 -0
  297. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_protocol_correctness.py +0 -0
  298. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_public_api.py +0 -0
  299. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_reservation_validation.py +0 -0
  300. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_reservations.py +0 -0
  301. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_adapters.py +0 -0
  302. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_api.py +0 -0
  303. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_book.py +0 -0
  304. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_book_history.py +0 -0
  305. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_calibration.py +0 -0
  306. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_circulation.py +0 -0
  307. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_codec.py +0 -0
  308. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_cold_supply.py +0 -0
  309. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_controller.py +0 -0
  310. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_defaults.py +0 -0
  311. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_draw_forecast.py +0 -0
  312. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_draw_loss.py +0 -0
  313. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_element.py +0 -0
  314. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_element_fit.py +0 -0
  315. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_forecast.py +0 -0
  316. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_golden.py +0 -0
  317. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_home_assistant.py +0 -0
  318. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_hot_water.py +0 -0
  319. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_import_contract.py +0 -0
  320. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_live.py +0 -0
  321. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_location_air.py +0 -0
  322. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_meter.py +0 -0
  323. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_no_clock_no_state.py +0 -0
  324. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_observed.py +0 -0
  325. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_profile.py +0 -0
  326. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_readiness.py +0 -0
  327. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_readiness_fit.py +0 -0
  328. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_ready.py +0 -0
  329. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_ready_api.py +0 -0
  330. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_replay.py +0 -0
  331. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_schedules.py +0 -0
  332. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_tank_physics.py +0 -0
  333. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_status_history.py +0 -0
  334. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_temperature_converters.py +0 -0
  335. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_threading_model.py +0 -0
  336. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_token_storage.py +0 -0
  337. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_tou_api.py +0 -0
  338. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_tou_schedule.py +0 -0
  339. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_unit_switching.py +0 -0
  340. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_unit_system_process_wide.py +0 -0
  341. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_utility_modules.py +0 -0
  342. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_utils.py +0 -0
  343. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tox.ini +0 -0
  344. {nwp500_python-9.7.0 → nwp500_python-9.8.0}/uv.lock +0 -0
@@ -2,6 +2,44 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version 9.8.0 (2026-10-21)
6
+ ==========================
7
+
8
+ Added
9
+ -----
10
+ - **Refine a fit without making it worse** (``keep_better=True`` on
11
+ ``sim_calibrate.calibrate`` and ``calibrate_changes``, #184). Each
12
+ group of terms holds out its latest evidence from its refit (the last
13
+ fifth, once it has 10 items) and keeps the existing values unless the
14
+ refit scores better there, by the error its own fit minimizes. The
15
+ terms in a group are kept or replaced together. ``Calibrated.judged``
16
+ records each judgement, and the report's reasons give both scores.
17
+ ``sim_calibrate.score(done, params)`` scores any parameters on that
18
+ held-out evidence.
19
+ - ``sim.calibration.split_recoveries``, ``recovery_minutes``,
20
+ ``power_cycles`` and ``cop_cycles``: the evidence the compressor and
21
+ power fits use, and their predictions, which ``fit_compressor``,
22
+ ``fit_power_scale`` and ``fit_cop_scale`` now take from.
23
+
24
+ Fixed
25
+ -----
26
+ - **The report names compressor runs without an air temperature.**
27
+ A recovery is timed against the air, so a run with none (no air
28
+ given, and no heater ambient reading while the compressor runs) is
29
+ left out of the recoveries. The report blamed the heater instead:
30
+ "0 recoveries reached their target". ``Calibrated`` now counts the
31
+ runs (``n_runs``, ``n_runs_without_air``), and the compressor term's
32
+ reason and the notes say how many had no air
33
+ (``sim_calibrate.air_unread_note``).
34
+ - ``sim.calibration.with_standby`` quotes the fitted UA at the ΔT the
35
+ law was fitted at (50 °F). It kept the ``ua_ref_delta_t_f`` of the
36
+ parameters it was given, so a refit applied over parameters quoted at
37
+ another ΔT ran as a different loss law.
38
+ - ``sim.calibration.fit_cop_scale`` takes the integrator's COP on the
39
+ heater's own tank (``HeaterParams.tank``), as the simulation does. It
40
+ used the default tank's mass, so a heater with another tank was fitted
41
+ against a COP the simulation does not run.
42
+
5
43
  Version 9.7.0 (2026-10-21)
6
44
  ==========================
7
45
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.7.0
3
+ Version: 9.8.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
@@ -223,6 +223,44 @@ Assistant integration, can calibrate the same way:
223
223
  4. Store ``params_to_document(done.calibration.params)``, and show the
224
224
  ``report(done)`` rows and ``done.notes``.
225
225
 
226
+ Refine an existing fit
227
+ ----------------------
228
+
229
+ A fit made on months of history should not give way to one made on the
230
+ last ten days unless the new one predicts better. Pass the existing
231
+ fit as ``params`` with ``keep_better=True``:
232
+
233
+ .. code-block:: python
234
+
235
+ done = sim_calibrate.calibrate(
236
+ recorded(days), ambient=65.0, params=existing, keep_better=True
237
+ )
238
+ for j in done.judged:
239
+ print(j.group, j.kept, j.base_error, j.refit_error, j.why)
240
+
241
+ Each group of terms holds out its latest evidence from its refit:
242
+
243
+ - the standby loss and its exponent: its quiet windows;
244
+ - the zone gap time constant: its windows that open a gap;
245
+ - the compressor constant and the evaporator approach: its reached
246
+ recoveries;
247
+ - the power scale: the metered compressor cycles it can use;
248
+ - the COP terms: the cycles the COP fit can use (without a meter of
249
+ draws, those no water was drawn around).
250
+
251
+ Once a group has 10 items, the latest fifth (at least 2) is held out.
252
+ The refit replaces the existing values only where it scores better on
253
+ them, by the error its own fit minimizes. With fewer items the existing
254
+ values stay. The terms in a group are kept or replaced together, and the
255
+ report's reasons say which, with both scores.
256
+ ``sim_calibrate.score(done, params)`` gives those errors for any
257
+ parameters.
258
+
259
+ Use it over a fit of this heater. The defaults are another heater's fit,
260
+ and ``keep_better`` keeps them wherever there is too little evidence to
261
+ judge by. A group the existing fit lists as unfitted takes its refit, as
262
+ without ``keep_better``.
263
+
226
264
  ``sim.calibration.calibrate`` underneath takes the frame
227
265
  ``sim_adapters.history`` builds; ``sim_adapters.add_inputs`` adds the
228
266
  air, the humidity, a meter's draws and the circulation's runs to it.
@@ -294,6 +332,12 @@ leave out what your installation does not record. ``read_heater_history``:
294
332
  - notes a value it could not read, and an entity whose history starts
295
333
  after ``start``.
296
334
 
335
+ A recovery is timed against the air, so the compressor and power fits
336
+ need an air temperature over each compressor run: the air beside the
337
+ heater (``record=``, ``air`` or ``ambient``), or the heater's own reading
338
+ (``ambient`` in ``HeaterEntities``). Runs without one are no recovery,
339
+ and the report says how many there were.
340
+
297
341
  Give ``anti_legionella`` where it is recorded. Without it an
298
342
  anti-legionella cycle is not kept out of the recoveries, and the notes
299
343
  say so.
@@ -150,7 +150,8 @@ Hot water and the heat book
150
150
 
151
151
  .. automodule:: nwp500.sim.calibration
152
152
  :members: calibrate, Calibration, fit_standby, StandbyFit, StandbyWindow,
153
- fit_compressor, CompressorFit, clean_draws, CleanDraw, crossing,
153
+ fit_compressor, CompressorFit, split_recoveries, recovery_minutes,
154
+ clean_draws, CleanDraw, crossing,
154
155
  demonstrated, frontier, capacity, held_out, fit_evidence,
155
156
  EvidenceFit, cold_lower_cap, RecordedBook, fit_book_terms, BookFit,
156
157
  book_error_bands, book_error_btu, empty_point,
@@ -163,7 +164,7 @@ Hot water and the heat book
163
164
  element_off_points, element_power, element_grid, element_runs,
164
165
  fit_element_rates, ElementRateFit, day_ci,
165
166
  energy_saver_off_deficit, upper_start_edge, fit_power,
166
- PowerFit, fit_power_scale,
167
+ PowerFit, fit_power_scale, power_cycles, cop_cycles, CopCycle,
167
168
  fit_cop_scale, solve_cop_fit, solve_cop_scale, draw_free_withdrawn,
168
169
  metered_withdrawn, metered_lead,
169
170
  path_to_end, with_power, snapshots, recoveries, quiet_windows,
@@ -299,6 +300,7 @@ Recorded history
299
300
 
300
301
  .. automodule:: nwp500.sim_calibrate
301
302
  :members: calibrate, calibrate_changes, Calibrated, report, Term, max_gap,
303
+ score, Judged, HeldOut, GROUPS, air_unread_note,
302
304
  CalibrationInputError, DEFAULT_HUMIDITY_PCT, GAP_SAMPLES, MIN_GAP,
303
305
  DEFAULT_BASE_NOTE, NO_AIR_NOTE
304
306
 
@@ -449,6 +449,92 @@ class CompressorFit:
449
449
  two_zone: bool
450
450
 
451
451
 
452
+ _RECOVERY_NEED = (
453
+ "t_start_lower",
454
+ "delta_t_lower",
455
+ "t_amb_mean",
456
+ "duration_minutes",
457
+ )
458
+
459
+
460
+ def split_recoveries(
461
+ recoveries: pd.DataFrame,
462
+ ) -> tuple[pd.DataFrame, pd.DataFrame]:
463
+ """``(fitted, held_out)``: the recoveries a compressor fit uses, in
464
+ time order where ``start_time`` says, the latest ``HELDOUT_FRACTION``
465
+ (at least ``MIN_HELDOUT``) held out once there are
466
+ ``MIN_RECOVERIES_FOR_SPLIT``. Usable: the figures known and a
467
+ duration from ``MIN_RECOVERY_MINUTES`` to ``MAX_RECOVERY_MINUTES``."""
468
+ if any(c not in recoveries.columns for c in _RECOVERY_NEED):
469
+ empty = recoveries.iloc[0:0]
470
+ return empty, empty
471
+ df = recoveries.dropna(subset=list(_RECOVERY_NEED))
472
+ minutes = df["duration_minutes"]
473
+ df = df.loc[
474
+ (minutes >= MIN_RECOVERY_MINUTES) & (minutes <= MAX_RECOVERY_MINUTES)
475
+ ].copy()
476
+ if "start_time" in df.columns:
477
+ df = df.sort_values("start_time").reset_index(drop=True)
478
+ n = len(df)
479
+ n_held = (
480
+ max(MIN_HELDOUT, round(n * HELDOUT_FRACTION))
481
+ if n >= MIN_RECOVERIES_FOR_SPLIT
482
+ else 0
483
+ )
484
+ return df.iloc[: n - n_held], df.iloc[n - n_held :]
485
+
486
+
487
+ def recovery_minutes(
488
+ recoveries: pd.DataFrame,
489
+ compressor_constant: float,
490
+ evap_approach_f: float,
491
+ *,
492
+ compressor: CompressorParams,
493
+ loss: LossParams,
494
+ tank: TankParams,
495
+ two_zone: bool,
496
+ gaps: bool,
497
+ ) -> np.ndarray:
498
+ """Each recovery's duration as the physics predicts it, minutes, on
499
+ ``fit_compressor``'s columns; ``gaps`` uses its
500
+ ``stratification_gap``, else none."""
501
+ rows = len(recoveries)
502
+ start = np.asarray(recoveries["t_start_lower"], dtype=float)
503
+ target = start + np.asarray(recoveries["delta_t_lower"], dtype=float)
504
+ air = np.asarray(recoveries["t_amb_mean"], dtype=float)
505
+ humidity = np.asarray(
506
+ recoveries["h_amb_start"]
507
+ if "h_amb_start" in recoveries.columns
508
+ else np.full(rows, 50.0),
509
+ dtype=float,
510
+ )
511
+ gap = np.asarray(
512
+ recoveries["stratification_gap"] if gaps else np.zeros(rows),
513
+ dtype=float,
514
+ )
515
+ return np.array(
516
+ [
517
+ predict_physics_detailed(
518
+ t_start_lower=float(start[i]),
519
+ hp_lower_off=float(target[i]),
520
+ t_amb_mean=float(air[i]),
521
+ compressor_constant=compressor_constant,
522
+ evap_approach_f=evap_approach_f,
523
+ compressor=compressor,
524
+ loss=loss,
525
+ h_amb_start=float(humidity[i]),
526
+ ua_btu_hr_f=loss.ua_btu_hr_f,
527
+ ua_exponent=loss.ua_exponent,
528
+ ua_ref_delta_t=loss.ua_ref_delta_t_f,
529
+ stratification_gap=float(gap[i]),
530
+ two_zone=two_zone,
531
+ tank=tank,
532
+ ).minutes
533
+ for i in range(rows)
534
+ ]
535
+ )
536
+
537
+
452
538
  def fit_compressor(
453
539
  recoveries: pd.DataFrame,
454
540
  compressor: CompressorParams,
@@ -470,27 +556,13 @@ def fit_compressor(
470
556
  ``stratification_gap`` (upper minus lower at the start; without it
471
557
  the fit falls back to the one-zone integrator, so the constant stays
472
558
  the one the simulation will use) and ``start_time`` (to hold out the
473
- latest). None with fewer than ``MIN_RECOVERIES`` usable.
559
+ latest, ``split_recoveries``). None with fewer than
560
+ ``MIN_RECOVERIES`` usable.
474
561
  """
475
- need = ["t_start_lower", "delta_t_lower", "t_amb_mean", "duration_minutes"]
476
- if any(c not in recoveries.columns for c in need):
477
- return None
478
- df = recoveries.dropna(subset=need)
479
- minutes = df["duration_minutes"]
480
- df = df.loc[
481
- (minutes >= MIN_RECOVERY_MINUTES) & (minutes <= MAX_RECOVERY_MINUTES)
482
- ].copy()
483
- if len(df) < MIN_RECOVERIES:
562
+ fit_df, held_df = split_recoveries(recoveries)
563
+ if len(fit_df) + len(held_df) < MIN_RECOVERIES:
484
564
  return None
485
- if "start_time" in df.columns:
486
- df = df.sort_values("start_time").reset_index(drop=True)
487
- n = len(df)
488
- n_held = (
489
- max(MIN_HELDOUT, round(n * HELDOUT_FRACTION))
490
- if n >= MIN_RECOVERIES_FOR_SPLIT
491
- else 0
492
- )
493
- fit_df, held_df = df.iloc[: n - n_held], df.iloc[n - n_held :]
565
+ n_held = len(held_df)
494
566
  # Usable gaps are judged on the rows fitted, as the model's fit did:
495
567
  # a gap the fit has not got cannot be the mass model's.
496
568
  has_gaps = "stratification_gap" in fit_df.columns and bool(
@@ -498,56 +570,21 @@ def fit_compressor(
498
570
  )
499
571
  two_zone = two_zone and has_gaps
500
572
 
501
- def columns(
502
- frame: pd.DataFrame, gaps: bool = has_gaps
503
- ) -> tuple[np.ndarray, ...]:
504
- rows = len(frame)
505
- return (
506
- np.asarray(frame["t_start_lower"], dtype=float),
507
- np.asarray(
508
- frame["t_start_lower"] + frame["delta_t_lower"], dtype=float
509
- ),
510
- np.asarray(frame["t_amb_mean"], dtype=float),
511
- np.asarray(
512
- frame["h_amb_start"]
513
- if "h_amb_start" in frame.columns
514
- else np.full(rows, 50.0),
515
- dtype=float,
516
- ),
517
- np.asarray(
518
- frame["stratification_gap"] if gaps else np.zeros(rows),
519
- dtype=float,
520
- ),
521
- np.asarray(frame["duration_minutes"], dtype=float),
522
- )
523
-
524
573
  def predicted(
525
- cols: tuple[np.ndarray, ...], k: float, approach: float
574
+ frame: pd.DataFrame, k: float, approach: float, gaps: bool = has_gaps
526
575
  ) -> np.ndarray:
527
- start, target, air, humidity, gap, _ = cols
528
- return np.array(
529
- [
530
- predict_physics_detailed(
531
- t_start_lower=float(start[i]),
532
- hp_lower_off=float(target[i]),
533
- t_amb_mean=float(air[i]),
534
- compressor_constant=k,
535
- evap_approach_f=approach,
536
- compressor=compressor,
537
- loss=loss,
538
- h_amb_start=float(humidity[i]),
539
- ua_btu_hr_f=loss.ua_btu_hr_f,
540
- ua_exponent=loss.ua_exponent,
541
- ua_ref_delta_t=loss.ua_ref_delta_t_f,
542
- stratification_gap=float(gap[i]),
543
- two_zone=two_zone,
544
- tank=tank,
545
- ).minutes
546
- for i in range(len(start))
547
- ]
576
+ return recovery_minutes(
577
+ frame,
578
+ k,
579
+ approach,
580
+ compressor=compressor,
581
+ loss=loss,
582
+ tank=tank,
583
+ two_zone=two_zone,
584
+ gaps=gaps,
548
585
  )
549
586
 
550
- fit_cols = columns(fit_df)
587
+ actual = np.asarray(fit_df["duration_minutes"], dtype=float)
551
588
 
552
589
  def objective(x: np.ndarray) -> float:
553
590
  k, approach = float(x[0]), float(x[1])
@@ -556,9 +593,7 @@ def fit_compressor(
556
593
  and APPROACH_BOUNDS[0] <= approach <= APPROACH_BOUNDS[1]
557
594
  ):
558
595
  return 1e12
559
- return float(
560
- np.sum((predicted(fit_cols, k, approach) - fit_cols[5]) ** 2)
561
- )
596
+ return float(np.sum((predicted(fit_df, k, approach) - actual) ** 2))
562
597
 
563
598
  found = minimize(
564
599
  objective,
@@ -573,15 +608,22 @@ def fit_compressor(
573
608
  for value, bounds in ((k, K_BOUNDS), (approach, APPROACH_BOUNDS))
574
609
  for bound in bounds
575
610
  )
576
- mae = float(np.mean(np.abs(predicted(fit_cols, k, approach) - fit_cols[5])))
611
+ mae = float(np.mean(np.abs(predicted(fit_df, k, approach) - actual)))
577
612
  held_mae = None
578
613
  if n_held:
579
614
  # The held-out rows' gaps as recorded, wherever the column is.
580
- held_cols = columns(
581
- held_df, gaps="stratification_gap" in held_df.columns
582
- )
583
615
  held_mae = float(
584
- np.mean(np.abs(predicted(held_cols, k, approach) - held_cols[5]))
616
+ np.mean(
617
+ np.abs(
618
+ predicted(
619
+ held_df,
620
+ k,
621
+ approach,
622
+ gaps="stratification_gap" in held_df.columns,
623
+ )
624
+ - np.asarray(held_df["duration_minutes"], dtype=float)
625
+ )
626
+ )
585
627
  )
586
628
  return CompressorFit(
587
629
  compressor_constant=k,
@@ -954,8 +996,13 @@ def with_standby(params: HeaterParams, fit: StandbyFit) -> HeaterParams:
954
996
  """``params`` with the standby fit's values where it had them."""
955
997
  loss, tank = params.loss, params.tank
956
998
  if fit.ua_ref is not None and fit.ua_exponent is not None:
999
+ # Quoted at the ΔT ``fit_standby``'s law was fitted at, not
1000
+ # whatever ``params`` held.
957
1001
  loss = replace(
958
- loss, ua_btu_hr_f=fit.ua_ref, ua_exponent=fit.ua_exponent
1002
+ loss,
1003
+ ua_btu_hr_f=fit.ua_ref,
1004
+ ua_exponent=fit.ua_exponent,
1005
+ ua_ref_delta_t_f=UA_REF_DELTA_T_F,
959
1006
  )
960
1007
  if fit.tau_gap_hours is not None:
961
1008
  tank = replace(tank, stratification_gap_tau_h=fit.tau_gap_hours)
@@ -1060,15 +1107,14 @@ class PowerFit:
1060
1107
  n_cop: int
1061
1108
 
1062
1109
 
1063
- def fit_power_scale(
1110
+ def power_cycles(
1064
1111
  rows: pd.DataFrame, params: HeaterParams
1065
- ) -> tuple[float | None, int]:
1066
- """``(metered / modelled, cycles)``: each cycle charged at the
1067
- integrator's own mean power for its real duration, so both sides
1068
- are energy over the same clock. ``(None, n)`` with fewer than
1069
- ``MIN_POWER_EVENTS``."""
1070
- metered_total = modelled_total = 0.0
1071
- n = 0
1112
+ ) -> list[tuple[datetime | None, float, float]]:
1113
+ """Each cycle's ``(start, metered, modelled)`` Wh for the power scale
1114
+ (``start`` None where the rows carry no ``start_time``):
1115
+ the integrator's own mean power over the cycle's real duration, so
1116
+ both sides are energy over the same clock."""
1117
+ out: list[tuple[datetime | None, float, float]] = []
1072
1118
  for row in _records(_starts(rows)):
1073
1119
  raw = row["metered_input_wh"]
1074
1120
  if raw is None or pd.isna(raw):
@@ -1085,9 +1131,19 @@ def fit_power_scale(
1085
1131
  modelled = power_w * real_minutes / 60
1086
1132
  if not math.isfinite(modelled) or modelled <= 0.0:
1087
1133
  continue
1088
- metered_total += metered
1089
- modelled_total += modelled
1090
- n += 1
1134
+ out.append((row.get("start_time"), metered, modelled))
1135
+ return out
1136
+
1137
+
1138
+ def fit_power_scale(
1139
+ rows: pd.DataFrame, params: HeaterParams
1140
+ ) -> tuple[float | None, int]:
1141
+ """``(metered / modelled, cycles)`` over ``power_cycles``, a ratio
1142
+ of sums. ``(None, n)`` with fewer than ``MIN_POWER_EVENTS``."""
1143
+ cycles = power_cycles(rows, params)
1144
+ metered_total = sum(m for _, m, _ in cycles)
1145
+ modelled_total = sum(e for _, _, e in cycles)
1146
+ n = len(cycles)
1091
1147
  if n < MIN_POWER_EVENTS or modelled_total <= 0.0:
1092
1148
  return None, n
1093
1149
  return metered_total / modelled_total, n
@@ -1278,24 +1334,29 @@ def draw_free_withdrawn(row: Mapping[str, Any]) -> float | None:
1278
1334
  return 0.0 if bool(row.get("draw_free")) else None
1279
1335
 
1280
1336
 
1281
- def fit_cop_scale(
1337
+ @dataclass(frozen=True)
1338
+ class CopCycle:
1339
+ """One cycle's figures for the apparent COP: when it started, the
1340
+ heat it stored plus what draws took, the lead draws' heat, the
1341
+ integrator's COP, the metered energy (Wh) and the lift (°F).
1342
+ ``start`` is None where the rows carry no ``start_time``."""
1343
+
1344
+ start: datetime | None
1345
+ heat_wh: float
1346
+ lead_wh: float
1347
+ modelled_cop: float
1348
+ metered_wh: float
1349
+ lift_f: float
1350
+
1351
+
1352
+ def cop_cycles(
1282
1353
  rows: pd.DataFrame,
1283
1354
  params: HeaterParams,
1284
1355
  *,
1285
1356
  withdrawn: Withdrawn = draw_free_withdrawn,
1286
1357
  lead: Withdrawn | None = None,
1287
- ) -> tuple[tuple[float, float, float, float] | None, int]:
1288
- """``((scale, startup_wh, lead_share, lift_coeff), cycles)`` for the
1289
- apparent COP, the heat a cycle stored (the full tank's mass times
1290
- the rise of the probes' mean) plus what ``withdrawn`` says draws
1291
- took during it, and ``lead`` those just before it, whose cold water
1292
- the start reading had not yet seen. ``(None, n)`` with fewer than
1293
- ``MIN_POWER_EVENTS``.
1294
-
1295
- Without a meter the default keeps only the cycles no water was
1296
- drawn around: the heat drawn during a cycle is otherwise missing
1297
- from its heat and pulls the scale low.
1298
- """
1358
+ ) -> list[CopCycle]:
1359
+ """The cycles ``fit_cop_scale`` fits on, and their figures."""
1299
1360
  needed = [
1300
1361
  "metered_input_wh",
1301
1362
  "t_start_lower",
@@ -1304,13 +1365,9 @@ def fit_cop_scale(
1304
1365
  "t_upper_end",
1305
1366
  ]
1306
1367
  if not set(needed) <= set(rows.columns):
1307
- return None, 0
1368
+ return []
1308
1369
  wh_per_f = params.tank.thermal_mass_btu_f / BTU_PER_WH
1309
- heat: list[float] = []
1310
- before: list[float] = []
1311
- cop: list[float] = []
1312
- metered_wh: list[float] = []
1313
- lift: list[float] = []
1370
+ out: list[CopCycle] = []
1314
1371
  for row in _records(_starts(rows)):
1315
1372
  if any(row[c] is None or pd.isna(row[c]) for c in needed):
1316
1373
  continue
@@ -1338,6 +1395,7 @@ def fit_cop_scale(
1338
1395
  path.elapsed_s[-1],
1339
1396
  params.compressor.two_zone,
1340
1397
  float(row["t_start_upper"]),
1398
+ tank=params.tank,
1341
1399
  )
1342
1400
  if modelled is None or not math.isfinite(modelled) or modelled <= 0:
1343
1401
  continue
@@ -1350,15 +1408,51 @@ def fit_cop_scale(
1350
1408
  + float(row["t_start_lower"])
1351
1409
  + float(row["t_start_upper"])
1352
1410
  ) / 4
1353
- heat.append(stored + took)
1354
- before.append(lead_wh)
1355
- cop.append(modelled)
1356
- metered_wh.append(metered)
1357
- lift.append(mean - float(air))
1358
- n = len(heat)
1411
+ out.append(
1412
+ CopCycle(
1413
+ start=row.get("start_time"),
1414
+ heat_wh=stored + took,
1415
+ lead_wh=lead_wh,
1416
+ modelled_cop=modelled,
1417
+ metered_wh=metered,
1418
+ lift_f=mean - float(air),
1419
+ )
1420
+ )
1421
+ return out
1422
+
1423
+
1424
+ def fit_cop_scale(
1425
+ rows: pd.DataFrame,
1426
+ params: HeaterParams,
1427
+ *,
1428
+ withdrawn: Withdrawn = draw_free_withdrawn,
1429
+ lead: Withdrawn | None = None,
1430
+ ) -> tuple[tuple[float, float, float, float] | None, int]:
1431
+ """``((scale, startup_wh, lead_share, lift_coeff), cycles)`` for the
1432
+ apparent COP, the heat a cycle stored (the full tank's mass times
1433
+ the rise of the probes' mean) plus what ``withdrawn`` says draws
1434
+ took during it, and ``lead`` those just before it, whose cold water
1435
+ the start reading had not yet seen (``cop_cycles``). ``(None, n)``
1436
+ with fewer than ``MIN_POWER_EVENTS``.
1437
+
1438
+ Without a meter the default keeps only the cycles no water was
1439
+ drawn around: the heat drawn during a cycle is otherwise missing
1440
+ from its heat and pulls the scale low.
1441
+ """
1442
+ cycles = cop_cycles(rows, params, withdrawn=withdrawn, lead=lead)
1443
+ n = len(cycles)
1359
1444
  if n < MIN_POWER_EVENTS:
1360
1445
  return None, n
1361
- return solve_cop_fit(heat, cop, metered_wh, before, lift), n
1446
+ return (
1447
+ solve_cop_fit(
1448
+ [c.heat_wh for c in cycles],
1449
+ [c.modelled_cop for c in cycles],
1450
+ [c.metered_wh for c in cycles],
1451
+ [c.lead_wh for c in cycles],
1452
+ [c.lift_f for c in cycles],
1453
+ ),
1454
+ n,
1455
+ )
1362
1456
 
1363
1457
 
1364
1458
  def fit_power(