nwp500-python 9.12.1__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 (346) hide show
  1. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/CHANGELOG.rst +561 -133
  2. {nwp500_python-9.12.1/src/nwp500_python.egg-info → nwp500_python-9.12.2}/PKG-INFO +1 -1
  3. nwp500_python-9.12.2/docs/_static/light-page.js +1 -0
  4. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/conf.py +5 -0
  5. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/explanation/draws.rst +115 -48
  6. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/explanation/heating-elements.rst +2 -2
  7. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/explanation/index.rst +1 -0
  8. nwp500_python-9.12.2/docs/explanation/simulator-methodology.rst +726 -0
  9. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/explanation/simulator.rst +79 -62
  10. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/explanation/standby-loss.rst +109 -19
  11. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/explanation/tank-energy.rst +10 -67
  12. nwp500_python-9.12.2/docs/explanation/unknown-values.rst +198 -0
  13. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/explanation/what-starts-a-recovery.rst +17 -12
  14. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/authenticate.rst +1 -1
  15. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/bluetooth.rst +18 -24
  16. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/calibrate-simulator.rst +24 -19
  17. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/diagnose-mqtt.rst +2 -3
  18. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/maintenance.rst +13 -17
  19. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/optimize-tou.rst +8 -9
  20. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/schedule-operation.rst +15 -9
  21. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/simulate-heater.rst +108 -73
  22. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/track-energy.rst +1 -1
  23. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/project/history.rst +0 -1
  24. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/configuration.rst +1 -1
  25. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/enumerations.rst +11 -14
  26. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/protocol/data_conversions.rst +8 -8
  27. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/protocol/device_features.rst +4 -4
  28. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/protocol/device_status.rst +7 -7
  29. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/protocol/mqtt_protocol.rst +76 -98
  30. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/protocol/rest_api.rst +49 -54
  31. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/python_api/api_client.rst +24 -28
  32. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/python_api/auth_client.rst +1 -1
  33. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/python_api/cli.rst +7 -8
  34. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/python_api/events.rst +2 -2
  35. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/python_api/models.rst +26 -30
  36. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/python_api/mqtt_client.rst +30 -36
  37. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/python_api/sim.rst +48 -20
  38. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/simulator-inputs.rst +349 -140
  39. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/requirements.txt +1 -0
  40. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/tutorials/getting-started.rst +5 -5
  41. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/api_client.py +39 -43
  42. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/auth.py +9 -9
  43. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/ble/client.py +5 -5
  44. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/ble/protocol.py +55 -68
  45. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/cli/__main__.py +4 -5
  46. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/cli/calibration.py +2 -2
  47. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/cli/presentation.py +4 -4
  48. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/cli/simulate.py +5 -1
  49. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/converters.py +10 -12
  50. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/encoding.py +1 -1
  51. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/enums.py +21 -27
  52. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/models/account.py +3 -3
  53. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/models/device.py +20 -25
  54. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/models/diagnostics.py +8 -9
  55. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/models/energy.py +6 -6
  56. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/models/feature.py +4 -8
  57. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/models/maintenance.py +5 -6
  58. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/models/mqtt_models.py +16 -19
  59. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/models/schedule.py +10 -14
  60. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/models/status.py +12 -19
  61. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/models/tou.py +1 -2
  62. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/_control_commands.py +2 -2
  63. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/_device_subscriptions.py +1 -1
  64. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/client.py +12 -15
  65. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/control.py +36 -48
  66. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/subscriptions.py +3 -3
  67. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/types.py +1 -1
  68. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/utils.py +7 -11
  69. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/api.py +31 -50
  70. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/attribution.py +5 -2
  71. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/book.py +322 -454
  72. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/calibration.py +328 -653
  73. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/circulation.py +50 -15
  74. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/cold_supply.py +3 -4
  75. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/controller.py +140 -59
  76. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/decay.py +72 -101
  77. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/defaults.py +50 -36
  78. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/delivery.py +74 -164
  79. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/draw_forecast.py +11 -3
  80. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/engine.py +118 -472
  81. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/extensions.py +13 -38
  82. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/heat_pump_recovery.py +3 -1
  83. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/heating_trajectory.py +3 -1
  84. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/inputs.py +3 -2
  85. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/params.py +184 -118
  86. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/physics/compressor.py +42 -17
  87. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/physics/draw.py +171 -139
  88. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/physics/refrigerant.py +43 -34
  89. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/physics/tank.py +2 -2
  90. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/ready.py +35 -89
  91. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/tank_physics.py +9 -12
  92. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/tou_triggers.py +116 -15
  93. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim_adapters.py +86 -25
  94. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim_calibrate.py +64 -93
  95. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim_home_assistant.py +62 -21
  96. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/temperature.py +2 -2
  97. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/topic_builder.py +4 -5
  98. {nwp500_python-9.12.1 → nwp500_python-9.12.2/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  99. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/SOURCES.txt +3 -0
  100. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/scm_file_list.json +3 -0
  101. nwp500_python-9.12.2/src/nwp500_python.egg-info/scm_version.json +8 -0
  102. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/_sim_site.py +0 -44
  103. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/fixtures/sim/engine_golden.json +45 -55
  104. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/fixtures/sim/heater_params.json +7 -4
  105. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/fixtures/sim/heater_params_fitted.json +17 -11
  106. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_cli_calibrate.py +3 -5
  107. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_cli_simulate.py +45 -5
  108. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_adapters.py +5 -6
  109. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_api.py +37 -47
  110. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_book.py +182 -72
  111. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_book_history.py +382 -47
  112. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_bottom_loss.py +10 -8
  113. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_calibrate.py +71 -68
  114. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_calibration.py +49 -33
  115. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_change_history.py +123 -9
  116. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_circulation.py +54 -15
  117. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_codec.py +0 -2
  118. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_controller.py +155 -28
  119. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_defaults.py +9 -16
  120. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_documents.py +0 -4
  121. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_draw_forecast.py +0 -2
  122. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_draw_loss.py +170 -58
  123. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_element_fit.py +0 -2
  124. nwp500_python-9.12.2/tests/test_sim_evaporator.py +59 -0
  125. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_fit_book.py +1 -6
  126. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_forecast.py +0 -2
  127. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_golden.py +4 -6
  128. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_home_assistant.py +26 -14
  129. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_hot_water.py +328 -38
  130. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_live.py +21 -5
  131. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_location_air.py +0 -2
  132. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_no_clock_no_state.py +3 -4
  133. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_overlay.py +0 -2
  134. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_readiness.py +147 -334
  135. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_readiness_fit.py +102 -64
  136. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_ready.py +52 -34
  137. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_ready_api.py +41 -20
  138. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_replay.py +0 -2
  139. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_tank_physics.py +13 -13
  140. nwp500_python-9.12.1/docs/explanation/unknown-values.rst +0 -264
  141. nwp500_python-9.12.1/src/nwp500_python.egg-info/scm_version.json +0 -8
  142. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/.coveragerc +0 -0
  143. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  144. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/.github/copilot-instructions.md +0 -0
  145. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/.github/workflows/ci.yml +0 -0
  146. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/.github/workflows/release.yml +0 -0
  147. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/.gitignore +0 -0
  148. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/.pre-commit-config.yaml +0 -0
  149. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/.readthedocs.yml +0 -0
  150. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/AUTHORS.rst +0 -0
  151. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/CONTRIBUTING.rst +0 -0
  152. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/LICENSE.txt +0 -0
  153. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/Makefile +0 -0
  154. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/README.rst +0 -0
  155. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/RELEASE.md +0 -0
  156. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/Makefile +0 -0
  157. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/_static/.gitignore +0 -0
  158. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/explanation/architecture.rst +0 -0
  159. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/explanation/recovery-speed.rst +0 -0
  160. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/explanation/tou-recovery-cap.rst +0 -0
  161. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/auto-recovery.rst +0 -0
  162. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/home-assistant.rst +0 -0
  163. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/index.rst +0 -0
  164. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/manage-units.rst +0 -0
  165. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/monitor-status.rst +0 -0
  166. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/how-to/queue-commands.rst +0 -0
  167. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/index.rst +0 -0
  168. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/openapi.yaml +0 -0
  169. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/project/authors.rst +0 -0
  170. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/project/changelog.rst +0 -0
  171. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/project/contributing.rst +0 -0
  172. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/project/license.rst +0 -0
  173. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/index.rst +0 -0
  174. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/installation.rst +0 -0
  175. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/protocol/error_codes.rst +0 -0
  176. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/protocol/quick_reference.rst +0 -0
  177. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/docs/reference/python_api/exceptions.rst +0 -0
  178. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/.ruff.toml +0 -0
  179. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/README.md +0 -0
  180. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/air_filter_reset.py +0 -0
  181. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/anti_legionella.py +0 -0
  182. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/auto_recovery.py +0 -0
  183. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/combined_callbacks.py +0 -0
  184. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/demand_response.py +0 -0
  185. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/device_capabilities.py +0 -0
  186. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/device_status_debug.py +0 -0
  187. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/energy_analytics.py +0 -0
  188. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/error_code_demo.py +0 -0
  189. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/firmware_payload_capture.py +0 -0
  190. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/mqtt_diagnostics.py +0 -0
  191. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/power_control.py +0 -0
  192. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/recirculation_control.py +0 -0
  193. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/reconnection_demo.py +0 -0
  194. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/reservation_schedule.py +0 -0
  195. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/simple_auto_recovery.py +0 -0
  196. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/token_restoration.py +0 -0
  197. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/tou_openei.py +0 -0
  198. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/tou_schedule.py +0 -0
  199. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/advanced/water_reservation.py +0 -0
  200. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/beginner/01_authentication.py +0 -0
  201. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/beginner/02_list_devices.py +0 -0
  202. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/beginner/03_get_status.py +0 -0
  203. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/beginner/04_set_temperature.py +0 -0
  204. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/intermediate/advanced_auth_patterns.py +0 -0
  205. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/intermediate/command_queue.py +0 -0
  206. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/intermediate/device_status_callback.py +0 -0
  207. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/intermediate/error_handling.py +0 -0
  208. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/intermediate/event_driven_control.py +0 -0
  209. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/intermediate/improved_auth.py +0 -0
  210. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/intermediate/legacy_auth_constructor.py +0 -0
  211. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  212. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/intermediate/periodic_requests.py +0 -0
  213. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/intermediate/set_mode.py +0 -0
  214. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/intermediate/vacation_mode.py +0 -0
  215. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/mask.py +0 -0
  216. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/simulator/lower-setpoint.json +0 -0
  217. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/simulator/plan.json +0 -0
  218. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/simulator/scenario.json +0 -0
  219. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/testing/periodic_device_info.py +0 -0
  220. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/testing/simple_periodic_info.py +0 -0
  221. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/testing/test_api_client.py +0 -0
  222. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/testing/test_mqtt_connection.py +0 -0
  223. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/testing/test_mqtt_messaging.py +0 -0
  224. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/examples/testing/test_periodic_minimal.py +0 -0
  225. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/pyproject.toml +0 -0
  226. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/scripts/README.md +0 -0
  227. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/scripts/bump_version.py +0 -0
  228. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/scripts/diagnose_mqtt_connection.py +0 -0
  229. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/scripts/extract_changelog.py +0 -0
  230. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/scripts/format.py +0 -0
  231. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/scripts/lint.py +0 -0
  232. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/scripts/setup-dev.py +0 -0
  233. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/scripts/validate_version.py +0 -0
  234. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/setup.cfg +0 -0
  235. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/__init__.py +0 -0
  236. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/_base.py +0 -0
  237. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/ble/__init__.py +0 -0
  238. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/ble/__main__.py +0 -0
  239. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/ble/mqtt_adapter.py +0 -0
  240. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/cli/__init__.py +0 -0
  241. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/cli/ble_backend.py +0 -0
  242. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/cli/handlers.py +0 -0
  243. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/cli/monitoring.py +0 -0
  244. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/cli/output_formatters.py +0 -0
  245. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/cli/recording.py +0 -0
  246. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/cli/rich_output.py +0 -0
  247. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/cli/token_storage.py +0 -0
  248. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/command_decorators.py +0 -0
  249. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/config.py +0 -0
  250. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/device_capabilities.py +0 -0
  251. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/device_info_cache.py +0 -0
  252. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/events.py +0 -0
  253. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/exceptions.py +0 -0
  254. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/factory.py +0 -0
  255. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/field_factory.py +0 -0
  256. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/models/__init__.py +0 -0
  257. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/models/_converters.py +0 -0
  258. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/models/firmware.py +0 -0
  259. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/__init__.py +0 -0
  260. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/command_queue.py +0 -0
  261. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/connection.py +0 -0
  262. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/diagnostics.py +0 -0
  263. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/periodic.py +0 -0
  264. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/reconnection.py +0 -0
  265. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt/state_tracker.py +0 -0
  266. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/mqtt_events.py +0 -0
  267. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/openei.py +0 -0
  268. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/py.typed +0 -0
  269. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/reservations.py +0 -0
  270. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/__init__.py +0 -0
  271. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/codec.py +0 -0
  272. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/location_air.py +0 -0
  273. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/observed.py +0 -0
  274. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/physics/__init__.py +0 -0
  275. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/physics/element.py +0 -0
  276. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/profile.py +0 -0
  277. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/record.py +0 -0
  278. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/result.py +0 -0
  279. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/schedules.py +0 -0
  280. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim/units.py +0 -0
  281. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/sim_live.py +0 -0
  282. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/status_history.py +0 -0
  283. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/tou_schedule.py +0 -0
  284. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/unit_system.py +0 -0
  285. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500/utils.py +0 -0
  286. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  287. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  288. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  289. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/requires.txt +0 -0
  290. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/src/nwp500_python.egg-info/top_level.txt +0 -0
  291. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/conftest.py +0 -0
  292. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_api_endpoints_201.py +0 -0
  293. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_api_helpers.py +0 -0
  294. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_app_queries.py +0 -0
  295. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_auth.py +0 -0
  296. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_auth_session_lifecycle.py +0 -0
  297. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_auth_session_refresh.py +0 -0
  298. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_ble_mqtt_adapter.py +0 -0
  299. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_ble_protocol.py +0 -0
  300. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_bug_fixes.py +0 -0
  301. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_canonical_schedule.py +0 -0
  302. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_cli_basic.py +0 -0
  303. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_cli_commands.py +0 -0
  304. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_cli_tristate_presentation.py +0 -0
  305. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_command_decorators.py +0 -0
  306. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_command_queue.py +0 -0
  307. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_device_capabilities.py +0 -0
  308. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_device_info_cache.py +0 -0
  309. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_device_rest_models.py +0 -0
  310. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_events.py +0 -0
  311. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_exceptions.py +0 -0
  312. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_model_converters.py +0 -0
  313. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_models.py +0 -0
  314. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_mqtt_clean_session_resume.py +0 -0
  315. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_mqtt_client_init.py +0 -0
  316. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_mqtt_control_fail.py +0 -0
  317. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_mqtt_events.py +0 -0
  318. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_mqtt_hypothesis.py +0 -0
  319. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_mqtt_last_will.py +0 -0
  320. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_mqtt_reconnection.py +0 -0
  321. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_mqtt_reconnection_storm.py +0 -0
  322. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_mqtt_reliability.py +0 -0
  323. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_multi_device.py +0 -0
  324. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_navilink_201_enums.py +0 -0
  325. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_openei.py +0 -0
  326. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_protocol_correctness.py +0 -0
  327. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_public_api.py +0 -0
  328. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_reservation_validation.py +0 -0
  329. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_reservations.py +0 -0
  330. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_cold_supply.py +0 -0
  331. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_element.py +0 -0
  332. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_import_contract.py +0 -0
  333. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_observed.py +0 -0
  334. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_profile.py +0 -0
  335. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_sim_schedules.py +0 -0
  336. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_status_history.py +0 -0
  337. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_temperature_converters.py +0 -0
  338. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_threading_model.py +0 -0
  339. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_token_storage.py +0 -0
  340. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_tou_api.py +0 -0
  341. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_tou_schedule.py +0 -0
  342. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_unit_switching.py +0 -0
  343. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_unit_system_process_wide.py +0 -0
  344. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_utility_modules.py +0 -0
  345. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tests/test_utils.py +0 -0
  346. {nwp500_python-9.12.1 → nwp500_python-9.12.2}/tox.ini +0 -0
@@ -2,6 +2,460 @@
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
+
5
459
  Version 9.12.1 (2026-10-23)
6
460
  ===========================
7
461
 
@@ -140,7 +594,7 @@ Changed
140
594
  says so in ``defaults_used`` (#232). They raised in every walk, and a
141
595
  book left them unpriced. ``sim_adapters.with_circulation_terms`` does
142
596
  this for the books.
143
- - A status in Standby is Power Off, as the NaviLink app counts it (#232).
597
+ - A status in Standby counts as Power Off (#232).
144
598
  It raised.
145
599
  - ``book_from_changes`` books from the flow's first reading when its
146
600
  history starts later than the window, rather than raising (#232).
@@ -516,7 +970,7 @@ Added
516
970
  ``TOUPeriod``, the cloud's model, reading the controller's 40-slot table
517
971
  whole (a ``TOU_PLAN`` read's payload byte is the last slot, not a
518
972
  length; ``build_tou_read()``). ``write_tou_records()`` stores a plan of
519
- up to twelve intervals (``build_tou_write()``: the NaviLink app's clock
973
+ up to twelve intervals (``build_tou_write()``: an 8-byte clock
520
974
  header, then the records in the read layout) and reads it back. It
521
975
  refuses to write over a longer stored plan, whose extra intervals no
522
976
  Bluetooth write can clear, and raises ``TouWriteError`` when the
@@ -527,8 +981,8 @@ Added
527
981
  (``MSCD``) and ``SoftwareInfoBlock`` (``MSWD``) use the cloud models'
528
982
  field names for the fields matched against them.
529
983
  ``read_sub_status()`` decodes the ``SSD`` block into two DIP-switch
530
- words (``SubStatus``), using the layout Navien's boiler service toolkit
531
- reads; it is unconfirmed on the NWP500.
984
+ words (``SubStatus``), using the boiler layout; it is unconfirmed on
985
+ the NWP500.
532
986
  ``read_status()`` returns live status: the class-50 session command 200
533
987
  makes the heater push the ``RTMD`` block about every 300 ms until the
534
988
  connection closes, and ``RealTimeStatus`` decodes the fields checked
@@ -553,10 +1007,10 @@ Added
553
1007
  (``/user/get-push-settings``, ``/user/set-push-settings``),
554
1008
  ``record_filter_change()`` / ``record_cip_change()`` and
555
1009
  ``get_filter_history()`` / ``get_cip_history()`` (the cloud's maintenance
556
- 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),
557
1011
  ``get_display_preferences()`` / ``set_display_preferences()``
558
- (``/device/preferences/format``; the cloud rejects it for the NWP500,
559
- 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``),
560
1014
  ``get_firmware_detail()`` (``/firmware/info``; ``run_time`` is the
561
1015
  estimated update time in minutes), ``delete_tou()``
562
1016
  (``DELETE /device/tou``), ``get_manufacture_id()``,
@@ -566,24 +1020,24 @@ Added
566
1020
  ``get_app_version()`` (unauthenticated; a 403 maintenance notice is
567
1021
  returned as ``server_check_text``; ``nwp500.get_app_version()`` asks
568
1022
  before sign-in, without a client). ``set_push_settings()`` keeps the
569
- current value of a flag left ``None``, since the app always sends
570
- 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
571
1025
  ``timeZone`` (``time_zone``, ``occurred_at_local``). New models: ``PushSettings``,
572
1026
  ``DeviceInstaller``, ``FirmwareDeviceGroup``, ``FirmwareDetail``,
573
1027
  ``DisplayPreferences``, ``SimpleDevice``, ``DeviceErrorHistoryEntry``,
574
1028
  ``MaintenanceHistoryEntry``, ``UserProfile``, ``AppVersionInfo``. Push
575
1029
  flags are 1 = on, 2 = off, the reverse of the device's on/off flags, and
576
1030
  decode to ``True``/``False``/``None``. ``Device.installer`` and
577
- ``Device.alarm_info`` are parsed from ``/device/info`` as the app does;
1031
+ ``Device.alarm_info`` are parsed from ``/device/info``;
578
1032
  ``TOUSchedule.nickname`` is modelled.
579
1033
  - **AWS credentials renew without the password.**
580
1034
  ``NavienAuthClient.refresh_session()`` posts to ``/auth/session/refresh``
581
1035
  with the access token and updates only the AWS credential fields the
582
- server returns, as the app does when the MQTT connection fails with an
583
- 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
584
1038
  the access token and overlays whatever token fields come back (observed
585
- live: the AWS credentials only; the JWTs are kept), which the app uses
586
- 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()``
587
1041
  and the context manager's stored-token check now try session refresh,
588
1042
  then secured sign-in, and only then the password sign-in; both token
589
1043
  steps are skipped when the access token has expired and cannot be
@@ -598,14 +1052,14 @@ Added
598
1052
  control command it publishes on ``cmd/52/navilink-{mac}/res/ctrl-fail``
599
1053
  instead of acknowledging it; the library silently dropped that message.
600
1054
  It is now parsed into ``ControlFailResponse``
601
- (``ControlFailCode.DHW_CONTROL_INTERVAL_EXCEEDED`` = 2 is the only code
602
- 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
603
1057
  as the ``control_failed`` client event (``ControlFailedEvent`` with
604
1058
  ``own_command`` and ``reason``) for every device subscription, with a
605
1059
  WARNING log. ``subscribe_control_fail_response()`` /
606
1060
  ``unsubscribe_control_fail_response()`` add a plain callback.
607
1061
  - **MQTT Last-Will and app-connection presence.** Set
608
- ``MqttConnectionConfig.last_will_device`` to register the app's Last-Will
1062
+ ``MqttConnectionConfig.last_will_device`` to register a Last-Will
609
1063
  on ``evt/52/navilink-{mac}/app-connection`` (``event.connection.status``
610
1064
  0, QoS 1) so other clients learn when this one drops; every rebuilt
611
1065
  connection carries it. ``subscribe_app_connection()`` /
@@ -613,24 +1067,22 @@ Added
613
1067
  ``AppConnectionEvent`` and emit ``app_connection_received``
614
1068
  (``AppConnectionReceivedEvent``). New ``MqttTopicBuilder`` helpers
615
1069
  ``control_fail_response_topic()`` and ``app_connection_topic()``.
616
- - **``DeviceStatus.air_filter_remaining_hours``**: the app's remaining
1070
+ - **``DeviceStatus.air_filter_remaining_hours``**: the remaining
617
1071
  filter life (``None`` when the reminder period is 0, else clamped at 0).
618
- - **``DeviceStatus.minor_code``** (``minorCode``): the secondary alarm code
619
- 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.
620
1074
  Defaults to 0 for payloads that omit it.
621
- - **``DhwOperationSetting.STANDBY`` (0)**, the app's
622
- ``MgppOperationMode.STANDBY``. ``DeviceStatus.dhw_operation_setting``
1075
+ - **``DhwOperationSetting.STANDBY`` (0)**, the device's Standby mode.
1076
+ ``DeviceStatus.dhw_operation_setting``
623
1077
  previously rejected a 0. It is status-only: ``set_dhw_mode()`` refuses
624
1078
  it, as it now refuses any id outside 1-6. ``sim_adapters.settings()``
625
1079
  refuses a Standby status, which the simulator does not model.
626
- - **``DREvent.GRID_EMERGENCY`` (6).** The app declares CPE and Grid
627
- Emergency both as id 5 but decodes ``drEventStatus`` by array position,
628
- 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".
629
1081
  - **``UnitType``** NCB700 (22), NFB700_H (24), CAS_NFB700_H (25),
630
1082
  NPF_SYSTEM (514); **``DeviceType``** IOT_MODULE (8), NFB700_MAIN (50),
631
- NFB700_SUB (51) (the app's names; ``NPF700_MAIN``/``NPF700_SUB`` remain
632
- as aliases) and ``HPWH`` as an alias of 52, which is the NWP500
633
- (``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
634
1086
  of type 50 or 51 now names it ``NFB700_MAIN`` or ``NFB700_SUB``.
635
1087
  - **A recovery model's inputs, through ``nwp500.sim.simulate``** (#165).
636
1088
  Beside ``recovery=``, ``simulate`` takes what a learned model was
@@ -984,11 +1436,10 @@ Added
984
1436
 
985
1437
  Changed
986
1438
  -------
987
- - **Recirculation mode values follow the NaviLink app.** ``RecirculationMode``
988
- had 2 and 3 swapped and 4 wrong: the app's
989
- ``MgppRecirculationOperationMode`` (sent by ``recirc-mode`` and decoded
990
- from ``recircOperationMode``) is 1 Always on, 2 Weekly, 3 HotButton only,
991
- 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``,
992
1443
  ``HOT_BUTTON_ONLY`` and ``OFF``; ``ALWAYS`` remains as an alias of
993
1444
  ``ALWAYS_ON``. **Behaviour change:** ``BUTTON``, ``SCHEDULE`` and
994
1445
  ``TEMPERATURE`` are removed, since their meanings were wrong; code that
@@ -999,7 +1450,7 @@ Changed
999
1450
  ``"ALWAYS"``) and ``"WEEKLY"`` for 2 (was ``"BUTTON"``); code keyed on
1000
1451
  the old strings must follow.
1001
1452
  - **Reservation mode 5 is Power Off, not Vacation.** Weekly-schedule
1002
- entries carry the app's ``MgppReservationMode`` (0 not reserved, 1 Heat
1453
+ entries carry a reservation mode (0 not reserved, 1 Heat
1003
1454
  Pump, 2 Electric, 3 Energy Saver, 4 High Demand, 5 Power Off; there is
1004
1455
  no vacation entry), now the ``ReservationMode`` enum.
1005
1456
  ``ReservationEntry.mode_name`` resolves through it, so a mode-5 entry
@@ -1013,20 +1464,19 @@ Changed
1013
1464
  (``update_reservations(kept=)``). Changing such an entry needs a mode
1014
1465
  of 1-5. The simulator's reservation entries take the same numbering
1015
1466
  (``sim.inputs.EntryMode``); only 1-4 are modelled, as before.
1016
- - **``dr_override_status`` is an on/off flag, not hours.** The app decodes
1017
- 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
1018
1469
  field stays an ``int`` but no longer carries an ``hours`` unit; the new
1019
1470
  ``DeviceStatus.dr_override_active`` is True when it is 2.
1020
- - **Air-filter life comes from ``cumulated_op_time_eva_fan``.** The app
1021
- computes remaining filter life as ``air_filter_alarm_period -
1022
- 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.
1023
1474
  The two descriptions were the wrong way round; ``cumulated_op_time_eva_fan``
1024
1475
  is fan hours since the last filter reset and ``air_filter_alarm_elapsed``
1025
1476
  is unconfirmed.
1026
- - **``signal_app_connection()`` sends the app's message shape.** It
1477
+ - **``signal_app_connection()`` sends the Last-Will message shape.** It
1027
1478
  published ``{clientID, timestamp}``, a shape nothing reads. It now
1028
- publishes the NaviLink app's ``RequestLastWill`` envelope with
1029
- ``event.connection.status`` 1.
1479
+ publishes the Last-Will envelope with ``event.connection.status`` 1.
1030
1480
  - Sign-in and ``/auth/refresh`` requests now send the library's
1031
1481
  ``User-Agent`` and ``Content-Type`` headers, as every other request does.
1032
1482
  - **The heater's own air reading is not as close as documented in
@@ -1058,15 +1508,14 @@ Changed
1058
1508
  describe both fields as differences.
1059
1509
  - **Vacation accepts 1-99 days** (#155). ``set_dhw_mode()`` (vacation),
1060
1510
  ``set_vacation_days()`` and ``set_vacation_duration()`` rejected anything
1061
- over 30 days. The NaviLink app's vacation picker and the front panel
1062
- (User's Information Manual, section 3.4.5) both offer 1-99, so the
1063
- 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
1064
1513
  ``vacation-duration`` CLI commands follow.
1065
1514
 
1066
1515
  Fixed
1067
1516
  -----
1068
- - ``FirmwareInfo.device_group`` was typed as a string; the app reads an
1069
- 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``).
1070
1519
  It is now ``FirmwareDeviceGroup``, with a bare string still accepted.
1071
1520
  - ``convert_tou()`` hard-coded ``userType: "O"``; it now sends the
1072
1521
  signed-in user's type from the sign-in response, defaulting to ``"O"``.
@@ -1101,8 +1550,6 @@ Documentation
1101
1550
  physics with a learned correction, and are removed. The "cold starts
1102
1551
  about 55 min short" figure is replaced by the recorded errors by time
1103
1552
  left. The lower trigger's in-window wait is given with its full count.
1104
- - ``unknown-values``, ``tank-energy`` and the MQTT protocol reference now
1105
- cite the current NaviLink app release they were checked against.
1106
1553
  - ``docs/openapi.yaml`` and ``rest_api.rst`` describe every endpoint above
1107
1554
  plus ``POST /auth/session/refresh`` and ``POST /user/secured-sign-in``;
1108
1555
  ``mqtt_protocol.rst`` documents ``res/ctrl-fail`` and the
@@ -1140,13 +1587,12 @@ Documentation
1140
1587
  - The ambient effect on the compressor is about 1.18x, not 1.7x; the
1141
1588
  standby soak's figures, the 60 h gap prior and the circulation runs
1142
1589
  behind the lower-zone drop are stated as measured.
1143
- - **Docs that contradicted the code or the vendor's sources** (#155). Each
1590
+ - **Docs that contradicted the code or the protocol** (#155). Each
1144
1591
  now has one value with its source:
1145
1592
 
1146
1593
  - The mode-ID table in ``device_features.rst`` numbered Energy Saver 2,
1147
1594
  High Demand 3 and Electric 4. The IDs are Electric 2, Energy Saver 3,
1148
- High Demand 4, as in ``DhwOperationSetting`` and the NaviLink app's
1149
- ``MgppOperationMode``.
1595
+ High Demand 4, as in ``DhwOperationSetting``.
1150
1596
  - ``track-energy.rst`` used ``comp_running_minute_total`` and
1151
1597
  ``heater1/2_running_minute_total``, which do not exist. Lifetime run
1152
1598
  times come from the diagnostics query, in hours
@@ -1177,12 +1623,11 @@ Documentation
1177
1623
  - **Flow-rate units corrected** (#154). The protocol reference said
1178
1624
  ``currentDhwFlowRate`` and ``recircDhwFlowRate`` were tenths of a GPM and
1179
1625
  ``cumulatedDhwFlowRate`` was gallons, while the properties treated them as
1180
- tenths of an LPM and as litres. The properties are right. Navien's
1181
- NaviLink app (2.01.02, decompiled) shows ``currentDhwFlowRate`` as
1182
- ``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.
1183
1629
  The NWP500 manual lists the panel's water-flow reading in LPM even though
1184
- the panel defaults to GPM. The app's diagnostics screen shows the heater's
1185
- hot-water volume totals in whole litres in either setting. Flow rates are
1630
+ the panel defaults to GPM. Flow rates are
1186
1631
  0.1 LPM and the cumulative counter is whole litres in both unit systems.
1187
1632
  ``device_status.rst``, ``data_conversions.rst`` and ``mqtt_protocol.rst``
1188
1633
  now say so, and ``DiagnosticsDhwUsage`` documents its units (litres, hours,
@@ -1193,7 +1638,7 @@ Documentation
1193
1638
  fixed ``"GPM"`` or ``"gal"`` ``unit_of_measurement``, which contradicted
1194
1639
  their raw units. ``get_field_unit()``, which was already driven by
1195
1640
  ``device_class``, returns the same units as before. New tests pin both
1196
- flow properties to the app's formula in each unit system.
1641
+ flow properties to these formulas in each unit system.
1197
1642
 
1198
1643
  Version 9.4.3 (2026-10-01)
1199
1644
  ==========================
@@ -1364,31 +1809,30 @@ Fixed
1364
1809
  Version 9.4.0 (2026-09-12)
1365
1810
  ==========================
1366
1811
 
1367
- This release realigns the MQTT command set with what the NaviLink app
1368
- sends. The app (version 2.03.00) was decompiled and its request
1369
- builder compared with the library; every query it sends now has a
1370
- method, six commands it never sends were removed, and the recirculation
1371
- 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
1372
1816
  *Removed* and *Changed*.
1373
1817
 
1374
1818
  Added
1375
1819
  -----
1376
1820
  - **Installer diagnostics query.** ``request_diagnostics()`` /
1377
1821
  ``subscribe_diagnostics()`` and the ``nwp-cli diagnostics`` command
1378
- return the lifetime counters the app's installer screen shows: heat
1822
+ return the device's lifetime installer counters: heat
1379
1823
  pump and element energy, days since installation, fault event counts,
1380
1824
  demand-response operation times, hot-water draw statistics and
1381
1825
  component run times and start counts, as
1382
1826
  :class:`~nwp500.models.DeviceDiagnostics`. The device answers the
1383
1827
  ``st/td/rd`` query with packed hex; the NaviLink cloud decodes it into
1384
- 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
1385
1829
  library requests that form. Verified live: the energies and compressor
1386
1830
  and element run times match the energy query's lifetime totals.
1387
1831
  - **Monthly energy query.** ``request_energy_usage_monthly()`` /
1388
1832
  ``subscribe_energy_usage_monthly()`` and ``nwp-cli energy --years``
1389
1833
  return one entry per year with twelve per-month items. Verified live.
1390
1834
  - **Hourly energy query.** ``request_energy_usage_hourly()`` /
1391
- ``subscribe_energy_usage_hourly()`` send the app's per-hour query. The
1835
+ ``subscribe_energy_usage_hourly()`` send the per-hour query. The
1392
1836
  NWP500 tested never answered it in four attempts, so it has no CLI
1393
1837
  command.
1394
1838
  - **Recirculation schedule read.** ``request_recirculation_schedule()``
@@ -1405,46 +1849,43 @@ Added
1405
1849
  ``subscribe_firmware_download_info()`` and ``nwp-cli firmware info``
1406
1850
  return :class:`~nwp500.models.FirmwareDownloadInfo`. The device replies
1407
1851
  on its own topic rather than a client-keyed one. Verified live.
1408
- - **Session end.** ``end_session()`` sends the ``st/end`` query the app
1409
- 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.
1410
1854
  Unlike other commands it is never queued while disconnected (it raises
1411
1855
  ``MqttNotConnectedError``), since a session end replayed after a
1412
1856
  reconnect would end the new session.
1413
1857
  - ``MqttConnectionConfig.send_session_end_on_disconnect`` (default
1414
1858
  ``True``) controls whether ``disconnect()`` sends it.
1415
1859
  - ``subscribe_firmware_commit_response()`` receives the reply to
1416
- ``commit_firmware_update()``, which is requested on the app-form
1860
+ ``commit_firmware_update()``, which is requested on the five-segment
1417
1861
  ``res/commit-ota`` topic that no other subscription covers.
1418
- - **Controls with app builder cases that had no method:**
1419
- ``set_vacation_duration(days)`` (``goout-day``; the app has a builder
1420
- case but no screen that calls it; verified live to change the day count
1421
- 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),
1422
1865
  ``set_air_filter_life(hours)`` (``air-filter-life``; the wire value is
1423
- hours divided by 500, matching the app's 0 or 1000-10000 picker;
1866
+ hours divided by 500, for 0 or 1000-10000 hours;
1424
1867
  verified live) and ``reset_condenser_fault()`` (``cond-fault-reset``,
1425
- installer-level in the app; acknowledged live, clearing effect not
1868
+ an installer-level control; acknowledged live, clearing effect not
1426
1869
  observable without a fault). CLI: ``vacation-duration``,
1427
1870
  ``filter-life``, ``reset-condenser-fault``.
1428
- - **Command codes** for every member of the app's ``DeviceControlMGPP``
1429
- enum, with the declared-but-never-sent ones annotated.
1871
+ - **Command codes** for every known control command, with the
1872
+ declared-only ones annotated.
1430
1873
  - ``EnergyUsageResponse.type_of_usage``, ``get_year_data()``,
1431
1874
  ``MonthlyEnergyData.day`` and ``EnergyUsageDay.ep_usage`` /
1432
- ``water_usage`` (declared by the app; not reported by the NWP500).
1875
+ ``water_usage`` (part of the protocol; not reported by the NWP500).
1433
1876
 
1434
1877
  Changed
1435
1878
  -------
1436
- - **Recirculation schedule models take the app's entry shape** (breaking).
1879
+ - **Recirculation schedule models take the reservation entry shape** (breaking).
1437
1880
  :class:`~nwp500.models.RecirculationScheduleEntry` is now
1438
1881
  ``enable``/``week``/``hour``/``min``/``mode``/``param`` like a
1439
- reservation entry (``mode`` is the entry's pump on/off toggle in the
1440
- 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
1441
1883
  ``reservation_use`` plus ``reservation`` instead of ``schedule``. The
1442
1884
  previous start/end window fields were not what the device accepts.
1443
1885
  - ``MonthlyEnergyData.month`` is optional: monthly-query entries have no
1444
1886
  month.
1445
1887
  - **``NavienMqttClient.disconnect()`` now sends ``st/end``** to every
1446
- device that was passed to any subscription, as the NaviLink app does
1447
- 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,
1448
1889
  never into the offline command queue, stop if the connection drops,
1449
1890
  are bounded by a short timeout and never fail the disconnect. What it
1450
1891
  does to other clients connected to the same device (for example a Home
@@ -1468,12 +1909,12 @@ Fixed
1468
1909
  -----
1469
1910
  - **``configure_recirculation_schedule()`` published to the wrong topic
1470
1911
  with the wrong keys.** It sent ``{"schedule": [...]}`` on the bare
1471
- ``ctrl`` topic; the app publishes RECIR_RESERVATION on
1472
- ``ctrl/recirc-rsv/rd`` with ``reservationUse`` and ``reservation`` and
1473
- 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``.
1474
1915
  - **``commit_firmware_update()`` published to the bare ``ctrl`` topic.**
1475
- The app publishes OTA_COMMIT on ``ctrl/commit-ota`` and asks for the
1476
- 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
1477
1918
  exercised against a device.
1478
1919
  - **Unsubscribing could remove the wrong handler.** When one callback was
1479
1920
  registered for more than one device-wildcard subscription (status,
@@ -1487,8 +1928,8 @@ Fixed
1487
1928
  - Recirculation read-backs in hex carry the unused ``param`` as ``0xFF``
1488
1929
  and in JSON as ``-1``; the model normalizes to ``-1`` so the two compare
1489
1930
  equal.
1490
- - **Protocol documentation** rewritten from the decompiled request
1491
- 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
1492
1933
  modes (``anti-leg-on [days]`` / ``anti-leg-off``, not
1493
1934
  ``anti-legionella-setting``), the message example that paired the
1494
1935
  weekly-reservation code with ``dhw-temperature``, the undocumented
@@ -1505,17 +1946,16 @@ Fixed
1505
1946
 
1506
1947
  Removed
1507
1948
  -------
1508
- - **Six MQTT commands the NaviLink app never sends** (breaking):
1949
+ - **Six MQTT commands that are not part of the protocol** (breaking):
1509
1950
  ``check_firmware_update()``, ``reconnect_wifi()``, ``reset_wifi()``,
1510
1951
  ``set_freeze_protection_temperature()``, ``run_smart_diagnostic()`` and
1511
1952
  ``update_weekly_reservation()`` with
1512
1953
  ``subscribe_weekly_reservation_response()`` and the
1513
1954
  ``WeeklyReservationEntry`` / ``WeeklyReservationSchedule`` models. The
1514
- app's enum declares the first five codes but its request builder has
1515
- no case for them, so the library's ``ota-check``, ``wifi-reconnect``,
1516
- ``wifi-reset``, ``frez-temp`` and ``smart-diagnostic`` mode strings were
1517
- invented. The app's weekly schedule is the ordinary reservation
1518
- 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``
1519
1959
  (``update_reservations()``), never with 33554438 or a ``rsv-weekly/rd``
1520
1960
  topic. The ``CommandCode`` members stay, annotated as declared only.
1521
1961
 
@@ -1581,12 +2021,10 @@ Removed
1581
2021
  waited for a reply on ``res/tou/rd``. No reply ever comes: a live device
1582
2022
  with TOU provisioned (``program_reservation_use`` true, a valid controller
1583
2023
  serial, and a plan the REST API returns in full) stayed silent for 45
1584
- seconds. ``ctrl/tou/rd`` with that command code is the *write* - it is what
1585
- :meth:`~nwp500.NavienMqttClient.configure_tou_schedule` publishes and what
1586
- the vendor app publishes from its TOU editor
1587
- (``TouScheduleViewmodel.setPublishMgppControlTou``), and the device answers
1588
- on ``res/tou/rd`` only to confirm such a write. The vendor app reads TOU
1589
- 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.
1590
2028
 
1591
2029
  Beyond returning nothing, the call published a write-shaped command with no
1592
2030
  schedule attached. This device ignored it (``touStatus`` was unchanged
@@ -1627,10 +2065,9 @@ Fixed
1627
2065
  unknown keys, so the field silently reported its ``False`` default on
1628
2066
  every device regardless of whether a thermostatic mixing valve was
1629
2067
  supported. The CLI's "Mixing Valve" capability row was wrong for the
1630
- same reason. The misspelling is Navien's own: it appears in the vendor
1631
- app's ``KDResponseMgppDid$Feature`` class and in
1632
- ``docs/reference/protocol/device_features.rst``, which documented the
1633
- 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.
1634
2071
 
1635
2072
  - **``firmware_payload_capture.py`` captured almost nothing.** Its wildcards
1636
2073
  covered ``cmd/{type}/{client_id}/res/#`` and the event topic, but the device
@@ -1706,10 +2143,6 @@ Changed
1706
2143
  - **Status flags now preserve the device's unknown state.** The protocol
1707
2144
  encodes these flags as ``0 = unknown, 1 = OFF, 2 = ON``, and the library
1708
2145
  was collapsing 0 to ``False``, inventing an OFF the device never claimed.
1709
- Confirmed against Navien's own NaviLink app (2.03.00, versionCode 141),
1710
- which decodes exactly this set of fields through an enum declared
1711
- ``UNKNOWN(0), OFF(1), ON(2)``; two sibling enums render their zero as
1712
- ``"-"`` and ``"Not Applied"`` rather than as an off state.
1713
2146
 
1714
2147
  Affected: ``operation_busy``, ``comp_use``, ``anti_legionella_use``,
1715
2148
  ``anti_legionella_operation_busy``, ``heat_upper_use``, ``heat_lower_use``,
@@ -1721,7 +2154,7 @@ Changed
1721
2154
  this is the wanted shape: ``None`` renders as "Unknown" instead of writing
1722
2155
  a fabricated OFF into the recorder database.
1723
2156
 
1724
- - ``OnOffFlag`` gains the vendor's ``UNKNOWN = 0`` member. It previously
2157
+ - ``OnOffFlag`` gains an ``UNKNOWN = 0`` member. It previously
1725
2158
  started at ``OFF = 1``, leaving the device's reserved value unrepresented.
1726
2159
 
1727
2160
  - The CLI renders these flags as ``Unknown`` rather than ``No``. Both
@@ -1746,7 +2179,7 @@ Added
1746
2179
  report. ``converters.device_bool_to_python`` is unchanged and remains
1747
2180
  correct for capability flags.
1748
2181
  - New ``docs/explanation/unknown-values.rst`` recording which field families
1749
- 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.
1750
2183
 
1751
2184
  Removed
1752
2185
  -------
@@ -1775,8 +2208,6 @@ Fixed
1775
2208
  - ``dhwTemperature`` is documented as an outlet temperature but is
1776
2209
  measured inside the tank: it tracks ``tankUpperTemperature`` to within
1777
2210
  one 0.5 degC step, and the device has no sensor downstream of itself.
1778
- Navien's own app agrees, labelling it "DHW Temp." beside the tank
1779
- thermistors and showing ``dischargeTemperature`` separately.
1780
2211
  - ``docs/explanation/tank-energy.rst`` tabulated ``totalEnergyCapacity``
1781
2212
  and ``availableEnergyCapacity`` at ten times their raw wire values,
1782
2213
  under column headings naming the raw protocol fields. The series had
@@ -1794,19 +2225,16 @@ Fixed
1794
2225
  in the changelog, the model field description and the protocol
1795
2226
  reference.
1796
2227
  - New ``docs/explanation/tank-energy.rst`` records what the two fields
1797
- measure and the calibration evidence behind the scale change,
1798
- including that Navien's own NaviLink app (2.03.00, versionCode 141)
1799
- reads neither field, so no vendor-side corroboration exists.
1800
-
1801
- - Documented that **temperature fields carry no sentinel at all**. The app
1802
- has no out-of-band constant (no ``0xFFFF``/``-999``/``-1``), no zero-guard
1803
- in any display path, and formats whatever arrives, so a temperature of
1804
- 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
1805
2233
  was applied to temperature converters and reported working sensors as
1806
2234
  missing during cold-weather operation.
1807
- - Documented that capability flags are a distinct case: the app hides a
1808
- feature's entire UI when its DID ``Use`` flag reads 0, so 0 there means
1809
- "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.
1810
2238
 
1811
2239
  Version 9.2.1 (2026-07-30)
1812
2240
  ==========================
@@ -2100,8 +2528,8 @@ Bug Fixes
2100
2528
  (default 300 s, ``None`` to disable) are now discarded at send time.
2101
2529
  - **Fix Fahrenheit conversion for sub-zero temperatures** (ASYMMETRIC
2102
2530
  formula): the rounding used Python's floored ``%``, which is always
2103
- non-negative, while the firmware/app uses a truncated remainder. Raw
2104
- ``-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
2105
2533
  uses ``math.fmod`` semantics.
2106
2534
  - **Fix freeze protection default limits**: the defaults (43/65) were
2107
2535
  Fahrenheit display values stored in raw half-Celsius fields, decoding
@@ -2530,7 +2958,7 @@ Fixed
2530
2958
  - **Vacation mode sent wrong MQTT command**: ``set_vacation_days()`` used
2531
2959
  ``CommandCode.GOOUT_DAY`` (33554466), which the device silently accepted
2532
2960
  but did not activate vacation mode; the operating mode remained unchanged.
2533
- HAR capture of the official Navien app confirms the correct command is
2961
+ A HAR capture of NaviLink traffic confirms the correct command is
2534
2962
  ``DHW_MODE`` (33554437) with ``param=[5, days]``
2535
2963
  (``DhwOperationSetting.VACATION``). The valid range has also been corrected
2536
2964
  from 1-365 to 1-30 to match the device's actual constraint.
@@ -2603,7 +3031,7 @@ Added
2603
3031
  -----
2604
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.
2605
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.
2606
- - **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.
2607
3035
  - **ConvertedTOUPlan Model**: New Pydantic model for parsed ``convert_tou()`` results (utility, name, schedule).
2608
3036
  - **CLI ``tou rates``**: Browse utilities and rate plans for a zip code (``nwp500 tou rates 94903``).
2609
3037
  - **CLI ``tou plan``**: View converted rate plan details with decoded pricing (``nwp500 tou plan 94903 "EV Rate A"``).
@@ -2617,8 +3045,8 @@ Changed
2617
3045
 
2618
3046
  Fixed
2619
3047
  -----
2620
- - **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.
2621
- - **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``.
2622
3050
  - **Reservation Set Command Timeout**: Fixed ``reservations set`` subscription pattern that had extra wildcards preventing response matching. Command now receives confirmations correctly.
2623
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.
2624
3052
 
@@ -3561,7 +3989,7 @@ Version 6.0.4 (2025-11-21)
3561
3989
  Fixed
3562
3990
  -----
3563
3991
 
3564
- - **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:
3565
3993
 
3566
3994
  - Replaced ``Add20`` validator with ``HalfCelsiusToF`` for fields transmitted as half-degrees Celsius
3567
3995
  - Replaced ``DeciCelsiusToF`` with ``PentaCelsiusToF`` for fields scaled by factor of 5
@@ -4430,7 +4858,7 @@ Fixed
4430
4858
  - Power-off now uses command code ``33554433``
4431
4859
  - Power-on now uses command code ``33554434``
4432
4860
  - DHW mode control now uses command code ``33554437``
4433
- - Discovered through network traffic analysis of official app
4861
+ - Discovered through network traffic analysis
4434
4862
 
4435
4863
  - **Bug**: MQTT topic pattern matching with wildcards
4436
4864