nwp500-python 9.12.0__tar.gz → 9.12.2__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 (348) hide show
  1. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/CHANGELOG.rst +590 -133
  2. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/CONTRIBUTING.rst +1 -1
  3. {nwp500_python-9.12.0/src/nwp500_python.egg-info → nwp500_python-9.12.2}/PKG-INFO +6 -17
  4. nwp500_python-9.12.2/docs/_static/light-page.js +1 -0
  5. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/conf.py +6 -2
  6. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/explanation/draws.rst +115 -48
  7. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/explanation/heating-elements.rst +2 -2
  8. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/explanation/index.rst +1 -0
  9. nwp500_python-9.12.2/docs/explanation/simulator-methodology.rst +726 -0
  10. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/explanation/simulator.rst +79 -62
  11. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/explanation/standby-loss.rst +109 -19
  12. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/explanation/tank-energy.rst +10 -67
  13. nwp500_python-9.12.2/docs/explanation/unknown-values.rst +198 -0
  14. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/explanation/what-starts-a-recovery.rst +17 -12
  15. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/authenticate.rst +1 -1
  16. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/bluetooth.rst +18 -24
  17. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/calibrate-simulator.rst +24 -19
  18. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/diagnose-mqtt.rst +2 -3
  19. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/maintenance.rst +13 -17
  20. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/optimize-tou.rst +8 -9
  21. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/schedule-operation.rst +15 -9
  22. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/simulate-heater.rst +125 -71
  23. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/track-energy.rst +1 -1
  24. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/project/history.rst +0 -1
  25. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/configuration.rst +1 -1
  26. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/enumerations.rst +11 -14
  27. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/protocol/data_conversions.rst +8 -8
  28. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/protocol/device_features.rst +4 -4
  29. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/protocol/device_status.rst +7 -7
  30. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/protocol/mqtt_protocol.rst +76 -98
  31. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/protocol/rest_api.rst +49 -54
  32. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/python_api/api_client.rst +24 -28
  33. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/python_api/auth_client.rst +1 -1
  34. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/python_api/cli.rst +7 -8
  35. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/python_api/events.rst +2 -2
  36. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/python_api/models.rst +26 -30
  37. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/python_api/mqtt_client.rst +30 -36
  38. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/python_api/sim.rst +48 -20
  39. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/simulator-inputs.rst +349 -140
  40. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/requirements.txt +2 -1
  41. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/tutorials/getting-started.rst +5 -5
  42. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/pyproject.toml +72 -2
  43. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/scripts/README.md +1 -1
  44. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/scripts/validate_version.py +21 -13
  45. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/setup.cfg +0 -65
  46. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/api_client.py +39 -43
  47. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/auth.py +9 -9
  48. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/ble/client.py +5 -5
  49. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/ble/protocol.py +55 -68
  50. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/cli/__main__.py +20 -8
  51. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/cli/calibration.py +2 -2
  52. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/cli/presentation.py +4 -4
  53. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/cli/simulate.py +125 -7
  54. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/converters.py +10 -12
  55. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/encoding.py +1 -1
  56. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/enums.py +21 -27
  57. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/models/account.py +3 -3
  58. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/models/device.py +20 -25
  59. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/models/diagnostics.py +8 -9
  60. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/models/energy.py +6 -6
  61. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/models/feature.py +4 -8
  62. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/models/maintenance.py +5 -6
  63. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/models/mqtt_models.py +16 -19
  64. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/models/schedule.py +10 -14
  65. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/models/status.py +12 -19
  66. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/models/tou.py +1 -2
  67. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/_control_commands.py +2 -2
  68. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/_device_subscriptions.py +1 -1
  69. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/client.py +12 -15
  70. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/control.py +36 -48
  71. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/subscriptions.py +3 -3
  72. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/types.py +1 -1
  73. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/utils.py +7 -11
  74. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/api.py +31 -50
  75. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/attribution.py +5 -2
  76. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/book.py +322 -454
  77. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/calibration.py +328 -653
  78. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/circulation.py +50 -15
  79. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/cold_supply.py +3 -4
  80. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/controller.py +140 -59
  81. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/decay.py +72 -101
  82. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/defaults.py +50 -36
  83. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/delivery.py +74 -164
  84. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/draw_forecast.py +11 -3
  85. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/engine.py +118 -472
  86. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/extensions.py +13 -38
  87. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/heat_pump_recovery.py +3 -1
  88. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/heating_trajectory.py +3 -1
  89. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/inputs.py +3 -2
  90. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/params.py +184 -118
  91. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/physics/compressor.py +42 -17
  92. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/physics/draw.py +171 -139
  93. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/physics/refrigerant.py +43 -34
  94. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/physics/tank.py +2 -2
  95. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/ready.py +35 -89
  96. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/tank_physics.py +9 -12
  97. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/tou_triggers.py +116 -15
  98. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim_adapters.py +86 -25
  99. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim_calibrate.py +64 -93
  100. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim_home_assistant.py +67 -21
  101. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/temperature.py +2 -2
  102. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/topic_builder.py +4 -5
  103. {nwp500_python-9.12.0 → nwp500_python-9.12.2/src/nwp500_python.egg-info}/PKG-INFO +6 -17
  104. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/SOURCES.txt +3 -2
  105. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/requires.txt +2 -11
  106. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/scm_file_list.json +4 -3
  107. nwp500_python-9.12.2/src/nwp500_python.egg-info/scm_version.json +8 -0
  108. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/_sim_site.py +0 -44
  109. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/fixtures/sim/engine_golden.json +45 -55
  110. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/fixtures/sim/heater_params.json +7 -4
  111. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/fixtures/sim/heater_params_fitted.json +17 -11
  112. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_cli_calibrate.py +3 -5
  113. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_cli_simulate.py +177 -2
  114. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_adapters.py +5 -6
  115. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_api.py +37 -47
  116. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_book.py +182 -72
  117. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_book_history.py +382 -47
  118. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_bottom_loss.py +10 -8
  119. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_calibrate.py +71 -68
  120. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_calibration.py +49 -33
  121. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_change_history.py +123 -9
  122. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_circulation.py +54 -15
  123. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_codec.py +0 -2
  124. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_controller.py +155 -28
  125. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_defaults.py +9 -16
  126. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_documents.py +0 -4
  127. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_draw_forecast.py +0 -2
  128. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_draw_loss.py +170 -58
  129. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_element_fit.py +0 -2
  130. nwp500_python-9.12.2/tests/test_sim_evaporator.py +59 -0
  131. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_fit_book.py +1 -6
  132. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_forecast.py +0 -2
  133. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_golden.py +4 -6
  134. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_home_assistant.py +30 -14
  135. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_hot_water.py +328 -38
  136. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_live.py +21 -5
  137. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_location_air.py +0 -2
  138. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_no_clock_no_state.py +3 -4
  139. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_overlay.py +0 -2
  140. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_readiness.py +147 -334
  141. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_readiness_fit.py +102 -64
  142. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_ready.py +52 -34
  143. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_ready_api.py +41 -20
  144. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_replay.py +0 -2
  145. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_tank_physics.py +13 -13
  146. nwp500_python-9.12.0/docs/explanation/unknown-values.rst +0 -264
  147. nwp500_python-9.12.0/setup.py +0 -22
  148. nwp500_python-9.12.0/src/nwp500_python.egg-info/scm_version.json +0 -8
  149. nwp500_python-9.12.0/uv.lock +0 -3
  150. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/.coveragerc +0 -0
  151. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  152. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/.github/copilot-instructions.md +0 -0
  153. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/.github/workflows/ci.yml +0 -0
  154. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/.github/workflows/release.yml +0 -0
  155. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/.gitignore +0 -0
  156. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/.pre-commit-config.yaml +0 -0
  157. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/.readthedocs.yml +0 -0
  158. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/AUTHORS.rst +0 -0
  159. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/LICENSE.txt +0 -0
  160. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/Makefile +0 -0
  161. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/README.rst +0 -0
  162. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/RELEASE.md +0 -0
  163. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/Makefile +0 -0
  164. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/_static/.gitignore +0 -0
  165. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/explanation/architecture.rst +0 -0
  166. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/explanation/recovery-speed.rst +0 -0
  167. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/explanation/tou-recovery-cap.rst +0 -0
  168. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/auto-recovery.rst +0 -0
  169. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/home-assistant.rst +0 -0
  170. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/index.rst +0 -0
  171. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/manage-units.rst +0 -0
  172. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/monitor-status.rst +0 -0
  173. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/how-to/queue-commands.rst +0 -0
  174. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/index.rst +0 -0
  175. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/openapi.yaml +0 -0
  176. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/project/authors.rst +0 -0
  177. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/project/changelog.rst +0 -0
  178. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/project/contributing.rst +0 -0
  179. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/project/license.rst +0 -0
  180. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/index.rst +0 -0
  181. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/installation.rst +0 -0
  182. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/protocol/error_codes.rst +0 -0
  183. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/protocol/quick_reference.rst +0 -0
  184. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/docs/reference/python_api/exceptions.rst +0 -0
  185. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/.ruff.toml +0 -0
  186. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/README.md +0 -0
  187. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/air_filter_reset.py +0 -0
  188. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/anti_legionella.py +0 -0
  189. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/auto_recovery.py +0 -0
  190. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/combined_callbacks.py +0 -0
  191. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/demand_response.py +0 -0
  192. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/device_capabilities.py +0 -0
  193. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/device_status_debug.py +0 -0
  194. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/energy_analytics.py +0 -0
  195. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/error_code_demo.py +0 -0
  196. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/firmware_payload_capture.py +0 -0
  197. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/mqtt_diagnostics.py +0 -0
  198. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/power_control.py +0 -0
  199. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/recirculation_control.py +0 -0
  200. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/reconnection_demo.py +0 -0
  201. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/reservation_schedule.py +0 -0
  202. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/simple_auto_recovery.py +0 -0
  203. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/token_restoration.py +0 -0
  204. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/tou_openei.py +0 -0
  205. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/tou_schedule.py +0 -0
  206. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/advanced/water_reservation.py +0 -0
  207. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/beginner/01_authentication.py +0 -0
  208. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/beginner/02_list_devices.py +0 -0
  209. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/beginner/03_get_status.py +0 -0
  210. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/beginner/04_set_temperature.py +0 -0
  211. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/intermediate/advanced_auth_patterns.py +0 -0
  212. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/intermediate/command_queue.py +0 -0
  213. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/intermediate/device_status_callback.py +0 -0
  214. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/intermediate/error_handling.py +0 -0
  215. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/intermediate/event_driven_control.py +0 -0
  216. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/intermediate/improved_auth.py +0 -0
  217. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/intermediate/legacy_auth_constructor.py +0 -0
  218. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  219. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/intermediate/periodic_requests.py +0 -0
  220. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/intermediate/set_mode.py +0 -0
  221. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/intermediate/vacation_mode.py +0 -0
  222. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/mask.py +0 -0
  223. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/simulator/lower-setpoint.json +0 -0
  224. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/simulator/plan.json +0 -0
  225. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/simulator/scenario.json +0 -0
  226. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/testing/periodic_device_info.py +0 -0
  227. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/testing/simple_periodic_info.py +0 -0
  228. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/testing/test_api_client.py +0 -0
  229. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/testing/test_mqtt_connection.py +0 -0
  230. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/testing/test_mqtt_messaging.py +0 -0
  231. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/examples/testing/test_periodic_minimal.py +0 -0
  232. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/scripts/bump_version.py +0 -0
  233. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/scripts/diagnose_mqtt_connection.py +0 -0
  234. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/scripts/extract_changelog.py +0 -0
  235. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/scripts/format.py +0 -0
  236. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/scripts/lint.py +0 -0
  237. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/scripts/setup-dev.py +0 -0
  238. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/__init__.py +0 -0
  239. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/_base.py +0 -0
  240. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/ble/__init__.py +0 -0
  241. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/ble/__main__.py +0 -0
  242. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/ble/mqtt_adapter.py +0 -0
  243. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/cli/__init__.py +0 -0
  244. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/cli/ble_backend.py +0 -0
  245. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/cli/handlers.py +0 -0
  246. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/cli/monitoring.py +0 -0
  247. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/cli/output_formatters.py +0 -0
  248. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/cli/recording.py +0 -0
  249. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/cli/rich_output.py +0 -0
  250. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/cli/token_storage.py +0 -0
  251. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/command_decorators.py +0 -0
  252. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/config.py +0 -0
  253. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/device_capabilities.py +0 -0
  254. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/device_info_cache.py +0 -0
  255. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/events.py +0 -0
  256. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/exceptions.py +0 -0
  257. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/factory.py +0 -0
  258. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/field_factory.py +0 -0
  259. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/models/__init__.py +0 -0
  260. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/models/_converters.py +0 -0
  261. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/models/firmware.py +0 -0
  262. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/__init__.py +0 -0
  263. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/command_queue.py +0 -0
  264. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/connection.py +0 -0
  265. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/diagnostics.py +0 -0
  266. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/periodic.py +0 -0
  267. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/reconnection.py +0 -0
  268. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt/state_tracker.py +0 -0
  269. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/mqtt_events.py +0 -0
  270. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/openei.py +0 -0
  271. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/py.typed +0 -0
  272. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/reservations.py +0 -0
  273. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/__init__.py +0 -0
  274. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/codec.py +0 -0
  275. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/location_air.py +0 -0
  276. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/observed.py +0 -0
  277. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/physics/__init__.py +0 -0
  278. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/physics/element.py +0 -0
  279. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/profile.py +0 -0
  280. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/record.py +0 -0
  281. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/result.py +0 -0
  282. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/schedules.py +0 -0
  283. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim/units.py +0 -0
  284. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/sim_live.py +0 -0
  285. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/status_history.py +0 -0
  286. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/tou_schedule.py +0 -0
  287. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/unit_system.py +0 -0
  288. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500/utils.py +0 -0
  289. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  290. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  291. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  292. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/top_level.txt +0 -0
  293. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/conftest.py +0 -0
  294. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_api_endpoints_201.py +0 -0
  295. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_api_helpers.py +0 -0
  296. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_app_queries.py +0 -0
  297. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_auth.py +0 -0
  298. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_auth_session_lifecycle.py +0 -0
  299. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_auth_session_refresh.py +0 -0
  300. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_ble_mqtt_adapter.py +0 -0
  301. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_ble_protocol.py +0 -0
  302. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_bug_fixes.py +0 -0
  303. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_canonical_schedule.py +0 -0
  304. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_cli_basic.py +0 -0
  305. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_cli_commands.py +0 -0
  306. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_cli_tristate_presentation.py +0 -0
  307. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_command_decorators.py +0 -0
  308. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_command_queue.py +0 -0
  309. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_device_capabilities.py +0 -0
  310. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_device_info_cache.py +0 -0
  311. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_device_rest_models.py +0 -0
  312. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_events.py +0 -0
  313. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_exceptions.py +0 -0
  314. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_model_converters.py +0 -0
  315. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_models.py +0 -0
  316. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_mqtt_clean_session_resume.py +0 -0
  317. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_mqtt_client_init.py +0 -0
  318. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_mqtt_control_fail.py +0 -0
  319. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_mqtt_events.py +0 -0
  320. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_mqtt_hypothesis.py +0 -0
  321. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_mqtt_last_will.py +0 -0
  322. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_mqtt_reconnection.py +0 -0
  323. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_mqtt_reconnection_storm.py +0 -0
  324. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_mqtt_reliability.py +0 -0
  325. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_multi_device.py +0 -0
  326. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_navilink_201_enums.py +0 -0
  327. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_openei.py +0 -0
  328. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_protocol_correctness.py +0 -0
  329. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_public_api.py +0 -0
  330. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_reservation_validation.py +0 -0
  331. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_reservations.py +0 -0
  332. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_cold_supply.py +0 -0
  333. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_element.py +0 -0
  334. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_import_contract.py +0 -0
  335. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_observed.py +0 -0
  336. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_profile.py +0 -0
  337. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_sim_schedules.py +0 -0
  338. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_status_history.py +0 -0
  339. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_temperature_converters.py +0 -0
  340. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_threading_model.py +0 -0
  341. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_token_storage.py +0 -0
  342. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_tou_api.py +0 -0
  343. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_tou_schedule.py +0 -0
  344. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_unit_switching.py +0 -0
  345. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_unit_system_process_wide.py +0 -0
  346. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_utility_modules.py +0 -0
  347. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tests/test_utils.py +0 -0
  348. {nwp500_python-9.12.0 → nwp500_python-9.12.2}/tox.ini +0 -0
@@ -2,6 +2,489 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version 9.12.2 (2026-10-25)
6
+ ===========================
7
+
8
+ Changed
9
+ -------
10
+
11
+ - **The heat book prices its runs as cooled to the air, on the
12
+ record.** Pricing each run at the loop's return instead, the pipe
13
+ resting 7.84 °F over the air and cooling on 0.464 h, books the fitted
14
+ pairs no better (17,018 BTU mean absolute error against 16,991 on 87
15
+ pairs). The book is unchanged. See :doc:`/how-to/calibrate-simulator`.
16
+
17
+ - **The recovery fit's held-out error is draws inside recoveries.**
18
+ The latest recoveries come out about 25 min short at every
19
+ compressor fit tried. Those with under 2 gal of hot water and 5
20
+ minutes of circulation inside them come out 0.1 min short (20 of
21
+ 35); each hot gallon inside a recovery adds about 4.7 min. The fit is
22
+ unchanged: fitted on the draw-light recoveries alone, the constant
23
+ scores worse on the held-out draw-light ones (14.2 min against 11.6).
24
+ See :doc:`/explanation/simulator`.
25
+
26
+ - **The compressor's engineering values are not fittable on the
27
+ record.** The condenser approach (10 °F), clearance ratio (0.05) and
28
+ polytropic exponent (1.17) stay. The compressor constant absorbs
29
+ each: refitting it with each moved over a wide range changed the
30
+ held-out recovery error by under 0.7 min. See
31
+ :doc:`/reference/simulator-inputs`.
32
+
33
+ - **Hand-set values checked against the record.** None changes.
34
+ ``TankParams.lower_zone_share`` 0.5 stays: refitting the compressor
35
+ constant at 0.435-0.5 moved the held-out recovery error by under
36
+ 0.7 min. ``ControllerParams.tou_cap_f`` 3.6 °F stays at the device's
37
+ threshold: the probe's 3.42 °F at a capped stop is one probe step
38
+ past it, as a full recovery ends past its setpoint.
39
+ ``ReadinessParams.probe_resolution_f`` is a round 1 °F and is now
40
+ tagged as a default, not a measurement. The unused
41
+ ``engine.RESOLUTION_F`` is gone. The humidity default of 50 % stays:
42
+ holding every recovery there instead of its own reading moved the
43
+ mean duration error by 0.4 min. ``calibration.SETTLE_MINUTES`` 45
44
+ stays: ``ua_ref`` was 4.44-4.48 at 30 to 90 min. The recovery screen
45
+ (``MIN_MINUTES_PER_F``, ``MAX_MINUTES_PER_F``) stays: fitting with
46
+ the 12 it drops moved the compressor constant 0.2 %.
47
+ ``sim_adapters.FLOW_HOLD``, ``sim_home_assistant.PUMP_ON_RPM`` and
48
+ ``PUMP_STALE`` say what the record shows of them. See
49
+ :doc:`/reference/simulator-inputs` and
50
+ :doc:`/explanation/standby-loss`.
51
+
52
+ - **A window opening drops a running element mode: from the record.**
53
+ The simulator drops an element mode to the heat pump at a window's
54
+ start, which was assumed from one device run at a TOU switch-on. The
55
+ record has 18 openings with an element mode in force (2026-01-01 to
56
+ 10-02): every one dropped it within 2 s, 15 at the window's scheduled
57
+ start and 3 at TOU switched back on inside a window. The model is
58
+ unchanged. See :doc:`/how-to/schedule-operation`.
59
+
60
+ Fixed
61
+ -----
62
+
63
+ - **Without an air sensor, the standby loss is fitted on the heater's
64
+ idle reading.** It was fitted on the heater's running reading,
65
+ interpolated from one run to the next, on the quiet windows with a
66
+ run within ``HEATER_AIR_BRACKET`` (6 h, now removed) of both ends.
67
+ The night air dips below that line: at one installation, against a
68
+ sensor beside the heater, those windows read 3.9 °F warm on average
69
+ (7.8 °F on windows over 12 h), and the loss 5 % high on 150 of 378
70
+ windows, 21 % high on 54 from 2026-06-25. The idle reading, the
71
+ cabinet's less ``sim_adapters.IDLE_CABINET_WARM_F``, reads every
72
+ window: the loss comes out 0.7 % high on all 378, and 0.3 % low on
73
+ the 188 from 2026-06-25 (2.2 % low with the cabinet's warmth measured
74
+ before then). The frame has a new column, ``heater_cabinet_f``. See
75
+ :doc:`/how-to/calibrate-simulator`.
76
+
77
+ - **The site's standby loss and gap constant are the refits that
78
+ score better held out.** ``site_example_params`` kept the earlier
79
+ standby law (``UA`` 4.605, exponent 0.186, no bottom loss) and gap
80
+ constant (60.06 h), though the calibration with ``keep_better`` on
81
+ the record replaces both. Fitted on 378 quiet windows to 2026-10-24,
82
+ the law is a flat ``UA`` of 4.48 BTU/hr per °F with a 90.8 BTU/hr
83
+ bottom loss (gate 1.83 °F). The gap constant, fitted on 127 windows
84
+ with that bottom loss opening the gaps it relaxes, is 26.7 h. Held
85
+ out, the pair tracks the probes' fall within 0.066 °F/h against
86
+ 0.096 (56 windows) and the gap at 0.69 °F² against 1.78 (32
87
+ windows). Recalibrated with them as the base, every group keeps
88
+ them. The compressor, power and COP terms keep theirs (the refits
89
+ scored no better). See :doc:`/explanation/standby-loss`.
90
+
91
+ - **The ungated circulation share is the library's own fit.**
92
+ ``site_example_params``' ``DrawParams.ungated_observability`` was
93
+ 0.398, measured outside the library at the run model's earlier
94
+ terms, and the library's ``fit_ungated_observability`` never
95
+ reproduced it (0.200 at those terms on the same record). It is now
96
+ 0.263, that fit on 165 windows from April to October at the current
97
+ run model; 0.117 on the windows since September. It prices the runs
98
+ inside a metered COP cycle: held out, the latest 45 cycles' median
99
+ error is 6.1 % against 6.3 % with 0.398. See
100
+ :doc:`/explanation/standby-loss`.
101
+
102
+ - **High Demand's element restarts later in a stint.** After the entry
103
+ run the simulator never ran High Demand's element again. On the
104
+ record it restarts the upper element with the upper zone about 4 °F
105
+ short of the setpoint (the four restarts of 2026-04-26/27 at 3.78 to
106
+ 4.14 °F, each about 3 minutes to the setpoint), the measured
107
+ ``ElementParams.upper_reengages_below_setpoint_f`` (4.1 °F) that
108
+ nothing read. The controller now starts it there
109
+ (``controller.later_upper_on``, trigger ``high_demand_upper_short``,
110
+ also in ``attribution``). Electric's later starts are still not
111
+ modelled and still flagged: no Electric stint on record ran half an
112
+ hour. See :doc:`/explanation/simulator`.
113
+
114
+ - **A circulation run's return is not at the air.** The run model took
115
+ the returning water at the air beside the heater and the pipe as
116
+ cooled between runs. On 123 runs with the compressor off (April to
117
+ October), the return rested about 7 °F above the heater's air, and a
118
+ run within an hour of another took about half the heat of one after
119
+ a long rest. ``CirculationParams`` gains ``return_rest_offset_f`` and
120
+ ``return_cool_tau_h`` (defaults 0, the old form): the inflow is the
121
+ air plus the offset, and a run's gallons are scaled by the share of
122
+ the pipe cooled since the last run (``circulation.cooled_share``).
123
+ ``calibration.fit_return`` fits the volume and both terms together;
124
+ ``measure_runs`` records each run's rest, and ``run_heat_btu``,
125
+ ``metered_withdrawn`` and ``fit_ungated_observability`` take the
126
+ terms. The site's terms are 19.36 gal, 7.84 °F and 0.464 h, a
127
+ cross-validated error of 268 BTU a run against 767 for the old form.
128
+ The volume is effective: the long runs on the record never brought
129
+ hot water back. Snapshot and window runs are now ``(minutes, upper,
130
+ rest hours, minutes run before)``, and a run a snapshot takes up
131
+ partway is priced from where it was. See :doc:`/reference/simulator-inputs` and
132
+ :doc:`/explanation/standby-loss`.
133
+
134
+ - **The R-134a properties are CoolProp's.** The refrigerant table,
135
+ cited as "ASHRAE Standard 34" (a safety classification, not a property
136
+ table), had the saturated vapour density about 4x low and the liquid
137
+ enthalpy 14 to 40 BTU/lb high, so the condenser's latent heat fell to
138
+ 9.7 BTU/lb at 160 °F against a true 52.7. It is now CoolProp 8.0.0's
139
+ every 5 °F. On the record's mixed-tank heating, the model's rate now
140
+ falls 24 BTU/hr per °F of tank and rises 48 per °F of air, against
141
+ the record's 28 and 53 (the old table: 12 and 34). The site's
142
+ compressor constant is refit to 0.0160 ft³/s, a compressor of about
143
+ 8 cc at 3,500 rpm (the old 0.0499 was four times that, to make up the
144
+ density). The power scale is carried to 1.640 and the COP terms to
145
+ 0.688 and 0.002. The durations score the same: 27.0 min mean error
146
+ on 177 recoveries, against 27.2.
147
+ - **The compressor fit holds the evaporator approach.** With the true
148
+ properties the durations no longer place the approach: K and the
149
+ approach together reached the site's error from 6.9 °F, where the
150
+ evaporator coil reads 19.6 °F below the air. ``fit_compressor`` now
151
+ fits ``compressor_constant`` alone, at the parameters'
152
+ ``evap_approach_f``. ``CompressorParams.evap_approach_f`` defaults to
153
+ the coil's 19.56 °F and is labelled measured. ``K_BOUNDS`` is
154
+ 0.004-0.08, and ``INITIAL_K``, ``INITIAL_APPROACH_F`` and
155
+ ``APPROACH_BOUNDS`` are gone. See :doc:`/reference/simulator-inputs`.
156
+
157
+ - **The circulation share's story.** ``DrawParams.ungated_observability``
158
+ was described as the probes' share of a run's heat. The loop heat it
159
+ divides counts all the loop's flow as the tank's, but the return
160
+ feeds the mixing valve's cold port as well as the tank's inlet, and
161
+ that split is not measured. The share is the split and any probe
162
+ shortfall together (its refit to 0.263 is above). See
163
+ :doc:`/explanation/standby-loss`.
164
+
165
+ - **The upper zone's hold through a recovery rests on the record.** The
166
+ two-zone model warms the lower zone alone while the tank is
167
+ stratified and holds the upper where it started. On 30 compressor-only
168
+ recoveries the upper probe fell a median 0.54 °F while the gap was
169
+ over 3 °F and rose to 1.26 °F above its start over the gap's last 2 to
170
+ 3 °F, so the hold stays. See :doc:`/explanation/simulator`.
171
+
172
+ - **The outlet lag is the tank's.** ``ReadinessParams.delivered_lag_gal``
173
+ (5 gal) was described as mostly the pipe to the fixture. Both bath
174
+ figures behind it, 7.3 gal at the tub spout and 5.3 at the valve
175
+ outlet, were taken net of the pipe to each thermometer, and they
176
+ differ by calibration, not place. The lag is the tank water above the
177
+ upper probe, and the field is now labelled the heater's. The value is
178
+ unchanged. See :doc:`/reference/simulator-inputs`.
179
+
180
+ - **The draw windows' reach rests on the record.** ``HEATER_DRAW_REACH``
181
+ stays 60 minutes. Its reason was that a draw "can come that much
182
+ earlier than any the forecast has seen", which nothing measured. On 44
183
+ complete forecast issues (98 long draws), 60 minutes covered 94.9% of
184
+ long draws and 30 minutes 89.8%; the five covered only at 60 fell
185
+ either side of a 30-minute window. See :doc:`/how-to/simulate-heater`.
186
+
187
+ - **The heater's idle air reading is corrected.** Idle, the heater's
188
+ own air sensor reads its cabinet: on the record (February to October)
189
+ 6.2 °F above a sensor beside the heater on average, 9.3 in February to
190
+ 4.8 in October. ``sim_adapters.environment(status)``, and so the live
191
+ scenario and a replay without recorded air, now take
192
+ ``IDLE_CABINET_WARM_F`` (6.2 °F) off an idle reading. That cuts the
193
+ average error from 6.3 to 2.2 °F. The ``ambient_from_idle_cabinet``
194
+ reason still names it. See :doc:`/reference/simulator-inputs`.
195
+
196
+ - **The Home Assistant air fit settles 5 minutes into a run.**
197
+ ``sim_home_assistant.RUN_SETTLE`` was 2 minutes, against the book's 5
198
+ (``sim_adapters.HEATER_AIR_SETTLE``). It is now that constant. On the
199
+ record the reading runs a median 2.1 °F warm in a run's third minute
200
+ and 1.8 in its fifth.
201
+
202
+ - **The fan airflow's source.** ``CompressorParams.fan_airflow_cfm``
203
+ (229) was cited as the NWP500's specification. No airflow is
204
+ published; the manual's 229 is the 50 gal model's shipping weight in
205
+ pounds. The value stays: on 178 recorded recoveries the latent term it
206
+ sizes is at most about 150 BTU/hr, humidity's own effect is not
207
+ resolved, and the term off scored 0.6 min worse held out.
208
+
209
+ - **The 50 and 80 gal models are scaled by rated storage.** Their tank
210
+ volume, the book's count of it and its empty point were scaled from
211
+ the 65's by nominal size (48.6 and 77.8 gal). The manual's
212
+ specification table rates them at 45.2 and 76.1 gal, against the
213
+ 65's 63.2. ``defaults.NOMINAL_GAL`` is now ``RATED_GAL``.
214
+
215
+ - **A held mode landing over the setpoint starts nothing.** On
216
+ 2026-09-21 Energy Saver, held through a TOU window, landed at its
217
+ close with the upper tank 18.7 °F above a 107.6 °F setpoint and the
218
+ lower tank 20.9 °F under it. Nothing started for 9 h 56 min, and the
219
+ start came 2 min 17 s into a circulation run. The simulation started
220
+ the compressor at the landing, and again on the lower trigger, since
221
+ it treated Energy Saver's trigger as a plain level test. Three rules
222
+ change:
223
+
224
+ - A mode write starts the compressor on a short lower tank
225
+ (``mode_write_lower_starts_f``) out of an element mode whatever the
226
+ upper tank, as before (19 of 19), and otherwise only with the upper
227
+ tank short too.
228
+ - Energy Saver holds the lower trigger while the upper tank is at or
229
+ above the setpoint, as Heat Pump does, and a draw's drop under the
230
+ setting starts it. ``tou_triggers.LOWER_HOLD_MODE`` is now
231
+ ``LOWER_HOLD_MODES``, and ``autonomous_start`` takes
232
+ ``drop_starts``.
233
+ - A mode write held in a window is judged as a write in the window is
234
+ (``window_write_starts_f``). Before, it always charged a start. Two
235
+ held Energy Saver writes with the compressor off, 2.4 °F short and
236
+ 27.5 °F over, started nothing for hours.
237
+
238
+ Documented in :doc:`/explanation/what-starts-a-recovery`.
239
+
240
+ - **A recovery that ends during a draw books only what follows.** The
241
+ book anchored at such a recovery's end and then booked the movement
242
+ again from its start, on the premise that the probes at the anchor
243
+ showed none of it, the cold water lying below the lower probe. The
244
+ lower probe falls at once with a draw. On the record from 2026-02-14
245
+ to 10-24 the rule fired 5 times, each a circulation run with at most
246
+ 0.03 gal of household draw, and walked to the next clean anchor it
247
+ scored a mean absolute error of 9.92 gal against 9.55 without it.
248
+ ``book_steps`` and ``calibration.book_walk`` now book the movement
249
+ from the anchor's own step on, its probes reading the step's start.
250
+
251
+ - **A draw window runs cold once its delivery falls below the line.**
252
+ A window counted as depleted only when the delivery fell more than
253
+ 3.4 °F below the line or stayed below it more than 3 minutes, an
254
+ allowance for the upper probe dipping during a draw. That dip was
255
+ measured during an Electric recovery on 2026-09-20, with no draw, and
256
+ no draw on the record shows one: on 535 clean draws the upper probe's
257
+ fall by the draw's end was a median 0.00-0.72 °F by size, and none
258
+ came back 1 °F or more within 10 minutes. The simulated delivery has
259
+ no transient either, so the allowance only hid real crossings, up to
260
+ about 1.7 gal past the line. Removed: ``ReadinessParams.dip_f`` and
261
+ ``dip_min``. A parameters document that still carries them loads, and
262
+ ``unknown_fields`` doesn't name them.
263
+
264
+ - **The evaporator moves 0.6 °F per °F of air, not one for one.** The
265
+ compressor's physics held the evaporator a fixed approach below the
266
+ air, so its capacity followed the air too closely. Over 377
267
+ compressor runs at one installation, the coil read 6.7 + 0.596 × the
268
+ air beside the heater (air 48-86 °F), and a free fit on 142 recorded
269
+ recoveries' durations chose a slope of 0.62. With the coil's slope and
270
+ the compressor constant and approach refit (0.0499 and 18.69 °F at
271
+ 65 °F air, on the old refrigerant table; both are replaced above),
272
+ the 35 recoveries held out came out 32.0 min off on
273
+ average, against 34.2 for the fixed approach refit on the same split
274
+ and 36.5 as shipped. The bias below 60 °F air went from +14.3 to
275
+ +4.4 min, and above 75 °F from -7.6 to +3.5. The coil's live reading
276
+ (``active_cycle``'s ``evaporator_f``) now sits in the same frame: as
277
+ the evaporator it fits with an offset of 0.0 °F. Added:
278
+ ``CompressorParams.evap_air_slope`` (1, a fixed approach, for a
279
+ document without it) and ``physics.compressor.evaporator_f`` and
280
+ ``EVAP_REF_AIR_F``. ``evap_approach_f`` is now the approach at 65 °F
281
+ air.
282
+
283
+ - **The heat book converts gallons at the mains, not the pipe at
284
+ rest.** A book built from a record converted its heat to gallons at
285
+ the inlet probe's median over the last 3 hours. At rest the probe
286
+ tracks the air around the pipe. Now the book reads the mains from the
287
+ long draws in its own record, as ``cold_supply`` does: the probe's
288
+ median over the last 2 minutes of each household draw of 11 minutes
289
+ or more with circulation off. Without one it takes
290
+ ``BookParams.inlet_default_f``. Over 2026-09-21 to 10-24, against the
291
+ plateaus, the inlet's mean error went from 3.85 °F (up to 14.2) to
292
+ 0.97 °F (up to 3.3). ``Booked.inlet_measured_share`` is now 1 when the
293
+ inlet is read from a long draw and 0 when it is the default. Added:
294
+ ``book.mains_f``. Removed: ``BookParams.inlet_window_s``. A parameters
295
+ document that still carries it loads, and ``unknown_fields`` doesn't
296
+ name it. The docs now give the mains by season
297
+ (:doc:`/explanation/draws`): ``draw.inlet_temp_f``, 72.4 °F, is a
298
+ September value.
299
+
300
+ - **A draw's probe response follows the tank's gap.** A delivered draw
301
+ moved the probes the same way whatever the tank's state: the bulk
302
+ fell by its apparent heat and the gap opened 1.235 °F per withdrawn
303
+ gallon. The recorded lower probe falls further the wider the gap at
304
+ the draw. Now the water let in reaches the lower probe by a share
305
+ that grows with the gap, and the upper probe falls 0.05 °F per
306
+ delivered gallon (``physics.draw.probes_after_draw``). On 535 clean
307
+ draws from 2026-02-21 to 10-23, the lower probe's mean absolute error
308
+ 10 minutes after the draw went from 0.48 to 0.35 °F under 1 gal, 1.68
309
+ to 0.91 at 1-3 gal, 3.70 to 2.12 at 3-8 gal, 6.72 to 3.52 at 8-20 gal
310
+ and 8.78 to 6.48 over 20 gal; its bias went from -1.75 to -0.03 °F
311
+ with the probes within 3 °F and from +1.62 to -0.10 with them apart.
312
+ On the 136 draws from 08-01, held out of the fit, the lower probe's
313
+ error went from 2.79 to 1.91 °F. Added: ``DrawParams.reach_gate_f``,
314
+ ``reach_offset_f``, ``reach_gal`` and ``upper_f_per_gal``;
315
+ ``physics.draw.probes_after_draw``, ``DEFAULT_REACH_GATE_F``,
316
+ ``DEFAULT_REACH_OFFSET_F``, ``DEFAULT_REACH_GAL`` and
317
+ ``DEFAULT_UPPER_F_PER_GAL``; and the ``draw_reach_gate_f``,
318
+ ``draw_reach_offset_f``, ``draw_reach_gal`` and
319
+ ``draw_upper_f_per_gal`` arguments of ``decay.project_decay`` and
320
+ ``decay.draw_step``. ``draw_step``'s ``draw_observability`` takes
321
+ None, which leaves the tank as it was. Removed:
322
+ ``DrawParams.gap_open_f_per_gal``; ``physics.draw.gap_opened_f`` and
323
+ ``DEFAULT_GAP_OPEN_F_PER_GAL``; the ``draw_gap_open_f_per_gal``
324
+ arguments of ``decay.project_decay`` and ``decay.draw_step``; and
325
+ ``draw_step``'s ``tank``. ``DrawParams.observability`` still prices a
326
+ draw's heat and turns delivered draws on. A parameters document that
327
+ still carries ``gap_open_f_per_gal`` loads, and ``unknown_fields``
328
+ doesn't name it.
329
+
330
+ - **The heat book counts all its heat.** The book held back the heat
331
+ that landed in a draw's cold water, the heat pump's and the lower
332
+ element's, until that water passed 100 °F, or until the lower probe,
333
+ having read it below 100 °F, read 100 °F again. On the two recorded drawdowns where it held
334
+ heat back, its answer at the start under-read the gallons delivered
335
+ to the line by about 9 gal; counting all of it read within about 4.
336
+ Now all booked heat is drawable, the deliverable gallons, readiness
337
+ and the upper zone through a draw alike, and a spent tank's delivery
338
+ falls to the inlet. Removed: ``BookParams.cold_release_f``;
339
+ ``Booked.cold_gal``, ``cold_f``, ``cold_seen``, ``withheld_btu`` and
340
+ ``observed``; ``book.projected_release``; ``book.Step.lower_element``;
341
+ ``WalkCarry.drawn`` and ``bottom``; the ``withheld_btu`` arguments of
342
+ ``book.deliverable_from_heat``, ``probe_line_gal`` and
343
+ ``delivery.deliverable``; ``calibration.withheld_btu`` and
344
+ ``zone_empty_point`` (with nothing held back it is ``empty_point``'s
345
+ median); and ``book_walk``'s ``withheld``, ``cold_gal``, ``cold_f``,
346
+ ``cold_gal_after`` and ``cold_f_after`` columns. ``Booked.heated``
347
+ takes one heat, ``ready.Added`` has one ``heat_btu`` in place of
348
+ ``bottom_btu`` and ``top_btu``, and ``book.draw_out`` returns the
349
+ heat alone. ``zone_empty_btu`` keeps its value. A parameters document
350
+ that still carries ``cold_release_f`` loads, and ``unknown_fields``
351
+ doesn't name it.
352
+
353
+ - **With a heat book, a draw moves the lower probe at once.** With a
354
+ book, the simulated lower probe held where a draw found it until the
355
+ book's cold region reached about 9 gal, then settled over 5 hours
356
+ toward the draw's calibrated response, which left it 3 to 10 °F warm
357
+ after draws of 3 to 20 gal. The recorded lower probe falls on every
358
+ draw at once and neither rebounds nor keeps falling. Now a draw moves
359
+ the lower probe by its calibrated draw response at once, as it does
360
+ without a book; the book still takes the upper probe down. Removed
361
+ with the old behavior: ``BookParams.lower_probe_gal``,
362
+ ``front_width_gal`` and ``front_settle_min``; their fits
363
+ ``calibration.front_frame``, ``fit_front``, ``FrontFit``,
364
+ ``settle_tails``, ``fit_front_settle`` and ``SettleFit``;
365
+ ``book.front_share``, ``front_start_gal``, ``lower_after_draw_f`` and
366
+ ``front_cannot_move``; ``physics.draw.settled_lower_f`` and
367
+ ``unsettled_after_f``; ``HeatLedger.front_moves_at``,
368
+ ``LedgerZones.settle_min`` and ``inlet_f``, ``AfterDraw.lower_f``,
369
+ ``spent`` and ``inlet_f``, ``UpperAhead.inlet_f`` and
370
+ ``DrawTaken.heating``; and ``draw_lower`` of ``TankPhysics.coast``
371
+ and ``decay.project_decay``. ``simulate``'s ``draw_hold_factor`` is
372
+ gone too: it held a recovery for a share of each draw the probes were
373
+ taken to miss, which nothing measured. A recovery model now reads
374
+ ``lead_draws`` without it. A parameters document that still carries
375
+ the removed ``book`` terms loads, and ``unknown_fields`` doesn't name
376
+ them.
377
+
378
+ - **A Home Assistant restart no longer loses the heat book.** A restart
379
+ leaves the heater and the flow unknown for a few minutes, and the book
380
+ broke there: there was no book until the next anchor, often hours
381
+ later. ``book_from_changes`` and ``HomeAssistantBook`` now walk across
382
+ a span the heater wasn't heard or an entity was unavailable, holding
383
+ each value from before it, as if it were seen. The flow holds a
384
+ minute and the pump's speed five, so a draw or a run going in doesn't
385
+ run on for the whole outage.
386
+
387
+ - **Readiness reads the tank from its probes.** Without a heat book,
388
+ the deliverable gallons are now the tank's heat as its two probes
389
+ place it, as the book's anchor reads it, plus ``delivered_lag_gal``,
390
+ for any state with the upper probe at or above the line. A tank whose
391
+ probes read full no longer has 13.5 gal taken off; a tank with its
392
+ probes apart reads its probes' heat instead of 0; a cold lower probe
393
+ no longer caps the answer at 6.5 gal; and a running heater no longer
394
+ holds the answer back, now or along a projected recovery. With
395
+ evidence, the answer is the larger of the evidence's and the probes'.
396
+ On one installation, a drawdown from 136.4/111.4 °F delivered 31.1 gal
397
+ where this says 34.5, and two from uniform tanks matched it.
398
+ ``sim.delivery.probes_gal`` replaces ``full_tank_floor_gal`` and
399
+ ``hot_to_the_lower_probe``. Removed: ``ReadinessParams.full_hidden_gal``,
400
+ ``cold_lower_cutoff_f``, ``cold_lower_cap_gal`` and
401
+ ``full_setpoint_band_f``; ``calibration.cold_lower_cap``;
402
+ ``Deliverable.thermocline_unlocated``, ``colder_than_evidence``,
403
+ ``no_evidence`` and ``from_probes``; ``TankNow.heating``; the
404
+ ``setpoint_f`` and ``heating`` arguments of ``delivery.deliverable``;
405
+ and the reasons ``thermocline_unlocated``, ``colder_than_all_evidence``,
406
+ ``no_evidence_for_state`` and ``estimated_from_probes``
407
+ (``ready.NO_EVIDENCE_FOR_STATE``, ``ESTIMATED_FROM_PROBES``). A
408
+ parameters document that still carries the removed parameters loads
409
+ without them (``params.RETIRED_FIELDS``).
410
+
411
+ - **The heat book anchors while the heat pump runs.** The book anchored
412
+ only when a recovery ended, so a book lost to a restart, or an old one
413
+ drifting, waited for the recovery's end, often hours. Now a step with
414
+ the heat pump running alone, both probes within ``uniform_band_f`` and
415
+ nothing drawn or circulated in it anchors too, once the heat pump has
416
+ run the new ``BookParams.anchor_run_min`` (15 minutes of its running
417
+ time) since a draw after the last anchor. The book walked along a
418
+ simulation anchors the same way. A quiet tank after a draw still
419
+ doesn't anchor: in a mixed tank the lower probe shows only about a
420
+ third of a small draw's heat.
421
+
422
+ - **A draw flow can count tank water.** The heat book read every
423
+ gallon of its flow as delivered through the mixing valve, debiting
424
+ only its hot share of tank water. A flow from a meter or a detector
425
+ on the heater's hot outlet counts tank gallons, so the book debited
426
+ about a quarter of each draw too little (a 141 °F tank behind a valve
427
+ delivering 122 °F) and promised hot water that wasn't there. ``book_from_history``,
428
+ ``book_from_changes``, ``fit_book`` and ``HomeAssistantBook`` take a
429
+ new ``flow_volume``: ``"delivered"``, the default and the reading
430
+ until now, or ``"tank"``, each gallon priced at the upper probe less
431
+ the inlet. ``simulate run`` reads it from ``[home_assistant]``'s
432
+ ``draw_flow_volume``. The shipped empty points
433
+ (``BookParams.empty_btu``, ``zone_empty_btu``) were booked as
434
+ delivered gallons, so a tank-water book takes them refit on its own
435
+ record (``calibration.empty_point``, keeping ``zone_empty_btu`` as far
436
+ below it): booked as tank water, a drawdown leaves less heat at the
437
+ line.
438
+
439
+ Removed
440
+ -------
441
+
442
+ - **The COP fit's lead share.** With a meter, the COP fit charged a
443
+ share of the draws in the 30 minutes before each cycle, because the
444
+ start reading was taken to miss their cold water. The probes see a
445
+ draw's cold water at once, and on the record the term did not earn
446
+ its place: trained on 221 metered cycles (2026-02-14 to 2026-10-24)
447
+ and scored on the latest 55, it fitted a share of 0.19 and scored a
448
+ median relative error of 0.077 against 0.082 without it, and a mean
449
+ of 0.116 against 0.112. ``calibration.metered_lead``,
450
+ ``PowerFit.lead_share``, ``CopCycle.lead_wh``, the snapshots'
451
+ ``draw_lead_gal`` and the ``lead`` arguments of ``calibrate``,
452
+ ``fit_power``, ``fit_cop_scale`` and ``cop_cycles`` are gone;
453
+ ``solve_cop_scale`` returns ``(scale, startup_wh)`` and
454
+ ``solve_cop_fit`` ``(scale, startup_wh, lift_coeff)``. A draw in the
455
+ 30 minutes before a cycle still keeps it out of the draw-free fit
456
+ (``DRAW_LEAD``). The site's COP terms were fitted without a meter
457
+ and are unchanged.
458
+
459
+ Version 9.12.1 (2026-10-23)
460
+ ===========================
461
+
462
+ Fixed
463
+ -----
464
+
465
+ - **simulate run books the tank from Home Assistant.** Given the
466
+ sources' ``[home_assistant.heater]`` table and the new
467
+ ``draw_flow`` entity, the run walks from the tank's booked heat
468
+ (``HomeAssistantBook``), and "deliverable now" reads it: a tank with
469
+ its probes apart read 0 gal from the probes alone, the history
470
+ unused.
471
+
472
+ Added
473
+ -----
474
+
475
+ - **simulate run's bath and showers.** The table gives the bath and
476
+ the whole showers the tank is ready for (``sim.time_to_ready``), and
477
+ when the heat pump has the bath ready; ``--bath`` and ``--shower``
478
+ give the volumes.
479
+
480
+ Changed
481
+ -------
482
+
483
+ - **The package's metadata is in pyproject.toml.** Name, dependencies,
484
+ extras, classifiers and the ``nwp-cli`` entry point moved from
485
+ ``setup.cfg`` to the standard ``[project]`` table, and ``setup.py`` is
486
+ gone. The built package installs the same files and requirements.
487
+
5
488
  Version 9.12.0 (2026-10-23)
6
489
  ===========================
7
490
 
@@ -111,7 +594,7 @@ Changed
111
594
  says so in ``defaults_used`` (#232). They raised in every walk, and a
112
595
  book left them unpriced. ``sim_adapters.with_circulation_terms`` does
113
596
  this for the books.
114
- - A status in Standby is Power Off, as the NaviLink app counts it (#232).
597
+ - A status in Standby counts as Power Off (#232).
115
598
  It raised.
116
599
  - ``book_from_changes`` books from the flow's first reading when its
117
600
  history starts later than the window, rather than raising (#232).
@@ -487,7 +970,7 @@ Added
487
970
  ``TOUPeriod``, the cloud's model, reading the controller's 40-slot table
488
971
  whole (a ``TOU_PLAN`` read's payload byte is the last slot, not a
489
972
  length; ``build_tou_read()``). ``write_tou_records()`` stores a plan of
490
- up to twelve intervals (``build_tou_write()``: the NaviLink app's clock
973
+ up to twelve intervals (``build_tou_write()``: an 8-byte clock
491
974
  header, then the records in the read layout) and reads it back. It
492
975
  refuses to write over a longer stored plan, whose extra intervals no
493
976
  Bluetooth write can clear, and raises ``TouWriteError`` when the
@@ -498,8 +981,8 @@ Added
498
981
  (``MSCD``) and ``SoftwareInfoBlock`` (``MSWD``) use the cloud models'
499
982
  field names for the fields matched against them.
500
983
  ``read_sub_status()`` decodes the ``SSD`` block into two DIP-switch
501
- words (``SubStatus``), using the layout Navien's boiler service toolkit
502
- reads; it is unconfirmed on the NWP500.
984
+ words (``SubStatus``), using the boiler layout; it is unconfirmed on
985
+ the NWP500.
503
986
  ``read_status()`` returns live status: the class-50 session command 200
504
987
  makes the heater push the ``RTMD`` block about every 300 ms until the
505
988
  connection closes, and ``RealTimeStatus`` decodes the fields checked
@@ -524,10 +1007,10 @@ Added
524
1007
  (``/user/get-push-settings``, ``/user/set-push-settings``),
525
1008
  ``record_filter_change()`` / ``record_cip_change()`` and
526
1009
  ``get_filter_history()`` / ``get_cip_history()`` (the cloud's maintenance
527
- log; dates are sent ``YYYY-M-D`` without zero padding, as the app does),
1010
+ log; dates are sent ``YYYY-M-D`` without zero padding),
528
1011
  ``get_display_preferences()`` / ``set_display_preferences()``
529
- (``/device/preferences/format``; the cloud rejects it for the NWP500,
530
- the app only uses it for the IoT-module hydro unit), ``get_user_profile()`` (``/user/info``),
1012
+ (``/device/preferences/format``; the cloud rejects it for the NWP500;
1013
+ it serves the IoT-module hydro unit), ``get_user_profile()`` (``/user/info``),
531
1014
  ``get_firmware_detail()`` (``/firmware/info``; ``run_time`` is the
532
1015
  estimated update time in minutes), ``delete_tou()``
533
1016
  (``DELETE /device/tou``), ``get_manufacture_id()``,
@@ -537,24 +1020,24 @@ Added
537
1020
  ``get_app_version()`` (unauthenticated; a 403 maintenance notice is
538
1021
  returned as ``server_check_text``; ``nwp500.get_app_version()`` asks
539
1022
  before sign-in, without a client). ``set_push_settings()`` keeps the
540
- current value of a flag left ``None``, since the app always sends
541
- every flag. ``get_error_history()`` entries carry the list's
1023
+ current value of a flag left ``None``, since the cloud expects every
1024
+ flag. ``get_error_history()`` entries carry the list's
542
1025
  ``timeZone`` (``time_zone``, ``occurred_at_local``). New models: ``PushSettings``,
543
1026
  ``DeviceInstaller``, ``FirmwareDeviceGroup``, ``FirmwareDetail``,
544
1027
  ``DisplayPreferences``, ``SimpleDevice``, ``DeviceErrorHistoryEntry``,
545
1028
  ``MaintenanceHistoryEntry``, ``UserProfile``, ``AppVersionInfo``. Push
546
1029
  flags are 1 = on, 2 = off, the reverse of the device's on/off flags, and
547
1030
  decode to ``True``/``False``/``None``. ``Device.installer`` and
548
- ``Device.alarm_info`` are parsed from ``/device/info`` as the app does;
1031
+ ``Device.alarm_info`` are parsed from ``/device/info``;
549
1032
  ``TOUSchedule.nickname`` is modelled.
550
1033
  - **AWS credentials renew without the password.**
551
1034
  ``NavienAuthClient.refresh_session()`` posts to ``/auth/session/refresh``
552
1035
  with the access token and updates only the AWS credential fields the
553
- server returns, as the app does when the MQTT connection fails with an
554
- auth error. ``secured_sign_in()`` posts to ``/user/secured-sign-in`` with
1036
+ server returns; it is the step used when the MQTT connection fails with
1037
+ an auth error. ``secured_sign_in()`` posts to ``/user/secured-sign-in`` with
555
1038
  the access token and overlays whatever token fields come back (observed
556
- live: the AWS credentials only; the JWTs are kept), which the app uses
557
- for auto-login. When the AWS credentials expire, ``ensure_valid_token()``
1039
+ live: the AWS credentials only; the JWTs are kept), the auto-login
1040
+ step. When the AWS credentials expire, ``ensure_valid_token()``
558
1041
  and the context manager's stored-token check now try session refresh,
559
1042
  then secured sign-in, and only then the password sign-in; both token
560
1043
  steps are skipped when the access token has expired and cannot be
@@ -569,14 +1052,14 @@ Added
569
1052
  control command it publishes on ``cmd/52/navilink-{mac}/res/ctrl-fail``
570
1053
  instead of acknowledging it; the library silently dropped that message.
571
1054
  It is now parsed into ``ControlFailResponse``
572
- (``ControlFailCode.DHW_CONTROL_INTERVAL_EXCEEDED`` = 2 is the only code
573
- the app interprets: the previous DHW control was too recent) and emitted
1055
+ (``ControlFailCode.DHW_CONTROL_INTERVAL_EXCEEDED`` = 2 is the only known
1056
+ code: the previous DHW control was too recent) and emitted
574
1057
  as the ``control_failed`` client event (``ControlFailedEvent`` with
575
1058
  ``own_command`` and ``reason``) for every device subscription, with a
576
1059
  WARNING log. ``subscribe_control_fail_response()`` /
577
1060
  ``unsubscribe_control_fail_response()`` add a plain callback.
578
1061
  - **MQTT Last-Will and app-connection presence.** Set
579
- ``MqttConnectionConfig.last_will_device`` to register the app's Last-Will
1062
+ ``MqttConnectionConfig.last_will_device`` to register a Last-Will
580
1063
  on ``evt/52/navilink-{mac}/app-connection`` (``event.connection.status``
581
1064
  0, QoS 1) so other clients learn when this one drops; every rebuilt
582
1065
  connection carries it. ``subscribe_app_connection()`` /
@@ -584,24 +1067,22 @@ Added
584
1067
  ``AppConnectionEvent`` and emit ``app_connection_received``
585
1068
  (``AppConnectionReceivedEvent``). New ``MqttTopicBuilder`` helpers
586
1069
  ``control_fail_response_topic()`` and ``app_connection_topic()``.
587
- - **``DeviceStatus.air_filter_remaining_hours``**: the app's remaining
1070
+ - **``DeviceStatus.air_filter_remaining_hours``**: the remaining
588
1071
  filter life (``None`` when the reminder period is 0, else clamped at 0).
589
- - **``DeviceStatus.minor_code``** (``minorCode``): the secondary alarm code
590
- the app shows as ``R%03d``; 101 is the one it lets the user reset.
1072
+ - **``DeviceStatus.minor_code``** (``minorCode``): the secondary alarm code,
1073
+ formatted as ``R%03d``; 101 is the one a user can reset.
591
1074
  Defaults to 0 for payloads that omit it.
592
- - **``DhwOperationSetting.STANDBY`` (0)**, the app's
593
- ``MgppOperationMode.STANDBY``. ``DeviceStatus.dhw_operation_setting``
1075
+ - **``DhwOperationSetting.STANDBY`` (0)**, the device's Standby mode.
1076
+ ``DeviceStatus.dhw_operation_setting``
594
1077
  previously rejected a 0. It is status-only: ``set_dhw_mode()`` refuses
595
1078
  it, as it now refuses any id outside 1-6. ``sim_adapters.settings()``
596
1079
  refuses a Standby status, which the simulator does not model.
597
- - **``DREvent.GRID_EMERGENCY`` (6).** The app declares CPE and Grid
598
- Emergency both as id 5 but decodes ``drEventStatus`` by array position,
599
- so raw 6 is Grid Emergency; ``CPE`` (5) stays "Customer Peak Event".
1080
+ - **``DREvent.GRID_EMERGENCY`` (6).** Raw 6 is Grid Emergency; ``CPE`` (5) stays "Customer Peak Event".
600
1081
  - **``UnitType``** NCB700 (22), NFB700_H (24), CAS_NFB700_H (25),
601
1082
  NPF_SYSTEM (514); **``DeviceType``** IOT_MODULE (8), NFB700_MAIN (50),
602
- NFB700_SUB (51) (the app's names; ``NPF700_MAIN``/``NPF700_SUB`` remain
603
- as aliases) and ``HPWH`` as an alias of 52, which is the NWP500
604
- (``DefineData.HPWH_MODEL``), not an NPF700 WiFi module. A dumped device
1083
+ NFB700_SUB (51) (``NPF700_MAIN``/``NPF700_SUB`` remain as aliases) and
1084
+ ``HPWH`` as an alias of 52, which is the NWP500, not an NPF700 WiFi
1085
+ module. A dumped device
605
1086
  of type 50 or 51 now names it ``NFB700_MAIN`` or ``NFB700_SUB``.
606
1087
  - **A recovery model's inputs, through ``nwp500.sim.simulate``** (#165).
607
1088
  Beside ``recovery=``, ``simulate`` takes what a learned model was
@@ -955,11 +1436,10 @@ Added
955
1436
 
956
1437
  Changed
957
1438
  -------
958
- - **Recirculation mode values follow the NaviLink app.** ``RecirculationMode``
959
- had 2 and 3 swapped and 4 wrong: the app's
960
- ``MgppRecirculationOperationMode`` (sent by ``recirc-mode`` and decoded
961
- from ``recircOperationMode``) is 1 Always on, 2 Weekly, 3 HotButton only,
962
- 4 No recirculation. The members are now ``ALWAYS_ON``, ``WEEKLY``,
1439
+ - **Recirculation mode values corrected.** ``RecirculationMode``
1440
+ had 2 and 3 swapped and 4 wrong: the recirculation mode (sent by
1441
+ ``recirc-mode`` and decoded from ``recircOperationMode``) is 1 Always
1442
+ on, 2 Weekly, 3 HotButton only, 4 No recirculation. The members are now ``ALWAYS_ON``, ``WEEKLY``,
963
1443
  ``HOT_BUTTON_ONLY`` and ``OFF``; ``ALWAYS`` remains as an alias of
964
1444
  ``ALWAYS_ON``. **Behaviour change:** ``BUTTON``, ``SCHEDULE`` and
965
1445
  ``TEMPERATURE`` are removed, since their meanings were wrong; code that
@@ -970,7 +1450,7 @@ Changed
970
1450
  ``"ALWAYS"``) and ``"WEEKLY"`` for 2 (was ``"BUTTON"``); code keyed on
971
1451
  the old strings must follow.
972
1452
  - **Reservation mode 5 is Power Off, not Vacation.** Weekly-schedule
973
- entries carry the app's ``MgppReservationMode`` (0 not reserved, 1 Heat
1453
+ entries carry a reservation mode (0 not reserved, 1 Heat
974
1454
  Pump, 2 Electric, 3 Energy Saver, 4 High Demand, 5 Power Off; there is
975
1455
  no vacation entry), now the ``ReservationMode`` enum.
976
1456
  ``ReservationEntry.mode_name`` resolves through it, so a mode-5 entry
@@ -984,20 +1464,19 @@ Changed
984
1464
  (``update_reservations(kept=)``). Changing such an entry needs a mode
985
1465
  of 1-5. The simulator's reservation entries take the same numbering
986
1466
  (``sim.inputs.EntryMode``); only 1-4 are modelled, as before.
987
- - **``dr_override_status`` is an on/off flag, not hours.** The app decodes
988
- it with its 0/1/2 on-off enum and shows "DR applied" when it is 2. The
1467
+ - **``dr_override_status`` is an on/off flag, not hours.** It is a 0/1/2
1468
+ on-off value, and 2 means DR applied. The
989
1469
  field stays an ``int`` but no longer carries an ``hours`` unit; the new
990
1470
  ``DeviceStatus.dr_override_active`` is True when it is 2.
991
- - **Air-filter life comes from ``cumulated_op_time_eva_fan``.** The app
992
- computes remaining filter life as ``air_filter_alarm_period -
993
- cumulated_op_time_eva_fan`` and never reads ``air_filter_alarm_elapsed``.
1471
+ - **Air-filter life comes from ``cumulated_op_time_eva_fan``.** Remaining
1472
+ filter life is ``air_filter_alarm_period - cumulated_op_time_eva_fan``;
1473
+ ``air_filter_alarm_elapsed`` is not part of it.
994
1474
  The two descriptions were the wrong way round; ``cumulated_op_time_eva_fan``
995
1475
  is fan hours since the last filter reset and ``air_filter_alarm_elapsed``
996
1476
  is unconfirmed.
997
- - **``signal_app_connection()`` sends the app's message shape.** It
1477
+ - **``signal_app_connection()`` sends the Last-Will message shape.** It
998
1478
  published ``{clientID, timestamp}``, a shape nothing reads. It now
999
- publishes the NaviLink app's ``RequestLastWill`` envelope with
1000
- ``event.connection.status`` 1.
1479
+ publishes the Last-Will envelope with ``event.connection.status`` 1.
1001
1480
  - Sign-in and ``/auth/refresh`` requests now send the library's
1002
1481
  ``User-Agent`` and ``Content-Type`` headers, as every other request does.
1003
1482
  - **The heater's own air reading is not as close as documented in
@@ -1029,15 +1508,14 @@ Changed
1029
1508
  describe both fields as differences.
1030
1509
  - **Vacation accepts 1-99 days** (#155). ``set_dhw_mode()`` (vacation),
1031
1510
  ``set_vacation_days()`` and ``set_vacation_duration()`` rejected anything
1032
- over 30 days. The NaviLink app's vacation picker and the front panel
1033
- (User's Information Manual, section 3.4.5) both offer 1-99, so the
1034
- library now accepts the same. The ``vacation`` and
1511
+ over 30 days. The front panel (User's Information Manual, section
1512
+ 3.4.5) offers 1-99, so the library now accepts the same. The ``vacation`` and
1035
1513
  ``vacation-duration`` CLI commands follow.
1036
1514
 
1037
1515
  Fixed
1038
1516
  -----
1039
- - ``FirmwareInfo.device_group`` was typed as a string; the app reads an
1040
- object ``{groupSeq, unitNum}`` (plus a top-level ``deviceGroupUnitNo``).
1517
+ - ``FirmwareInfo.device_group`` was typed as a string; the cloud returns
1518
+ an object ``{groupSeq, unitNum}`` (plus a top-level ``deviceGroupUnitNo``).
1041
1519
  It is now ``FirmwareDeviceGroup``, with a bare string still accepted.
1042
1520
  - ``convert_tou()`` hard-coded ``userType: "O"``; it now sends the
1043
1521
  signed-in user's type from the sign-in response, defaulting to ``"O"``.
@@ -1072,8 +1550,6 @@ Documentation
1072
1550
  physics with a learned correction, and are removed. The "cold starts
1073
1551
  about 55 min short" figure is replaced by the recorded errors by time
1074
1552
  left. The lower trigger's in-window wait is given with its full count.
1075
- - ``unknown-values``, ``tank-energy`` and the MQTT protocol reference now
1076
- cite the current NaviLink app release they were checked against.
1077
1553
  - ``docs/openapi.yaml`` and ``rest_api.rst`` describe every endpoint above
1078
1554
  plus ``POST /auth/session/refresh`` and ``POST /user/secured-sign-in``;
1079
1555
  ``mqtt_protocol.rst`` documents ``res/ctrl-fail`` and the
@@ -1111,13 +1587,12 @@ Documentation
1111
1587
  - The ambient effect on the compressor is about 1.18x, not 1.7x; the
1112
1588
  standby soak's figures, the 60 h gap prior and the circulation runs
1113
1589
  behind the lower-zone drop are stated as measured.
1114
- - **Docs that contradicted the code or the vendor's sources** (#155). Each
1590
+ - **Docs that contradicted the code or the protocol** (#155). Each
1115
1591
  now has one value with its source:
1116
1592
 
1117
1593
  - The mode-ID table in ``device_features.rst`` numbered Energy Saver 2,
1118
1594
  High Demand 3 and Electric 4. The IDs are Electric 2, Energy Saver 3,
1119
- High Demand 4, as in ``DhwOperationSetting`` and the NaviLink app's
1120
- ``MgppOperationMode``.
1595
+ High Demand 4, as in ``DhwOperationSetting``.
1121
1596
  - ``track-energy.rst`` used ``comp_running_minute_total`` and
1122
1597
  ``heater1/2_running_minute_total``, which do not exist. Lifetime run
1123
1598
  times come from the diagnostics query, in hours
@@ -1148,12 +1623,11 @@ Documentation
1148
1623
  - **Flow-rate units corrected** (#154). The protocol reference said
1149
1624
  ``currentDhwFlowRate`` and ``recircDhwFlowRate`` were tenths of a GPM and
1150
1625
  ``cumulatedDhwFlowRate`` was gallons, while the properties treated them as
1151
- tenths of an LPM and as litres. The properties are right. Navien's
1152
- NaviLink app (2.01.02, decompiled) shows ``currentDhwFlowRate`` as
1153
- ``raw / 10`` LPM in Celsius and ``raw / 3.785 / 10`` GPM in Fahrenheit.
1626
+ tenths of an LPM and as litres. The properties are right.
1627
+ ``currentDhwFlowRate`` is ``raw / 10`` LPM in Celsius and
1628
+ ``raw / 3.785 / 10`` GPM in Fahrenheit.
1154
1629
  The NWP500 manual lists the panel's water-flow reading in LPM even though
1155
- the panel defaults to GPM. The app's diagnostics screen shows the heater's
1156
- hot-water volume totals in whole litres in either setting. Flow rates are
1630
+ the panel defaults to GPM. Flow rates are
1157
1631
  0.1 LPM and the cumulative counter is whole litres in both unit systems.
1158
1632
  ``device_status.rst``, ``data_conversions.rst`` and ``mqtt_protocol.rst``
1159
1633
  now say so, and ``DiagnosticsDhwUsage`` documents its units (litres, hours,
@@ -1164,7 +1638,7 @@ Documentation
1164
1638
  fixed ``"GPM"`` or ``"gal"`` ``unit_of_measurement``, which contradicted
1165
1639
  their raw units. ``get_field_unit()``, which was already driven by
1166
1640
  ``device_class``, returns the same units as before. New tests pin both
1167
- flow properties to the app's formula in each unit system.
1641
+ flow properties to these formulas in each unit system.
1168
1642
 
1169
1643
  Version 9.4.3 (2026-10-01)
1170
1644
  ==========================
@@ -1335,31 +1809,30 @@ Fixed
1335
1809
  Version 9.4.0 (2026-09-12)
1336
1810
  ==========================
1337
1811
 
1338
- This release realigns the MQTT command set with what the NaviLink app
1339
- sends. The app (version 2.03.00) was decompiled and its request
1340
- builder compared with the library; every query it sends now has a
1341
- method, six commands it never sends were removed, and the recirculation
1342
- schedule write was corrected. Breaking changes are listed under
1812
+ This release realigns the MQTT command set with the NaviLink protocol.
1813
+ Every query in the protocol now has a method, six commands that are not
1814
+ part of it were removed, and the recirculation schedule write was
1815
+ corrected. Breaking changes are listed under
1343
1816
  *Removed* and *Changed*.
1344
1817
 
1345
1818
  Added
1346
1819
  -----
1347
1820
  - **Installer diagnostics query.** ``request_diagnostics()`` /
1348
1821
  ``subscribe_diagnostics()`` and the ``nwp-cli diagnostics`` command
1349
- return the lifetime counters the app's installer screen shows: heat
1822
+ return the device's lifetime installer counters: heat
1350
1823
  pump and element energy, days since installation, fault event counts,
1351
1824
  demand-response operation times, hot-water draw statistics and
1352
1825
  component run times and start counts, as
1353
1826
  :class:`~nwp500.models.DeviceDiagnostics`. The device answers the
1354
1827
  ``st/td/rd`` query with packed hex; the NaviLink cloud decodes it into
1355
- JSON only when the reply topic has the app's five-segment form, so the
1828
+ JSON only when the reply topic has the five-segment form, so the
1356
1829
  library requests that form. Verified live: the energies and compressor
1357
1830
  and element run times match the energy query's lifetime totals.
1358
1831
  - **Monthly energy query.** ``request_energy_usage_monthly()`` /
1359
1832
  ``subscribe_energy_usage_monthly()`` and ``nwp-cli energy --years``
1360
1833
  return one entry per year with twelve per-month items. Verified live.
1361
1834
  - **Hourly energy query.** ``request_energy_usage_hourly()`` /
1362
- ``subscribe_energy_usage_hourly()`` send the app's per-hour query. The
1835
+ ``subscribe_energy_usage_hourly()`` send the per-hour query. The
1363
1836
  NWP500 tested never answered it in four attempts, so it has no CLI
1364
1837
  command.
1365
1838
  - **Recirculation schedule read.** ``request_recirculation_schedule()``
@@ -1376,46 +1849,43 @@ Added
1376
1849
  ``subscribe_firmware_download_info()`` and ``nwp-cli firmware info``
1377
1850
  return :class:`~nwp500.models.FirmwareDownloadInfo`. The device replies
1378
1851
  on its own topic rather than a client-keyed one. Verified live.
1379
- - **Session end.** ``end_session()`` sends the ``st/end`` query the app
1380
- sends whenever it leaves a device. No reply to it has been observed.
1852
+ - **Session end.** ``end_session()`` sends the ``st/end`` query that
1853
+ ends a session with a device. No reply to it has been observed.
1381
1854
  Unlike other commands it is never queued while disconnected (it raises
1382
1855
  ``MqttNotConnectedError``), since a session end replayed after a
1383
1856
  reconnect would end the new session.
1384
1857
  - ``MqttConnectionConfig.send_session_end_on_disconnect`` (default
1385
1858
  ``True``) controls whether ``disconnect()`` sends it.
1386
1859
  - ``subscribe_firmware_commit_response()`` receives the reply to
1387
- ``commit_firmware_update()``, which is requested on the app-form
1860
+ ``commit_firmware_update()``, which is requested on the five-segment
1388
1861
  ``res/commit-ota`` topic that no other subscription covers.
1389
- - **Controls with app builder cases that had no method:**
1390
- ``set_vacation_duration(days)`` (``goout-day``; the app has a builder
1391
- case but no screen that calls it; verified live to change the day count
1392
- without changing the operation mode),
1862
+ - **Controls that had no method:**
1863
+ ``set_vacation_duration(days)`` (``goout-day``; verified live to change
1864
+ the day count without changing the operation mode),
1393
1865
  ``set_air_filter_life(hours)`` (``air-filter-life``; the wire value is
1394
- hours divided by 500, matching the app's 0 or 1000-10000 picker;
1866
+ hours divided by 500, for 0 or 1000-10000 hours;
1395
1867
  verified live) and ``reset_condenser_fault()`` (``cond-fault-reset``,
1396
- installer-level in the app; acknowledged live, clearing effect not
1868
+ an installer-level control; acknowledged live, clearing effect not
1397
1869
  observable without a fault). CLI: ``vacation-duration``,
1398
1870
  ``filter-life``, ``reset-condenser-fault``.
1399
- - **Command codes** for every member of the app's ``DeviceControlMGPP``
1400
- enum, with the declared-but-never-sent ones annotated.
1871
+ - **Command codes** for every known control command, with the
1872
+ declared-only ones annotated.
1401
1873
  - ``EnergyUsageResponse.type_of_usage``, ``get_year_data()``,
1402
1874
  ``MonthlyEnergyData.day`` and ``EnergyUsageDay.ep_usage`` /
1403
- ``water_usage`` (declared by the app; not reported by the NWP500).
1875
+ ``water_usage`` (part of the protocol; not reported by the NWP500).
1404
1876
 
1405
1877
  Changed
1406
1878
  -------
1407
- - **Recirculation schedule models take the app's entry shape** (breaking).
1879
+ - **Recirculation schedule models take the reservation entry shape** (breaking).
1408
1880
  :class:`~nwp500.models.RecirculationScheduleEntry` is now
1409
1881
  ``enable``/``week``/``hour``/``min``/``mode``/``param`` like a
1410
- reservation entry (``mode`` is the entry's pump on/off toggle in the
1411
- app's editor) and :class:`~nwp500.models.RecirculationSchedule` carries
1882
+ reservation entry (``mode`` is the entry's pump on/off toggle) and :class:`~nwp500.models.RecirculationSchedule` carries
1412
1883
  ``reservation_use`` plus ``reservation`` instead of ``schedule``. The
1413
1884
  previous start/end window fields were not what the device accepts.
1414
1885
  - ``MonthlyEnergyData.month`` is optional: monthly-query entries have no
1415
1886
  month.
1416
1887
  - **``NavienMqttClient.disconnect()`` now sends ``st/end``** to every
1417
- device that was passed to any subscription, as the NaviLink app does
1418
- when it leaves a device. The publishes go straight to the connection,
1888
+ device that was passed to any subscription. The publishes go straight to the connection,
1419
1889
  never into the offline command queue, stop if the connection drops,
1420
1890
  are bounded by a short timeout and never fail the disconnect. What it
1421
1891
  does to other clients connected to the same device (for example a Home
@@ -1439,12 +1909,12 @@ Fixed
1439
1909
  -----
1440
1910
  - **``configure_recirculation_schedule()`` published to the wrong topic
1441
1911
  with the wrong keys.** It sent ``{"schedule": [...]}`` on the bare
1442
- ``ctrl`` topic; the app publishes RECIR_RESERVATION on
1443
- ``ctrl/recirc-rsv/rd`` with ``reservationUse`` and ``reservation`` and
1444
- asks for the echo on ``recirc-rsv/rd``.
1912
+ ``ctrl`` topic; RECIR_RESERVATION goes on ``ctrl/recirc-rsv/rd``
1913
+ with ``reservationUse`` and ``reservation`` and asks for the echo on
1914
+ ``recirc-rsv/rd``.
1445
1915
  - **``commit_firmware_update()`` published to the bare ``ctrl`` topic.**
1446
- The app publishes OTA_COMMIT on ``ctrl/commit-ota`` and asks for the
1447
- reply on ``res/commit-ota``; the library now does the same. Not
1916
+ OTA_COMMIT goes on ``ctrl/commit-ota`` and asks for the reply on
1917
+ ``res/commit-ota``; the library now does the same. Not
1448
1918
  exercised against a device.
1449
1919
  - **Unsubscribing could remove the wrong handler.** When one callback was
1450
1920
  registered for more than one device-wildcard subscription (status,
@@ -1458,8 +1928,8 @@ Fixed
1458
1928
  - Recirculation read-backs in hex carry the unused ``param`` as ``0xFF``
1459
1929
  and in JSON as ``-1``; the model normalizes to ``-1`` so the two compare
1460
1930
  equal.
1461
- - **Protocol documentation** rewritten from the decompiled request
1462
- builder and live captures: real topic structure, the anti-legionella
1931
+ - **Protocol documentation** rewritten from the protocol and live
1932
+ captures: real topic structure, the anti-legionella
1463
1933
  modes (``anti-leg-on [days]`` / ``anti-leg-off``, not
1464
1934
  ``anti-legionella-setting``), the message example that paired the
1465
1935
  weekly-reservation code with ``dhw-temperature``, the undocumented
@@ -1476,17 +1946,16 @@ Fixed
1476
1946
 
1477
1947
  Removed
1478
1948
  -------
1479
- - **Six MQTT commands the NaviLink app never sends** (breaking):
1949
+ - **Six MQTT commands that are not part of the protocol** (breaking):
1480
1950
  ``check_firmware_update()``, ``reconnect_wifi()``, ``reset_wifi()``,
1481
1951
  ``set_freeze_protection_temperature()``, ``run_smart_diagnostic()`` and
1482
1952
  ``update_weekly_reservation()`` with
1483
1953
  ``subscribe_weekly_reservation_response()`` and the
1484
1954
  ``WeeklyReservationEntry`` / ``WeeklyReservationSchedule`` models. The
1485
- app's enum declares the first five codes but its request builder has
1486
- no case for them, so the library's ``ota-check``, ``wifi-reconnect``,
1487
- ``wifi-reset``, ``frez-temp`` and ``smart-diagnostic`` mode strings were
1488
- invented. The app's weekly schedule is the ordinary reservation
1489
- program: it writes with 16777226 on ``ctrl/rsv/rd``
1955
+ first five codes are declared only, so the library's ``ota-check``,
1956
+ ``wifi-reconnect``, ``wifi-reset``, ``frez-temp`` and
1957
+ ``smart-diagnostic`` mode strings were invented. The weekly schedule is
1958
+ the ordinary reservation program: it is written with 16777226 on ``ctrl/rsv/rd``
1490
1959
  (``update_reservations()``), never with 33554438 or a ``rsv-weekly/rd``
1491
1960
  topic. The ``CommandCode`` members stay, annotated as declared only.
1492
1961
 
@@ -1552,12 +2021,10 @@ Removed
1552
2021
  waited for a reply on ``res/tou/rd``. No reply ever comes: a live device
1553
2022
  with TOU provisioned (``program_reservation_use`` true, a valid controller
1554
2023
  serial, and a plan the REST API returns in full) stayed silent for 45
1555
- seconds. ``ctrl/tou/rd`` with that command code is the *write* - it is what
1556
- :meth:`~nwp500.NavienMqttClient.configure_tou_schedule` publishes and what
1557
- the vendor app publishes from its TOU editor
1558
- (``TouScheduleViewmodel.setPublishMgppControlTou``), and the device answers
1559
- on ``res/tou/rd`` only to confirm such a write. The vendor app reads TOU
1560
- over REST, which is the only TOU read the protocol has.
2024
+ seconds. ``ctrl/tou/rd`` with that command code is the *write*: it is what
2025
+ :meth:`~nwp500.NavienMqttClient.configure_tou_schedule` publishes, and the
2026
+ device answers on ``res/tou/rd`` only to confirm such a write. REST is the
2027
+ only TOU read the protocol has.
1561
2028
 
1562
2029
  Beyond returning nothing, the call published a write-shaped command with no
1563
2030
  schedule attached. This device ignored it (``touStatus`` was unchanged
@@ -1598,10 +2065,9 @@ Fixed
1598
2065
  unknown keys, so the field silently reported its ``False`` default on
1599
2066
  every device regardless of whether a thermostatic mixing valve was
1600
2067
  supported. The CLI's "Mixing Valve" capability row was wrong for the
1601
- same reason. The misspelling is Navien's own: it appears in the vendor
1602
- app's ``KDResponseMgppDid$Feature`` class and in
1603
- ``docs/reference/protocol/device_features.rst``, which documented the
1604
- wire spelling correctly all along.
2068
+ same reason. The misspelling is Navien's own:
2069
+ ``docs/reference/protocol/device_features.rst`` documented the wire
2070
+ spelling correctly all along.
1605
2071
 
1606
2072
  - **``firmware_payload_capture.py`` captured almost nothing.** Its wildcards
1607
2073
  covered ``cmd/{type}/{client_id}/res/#`` and the event topic, but the device
@@ -1677,10 +2143,6 @@ Changed
1677
2143
  - **Status flags now preserve the device's unknown state.** The protocol
1678
2144
  encodes these flags as ``0 = unknown, 1 = OFF, 2 = ON``, and the library
1679
2145
  was collapsing 0 to ``False``, inventing an OFF the device never claimed.
1680
- Confirmed against Navien's own NaviLink app (2.03.00, versionCode 141),
1681
- which decodes exactly this set of fields through an enum declared
1682
- ``UNKNOWN(0), OFF(1), ON(2)``; two sibling enums render their zero as
1683
- ``"-"`` and ``"Not Applied"`` rather than as an off state.
1684
2146
 
1685
2147
  Affected: ``operation_busy``, ``comp_use``, ``anti_legionella_use``,
1686
2148
  ``anti_legionella_operation_busy``, ``heat_upper_use``, ``heat_lower_use``,
@@ -1692,7 +2154,7 @@ Changed
1692
2154
  this is the wanted shape: ``None`` renders as "Unknown" instead of writing
1693
2155
  a fabricated OFF into the recorder database.
1694
2156
 
1695
- - ``OnOffFlag`` gains the vendor's ``UNKNOWN = 0`` member. It previously
2157
+ - ``OnOffFlag`` gains an ``UNKNOWN = 0`` member. It previously
1696
2158
  started at ``OFF = 1``, leaving the device's reserved value unrepresented.
1697
2159
 
1698
2160
  - The CLI renders these flags as ``Unknown`` rather than ``No``. Both
@@ -1717,7 +2179,7 @@ Added
1717
2179
  report. ``converters.device_bool_to_python`` is unchanged and remains
1718
2180
  correct for capability flags.
1719
2181
  - New ``docs/explanation/unknown-values.rst`` recording which field families
1720
- use 0 as a sentinel and which do not, with the app evidence for each.
2182
+ use 0 as a sentinel and which do not, with the evidence for each.
1721
2183
 
1722
2184
  Removed
1723
2185
  -------
@@ -1746,8 +2208,6 @@ Fixed
1746
2208
  - ``dhwTemperature`` is documented as an outlet temperature but is
1747
2209
  measured inside the tank: it tracks ``tankUpperTemperature`` to within
1748
2210
  one 0.5 degC step, and the device has no sensor downstream of itself.
1749
- Navien's own app agrees, labelling it "DHW Temp." beside the tank
1750
- thermistors and showing ``dischargeTemperature`` separately.
1751
2211
  - ``docs/explanation/tank-energy.rst`` tabulated ``totalEnergyCapacity``
1752
2212
  and ``availableEnergyCapacity`` at ten times their raw wire values,
1753
2213
  under column headings naming the raw protocol fields. The series had
@@ -1765,19 +2225,16 @@ Fixed
1765
2225
  in the changelog, the model field description and the protocol
1766
2226
  reference.
1767
2227
  - New ``docs/explanation/tank-energy.rst`` records what the two fields
1768
- measure and the calibration evidence behind the scale change,
1769
- including that Navien's own NaviLink app (2.03.00, versionCode 141)
1770
- reads neither field, so no vendor-side corroboration exists.
1771
-
1772
- - Documented that **temperature fields carry no sentinel at all**. The app
1773
- has no out-of-band constant (no ``0xFFFF``/``-999``/``-1``), no zero-guard
1774
- in any display path, and formats whatever arrives, so a temperature of
1775
- zero means zero. This closes a recurring source of bugs where zero-as-none
2228
+ measure and the calibration evidence behind the scale change.
2229
+
2230
+ - Documented that **temperature fields carry no sentinel at all**. There
2231
+ is no out-of-band constant (no ``0xFFFF``/``-999``/``-1``), so a
2232
+ temperature of zero means zero. This closes a recurring source of bugs where zero-as-none
1776
2233
  was applied to temperature converters and reported working sensors as
1777
2234
  missing during cold-weather operation.
1778
- - Documented that capability flags are a distinct case: the app hides a
1779
- feature's entire UI when its DID ``Use`` flag reads 0, so 0 there means
1780
- "not fitted" and the existing ``bool`` mapping is correct.
2235
+ - Documented that capability flags are a distinct case: a DID ``Use``
2236
+ flag of 0 means "not fitted", so the existing ``bool`` mapping is
2237
+ correct.
1781
2238
 
1782
2239
  Version 9.2.1 (2026-07-30)
1783
2240
  ==========================
@@ -2071,8 +2528,8 @@ Bug Fixes
2071
2528
  (default 300 s, ``None`` to disable) are now discarded at send time.
2072
2529
  - **Fix Fahrenheit conversion for sub-zero temperatures** (ASYMMETRIC
2073
2530
  formula): the rounding used Python's floored ``%``, which is always
2074
- non-negative, while the firmware/app uses a truncated remainder. Raw
2075
- ``-11`` (-5.5 °C) decoded to 22 °F instead of the app's 23 °F. Now
2531
+ non-negative, while the firmware uses a truncated remainder. Raw
2532
+ ``-11`` (-5.5 °C) decoded to 22 °F instead of 23 °F. Now
2076
2533
  uses ``math.fmod`` semantics.
2077
2534
  - **Fix freeze protection default limits**: the defaults (43/65) were
2078
2535
  Fahrenheit display values stored in raw half-Celsius fields, decoding
@@ -2501,7 +2958,7 @@ Fixed
2501
2958
  - **Vacation mode sent wrong MQTT command**: ``set_vacation_days()`` used
2502
2959
  ``CommandCode.GOOUT_DAY`` (33554466), which the device silently accepted
2503
2960
  but did not activate vacation mode; the operating mode remained unchanged.
2504
- HAR capture of the official Navien app confirms the correct command is
2961
+ A HAR capture of NaviLink traffic confirms the correct command is
2505
2962
  ``DHW_MODE`` (33554437) with ``param=[5, days]``
2506
2963
  (``DhwOperationSetting.VACATION``). The valid range has also been corrected
2507
2964
  from 1-365 to 1-30 to match the device's actual constraint.
@@ -2574,7 +3031,7 @@ Added
2574
3031
  -----
2575
3032
  - **OpenEI Client Module**: New ``OpenEIClient`` async client (``nwp500.openei``) for browsing utility rate plans from the OpenEI API by zip code. Supports listing utilities, filtering rate plans, and fetching plan details. API key read from ``OPENEI_API_KEY`` environment variable.
2576
3033
  - **Convert TOU API**: ``NavienAPIClient.convert_tou()`` sends raw OpenEI rate data to the Navien backend for server-side conversion into device-ready TOU schedules with season/week bitfields and scaled pricing.
2577
- - **Update TOU API**: ``NavienAPIClient.update_tou()`` applies a converted TOU rate plan to a device, matching the mobile app's ``PUT /device/tou`` endpoint.
3034
+ - **Update TOU API**: ``NavienAPIClient.update_tou()`` applies a converted TOU rate plan to a device through the cloud's ``PUT /device/tou`` endpoint.
2578
3035
  - **ConvertedTOUPlan Model**: New Pydantic model for parsed ``convert_tou()`` results (utility, name, schedule).
2579
3036
  - **CLI ``tou rates``**: Browse utilities and rate plans for a zip code (``nwp500 tou rates 94903``).
2580
3037
  - **CLI ``tou plan``**: View converted rate plan details with decoded pricing (``nwp500 tou plan 94903 "EV Rate A"``).
@@ -2588,8 +3045,8 @@ Changed
2588
3045
 
2589
3046
  Fixed
2590
3047
  -----
2591
- - **Week Bitfield Encoding**: Fixed MGPP week bitfield encoding to match NaviLink APK protocol. Sunday is now correctly bit 7 (128), Monday bit 6 (64), ..., Saturday bit 1 (2); bit 0 is unused. Affects all reservation and TOU schedule operations. Verified against reference captures.
2592
- - **Enable/Disable Convention**: Fixed reservation and TOU enable/disable flags to use standard device boolean convention (1=OFF, 2=ON) instead of inverted logic. This aligns with other device binary sensors and matches app behavior. Global reservation status now correctly shows DISABLED when ``reservationUse=1``.
3048
+ - **Week Bitfield Encoding**: Fixed MGPP week bitfield encoding to match the NaviLink protocol. Sunday is now correctly bit 7 (128), Monday bit 6 (64), ..., Saturday bit 1 (2); bit 0 is unused. Affects all reservation and TOU schedule operations. Verified against reference captures.
3049
+ - **Enable/Disable Convention**: Fixed reservation and TOU enable/disable flags to use standard device boolean convention (1=OFF, 2=ON) instead of inverted logic. This aligns with other device binary sensors. Global reservation status now correctly shows DISABLED when ``reservationUse=1``.
2593
3050
  - **Reservation Set Command Timeout**: Fixed ``reservations set`` subscription pattern that had extra wildcards preventing response matching. Command now receives confirmations correctly.
2594
3051
  - **Intermittent Fetch Bug**: Tightened MQTT topic filter for reservation fetch from ``/res/`` to ``/res/rsv/`` with content validation to prevent false matches on unrelated response messages.
2595
3052
 
@@ -3532,7 +3989,7 @@ Version 6.0.4 (2025-11-21)
3532
3989
  Fixed
3533
3990
  -----
3534
3991
 
3535
- - **Temperature Conversion Accuracy**: Corrected temperature conversion logic based on analysis of the decompiled mobile application. Previous conversions used approximations; new logic uses exact formulas from the app:
3992
+ - **Temperature Conversion Accuracy**: Corrected temperature conversion logic. Previous conversions used approximations; new logic uses the exact formulas the protocol uses:
3536
3993
 
3537
3994
  - Replaced ``Add20`` validator with ``HalfCelsiusToF`` for fields transmitted as half-degrees Celsius
3538
3995
  - Replaced ``DeciCelsiusToF`` with ``PentaCelsiusToF`` for fields scaled by factor of 5
@@ -4401,7 +4858,7 @@ Fixed
4401
4858
  - Power-off now uses command code ``33554433``
4402
4859
  - Power-on now uses command code ``33554434``
4403
4860
  - DHW mode control now uses command code ``33554437``
4404
- - Discovered through network traffic analysis of official app
4861
+ - Discovered through network traffic analysis
4405
4862
 
4406
4863
  - **Bug**: MQTT topic pattern matching with wildcards
4407
4864