nwp500-python 9.4.2__tar.gz → 9.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/CHANGELOG.rst +929 -89
  2. {nwp500_python-9.4.2/src/nwp500_python.egg-info → nwp500_python-9.5.0}/PKG-INFO +50 -16
  3. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/README.rst +37 -15
  4. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/explanation/architecture.rst +3 -3
  5. nwp500_python-9.5.0/docs/explanation/draws.rst +292 -0
  6. nwp500_python-9.5.0/docs/explanation/heating-elements.rst +558 -0
  7. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/explanation/index.rst +6 -1
  8. nwp500_python-9.5.0/docs/explanation/recovery-speed.rst +339 -0
  9. nwp500_python-9.5.0/docs/explanation/simulator.rst +401 -0
  10. nwp500_python-9.5.0/docs/explanation/standby-loss.rst +237 -0
  11. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/explanation/tank-energy.rst +113 -72
  12. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/explanation/tou-recovery-cap.rst +38 -29
  13. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/explanation/unknown-values.rst +37 -32
  14. nwp500_python-9.5.0/docs/explanation/what-starts-a-recovery.rst +340 -0
  15. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/how-to/authenticate.rst +35 -2
  16. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/how-to/auto-recovery.rst +2 -2
  17. nwp500_python-9.5.0/docs/how-to/bluetooth.rst +329 -0
  18. nwp500_python-9.5.0/docs/how-to/calibrate-simulator.rst +214 -0
  19. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/how-to/diagnose-mqtt.rst +54 -29
  20. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/how-to/home-assistant.rst +7 -7
  21. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/how-to/index.rst +3 -0
  22. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/how-to/maintenance.rst +63 -16
  23. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/how-to/manage-units.rst +19 -20
  24. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/how-to/monitor-status.rst +9 -3
  25. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/how-to/optimize-tou.rst +14 -14
  26. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/how-to/queue-commands.rst +20 -20
  27. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/how-to/schedule-operation.rst +202 -86
  28. nwp500_python-9.5.0/docs/how-to/simulate-heater.rst +1174 -0
  29. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/how-to/track-energy.rst +44 -31
  30. nwp500_python-9.5.0/docs/openapi.yaml +1658 -0
  31. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/project/history.rst +161 -108
  32. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/configuration.rst +12 -12
  33. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/enumerations.rst +41 -16
  34. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/index.rst +2 -0
  35. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/installation.rst +18 -12
  36. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/protocol/data_conversions.rst +101 -44
  37. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/protocol/device_features.rst +44 -45
  38. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/protocol/device_status.rst +71 -53
  39. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/protocol/error_codes.rst +6 -6
  40. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/protocol/mqtt_protocol.rst +167 -43
  41. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/protocol/quick_reference.rst +4 -2
  42. nwp500_python-9.5.0/docs/reference/protocol/rest_api.rst +996 -0
  43. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/python_api/api_client.rst +311 -20
  44. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/python_api/auth_client.rst +76 -10
  45. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/python_api/cli.rst +20 -20
  46. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/python_api/events.rst +87 -21
  47. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/python_api/exceptions.rst +28 -28
  48. nwp500_python-9.5.0/docs/reference/python_api/models.rst +1053 -0
  49. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/reference/python_api/mqtt_client.rst +157 -26
  50. nwp500_python-9.5.0/docs/reference/python_api/sim.rst +309 -0
  51. nwp500_python-9.5.0/docs/reference/simulator-inputs.rst +1010 -0
  52. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/requirements.txt +3 -0
  53. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/tutorials/getting-started.rst +6 -6
  54. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/README.md +17 -0
  55. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/recirculation_control.py +8 -6
  56. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/intermediate/advanced_auth_patterns.py +2 -2
  57. nwp500_python-9.5.0/examples/simulator/lower-setpoint.json +8 -0
  58. nwp500_python-9.5.0/examples/simulator/plan.json +9 -0
  59. nwp500_python-9.5.0/examples/simulator/scenario.json +91 -0
  60. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/pyproject.toml +16 -0
  61. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/setup.cfg +68 -0
  62. nwp500_python-9.5.0/src/nwp500/__init__.py +464 -0
  63. nwp500_python-9.5.0/src/nwp500/api_client.py +1433 -0
  64. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/auth.py +394 -40
  65. nwp500_python-9.5.0/src/nwp500/ble/__init__.py +110 -0
  66. nwp500_python-9.5.0/src/nwp500/ble/__main__.py +163 -0
  67. nwp500_python-9.5.0/src/nwp500/ble/client.py +546 -0
  68. nwp500_python-9.5.0/src/nwp500/ble/mqtt_adapter.py +211 -0
  69. nwp500_python-9.5.0/src/nwp500/ble/protocol.py +890 -0
  70. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/cli/__main__.py +474 -100
  71. nwp500_python-9.5.0/src/nwp500/cli/ble_backend.py +196 -0
  72. nwp500_python-9.5.0/src/nwp500/cli/calibration.py +488 -0
  73. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/cli/handlers.py +2 -2
  74. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/cli/output_formatters.py +1 -1
  75. nwp500_python-9.5.0/src/nwp500/cli/recording.py +126 -0
  76. nwp500_python-9.5.0/src/nwp500/cli/simulate.py +507 -0
  77. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/config.py +2 -0
  78. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/converters.py +2 -2
  79. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/encoding.py +6 -5
  80. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/enums.py +79 -19
  81. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/models/__init__.py +40 -1
  82. nwp500_python-9.5.0/src/nwp500/models/account.py +61 -0
  83. nwp500_python-9.5.0/src/nwp500/models/device.py +325 -0
  84. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/models/diagnostics.py +5 -2
  85. nwp500_python-9.5.0/src/nwp500/models/maintenance.py +31 -0
  86. nwp500_python-9.5.0/src/nwp500/models/mqtt_models.py +232 -0
  87. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/models/schedule.py +15 -8
  88. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/models/status.py +88 -30
  89. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/models/tou.py +3 -0
  90. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/_control_commands.py +9 -4
  91. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/_device_subscriptions.py +76 -0
  92. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/client.py +55 -27
  93. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/command_queue.py +1 -1
  94. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/connection.py +30 -5
  95. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/control.py +258 -22
  96. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/periodic.py +1 -1
  97. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/reconnection.py +1 -1
  98. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/subscriptions.py +214 -4
  99. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/types.py +34 -0
  100. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/utils.py +17 -1
  101. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt_events.py +82 -1
  102. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/reservations.py +47 -17
  103. nwp500_python-9.5.0/src/nwp500/sim/__init__.py +130 -0
  104. nwp500_python-9.5.0/src/nwp500/sim/api.py +1934 -0
  105. nwp500_python-9.5.0/src/nwp500/sim/attribution.py +195 -0
  106. nwp500_python-9.5.0/src/nwp500/sim/book.py +1361 -0
  107. nwp500_python-9.5.0/src/nwp500/sim/calibration.py +3702 -0
  108. nwp500_python-9.5.0/src/nwp500/sim/circulation.py +191 -0
  109. nwp500_python-9.5.0/src/nwp500/sim/codec.py +92 -0
  110. nwp500_python-9.5.0/src/nwp500/sim/cold_supply.py +167 -0
  111. nwp500_python-9.5.0/src/nwp500/sim/controller.py +1584 -0
  112. nwp500_python-9.5.0/src/nwp500/sim/decay.py +1121 -0
  113. nwp500_python-9.5.0/src/nwp500/sim/defaults.py +197 -0
  114. nwp500_python-9.5.0/src/nwp500/sim/delivery.py +711 -0
  115. nwp500_python-9.5.0/src/nwp500/sim/draw_forecast.py +494 -0
  116. nwp500_python-9.5.0/src/nwp500/sim/engine.py +2308 -0
  117. nwp500_python-9.5.0/src/nwp500/sim/extensions.py +212 -0
  118. nwp500_python-9.5.0/src/nwp500/sim/heat_pump_recovery.py +268 -0
  119. nwp500_python-9.5.0/src/nwp500/sim/heating_trajectory.py +342 -0
  120. nwp500_python-9.5.0/src/nwp500/sim/inputs.py +352 -0
  121. nwp500_python-9.5.0/src/nwp500/sim/location_air.py +167 -0
  122. nwp500_python-9.5.0/src/nwp500/sim/meter.py +130 -0
  123. nwp500_python-9.5.0/src/nwp500/sim/observed.py +568 -0
  124. nwp500_python-9.5.0/src/nwp500/sim/params.py +1205 -0
  125. nwp500_python-9.5.0/src/nwp500/sim/physics/__init__.py +2 -0
  126. nwp500_python-9.5.0/src/nwp500/sim/physics/compressor.py +450 -0
  127. nwp500_python-9.5.0/src/nwp500/sim/physics/draw.py +474 -0
  128. nwp500_python-9.5.0/src/nwp500/sim/physics/element.py +623 -0
  129. nwp500_python-9.5.0/src/nwp500/sim/physics/refrigerant.py +168 -0
  130. nwp500_python-9.5.0/src/nwp500/sim/physics/tank.py +72 -0
  131. nwp500_python-9.5.0/src/nwp500/sim/profile.py +238 -0
  132. nwp500_python-9.5.0/src/nwp500/sim/ready.py +728 -0
  133. nwp500_python-9.5.0/src/nwp500/sim/record.py +333 -0
  134. nwp500_python-9.5.0/src/nwp500/sim/result.py +257 -0
  135. nwp500_python-9.5.0/src/nwp500/sim/schedules.py +243 -0
  136. nwp500_python-9.5.0/src/nwp500/sim/tank_physics.py +504 -0
  137. nwp500_python-9.5.0/src/nwp500/sim/tou_triggers.py +308 -0
  138. nwp500_python-9.5.0/src/nwp500/sim/units.py +20 -0
  139. nwp500_python-9.5.0/src/nwp500/sim_adapters.py +1323 -0
  140. nwp500_python-9.5.0/src/nwp500/sim_home_assistant.py +1153 -0
  141. nwp500_python-9.5.0/src/nwp500/sim_live.py +360 -0
  142. nwp500_python-9.5.0/src/nwp500/status_history.py +68 -0
  143. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/topic_builder.py +36 -0
  144. {nwp500_python-9.4.2 → nwp500_python-9.5.0/src/nwp500_python.egg-info}/PKG-INFO +50 -16
  145. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500_python.egg-info/SOURCES.txt +113 -2
  146. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500_python.egg-info/requires.txt +14 -0
  147. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500_python.egg-info/scm_file_list.json +112 -1
  148. nwp500_python-9.5.0/src/nwp500_python.egg-info/scm_version.json +8 -0
  149. nwp500_python-9.5.0/tests/_sim_site.py +168 -0
  150. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/conftest.py +20 -0
  151. nwp500_python-9.5.0/tests/fixtures/sim/engine_golden.json +377 -0
  152. nwp500_python-9.5.0/tests/fixtures/sim/heater_params.json +65 -0
  153. nwp500_python-9.5.0/tests/fixtures/sim/heater_params_fitted.json +65 -0
  154. nwp500_python-9.5.0/tests/test_api_endpoints_201.py +821 -0
  155. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_api_helpers.py +45 -13
  156. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_app_queries.py +51 -45
  157. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_auth.py +85 -56
  158. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_auth_session_lifecycle.py +24 -16
  159. nwp500_python-9.5.0/tests/test_auth_session_refresh.py +847 -0
  160. nwp500_python-9.5.0/tests/test_ble_mqtt_adapter.py +547 -0
  161. nwp500_python-9.5.0/tests/test_ble_protocol.py +803 -0
  162. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_bug_fixes.py +26 -20
  163. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_canonical_schedule.py +10 -10
  164. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_cli_basic.py +8 -7
  165. nwp500_python-9.5.0/tests/test_cli_calibrate.py +646 -0
  166. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_cli_commands.py +167 -97
  167. nwp500_python-9.5.0/tests/test_cli_simulate.py +527 -0
  168. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_cli_tristate_presentation.py +28 -9
  169. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_command_decorators.py +1 -1
  170. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_command_queue.py +7 -7
  171. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_device_info_cache.py +23 -16
  172. nwp500_python-9.5.0/tests/test_device_rest_models.py +241 -0
  173. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_events.py +37 -37
  174. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_exceptions.py +30 -30
  175. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_model_converters.py +69 -65
  176. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_models.py +78 -42
  177. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_mqtt_clean_session_resume.py +21 -18
  178. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_mqtt_client_init.py +73 -67
  179. nwp500_python-9.5.0/tests/test_mqtt_control_fail.py +423 -0
  180. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_mqtt_events.py +17 -8
  181. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_mqtt_hypothesis.py +5 -2
  182. nwp500_python-9.5.0/tests/test_mqtt_last_will.py +524 -0
  183. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_mqtt_reconnection.py +39 -17
  184. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_mqtt_reconnection_storm.py +55 -33
  185. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_mqtt_reliability.py +288 -64
  186. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_multi_device.py +26 -15
  187. nwp500_python-9.5.0/tests/test_navilink_201_enums.py +253 -0
  188. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_openei.py +2 -1
  189. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_protocol_correctness.py +200 -93
  190. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_public_api.py +37 -7
  191. nwp500_python-9.5.0/tests/test_reservation_validation.py +341 -0
  192. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_reservations.py +87 -1
  193. nwp500_python-9.5.0/tests/test_sim_adapters.py +389 -0
  194. nwp500_python-9.5.0/tests/test_sim_api.py +800 -0
  195. nwp500_python-9.5.0/tests/test_sim_book.py +488 -0
  196. nwp500_python-9.5.0/tests/test_sim_book_history.py +250 -0
  197. nwp500_python-9.5.0/tests/test_sim_calibration.py +863 -0
  198. nwp500_python-9.5.0/tests/test_sim_circulation.py +395 -0
  199. nwp500_python-9.5.0/tests/test_sim_codec.py +165 -0
  200. nwp500_python-9.5.0/tests/test_sim_cold_supply.py +145 -0
  201. nwp500_python-9.5.0/tests/test_sim_controller.py +1227 -0
  202. nwp500_python-9.5.0/tests/test_sim_defaults.py +201 -0
  203. nwp500_python-9.5.0/tests/test_sim_draw_forecast.py +174 -0
  204. nwp500_python-9.5.0/tests/test_sim_draw_loss.py +225 -0
  205. nwp500_python-9.5.0/tests/test_sim_element.py +869 -0
  206. nwp500_python-9.5.0/tests/test_sim_element_fit.py +135 -0
  207. nwp500_python-9.5.0/tests/test_sim_forecast.py +288 -0
  208. nwp500_python-9.5.0/tests/test_sim_golden.py +359 -0
  209. nwp500_python-9.5.0/tests/test_sim_home_assistant.py +1211 -0
  210. nwp500_python-9.5.0/tests/test_sim_hot_water.py +361 -0
  211. nwp500_python-9.5.0/tests/test_sim_import_contract.py +413 -0
  212. nwp500_python-9.5.0/tests/test_sim_live.py +474 -0
  213. nwp500_python-9.5.0/tests/test_sim_location_air.py +226 -0
  214. nwp500_python-9.5.0/tests/test_sim_meter.py +163 -0
  215. nwp500_python-9.5.0/tests/test_sim_no_clock_no_state.py +363 -0
  216. nwp500_python-9.5.0/tests/test_sim_observed.py +348 -0
  217. nwp500_python-9.5.0/tests/test_sim_profile.py +86 -0
  218. nwp500_python-9.5.0/tests/test_sim_readiness.py +879 -0
  219. nwp500_python-9.5.0/tests/test_sim_readiness_fit.py +296 -0
  220. nwp500_python-9.5.0/tests/test_sim_ready.py +406 -0
  221. nwp500_python-9.5.0/tests/test_sim_ready_api.py +770 -0
  222. nwp500_python-9.5.0/tests/test_sim_replay.py +232 -0
  223. nwp500_python-9.5.0/tests/test_sim_schedules.py +336 -0
  224. nwp500_python-9.5.0/tests/test_sim_tank_physics.py +184 -0
  225. nwp500_python-9.5.0/tests/test_status_history.py +236 -0
  226. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_temperature_converters.py +52 -40
  227. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_threading_model.py +32 -28
  228. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_token_storage.py +10 -5
  229. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_tou_api.py +1 -1
  230. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_unit_switching.py +86 -15
  231. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_unit_system_process_wide.py +4 -4
  232. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_utility_modules.py +38 -22
  233. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_utils.py +35 -22
  234. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tox.ini +6 -1
  235. nwp500_python-9.4.2/docs/explanation/advanced-features.rst +0 -497
  236. nwp500_python-9.4.2/docs/openapi.yaml +0 -805
  237. nwp500_python-9.4.2/docs/reference/protocol/rest_api.rst +0 -435
  238. nwp500_python-9.4.2/docs/reference/python_api/models.rst +0 -860
  239. nwp500_python-9.4.2/src/nwp500/__init__.py +0 -253
  240. nwp500_python-9.4.2/src/nwp500/api_client.py +0 -558
  241. nwp500_python-9.4.2/src/nwp500/models/device.py +0 -104
  242. nwp500_python-9.4.2/src/nwp500/models/mqtt_models.py +0 -31
  243. nwp500_python-9.4.2/src/nwp500_python.egg-info/scm_version.json +0 -8
  244. nwp500_python-9.4.2/tests/test_device_rest_models.py +0 -128
  245. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/.coveragerc +0 -0
  246. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  247. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/.github/copilot-instructions.md +0 -0
  248. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/.github/workflows/ci.yml +0 -0
  249. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/.github/workflows/release.yml +0 -0
  250. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/.gitignore +0 -0
  251. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/.pre-commit-config.yaml +0 -0
  252. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/.readthedocs.yml +0 -0
  253. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/AUTHORS.rst +0 -0
  254. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/CONTRIBUTING.rst +0 -0
  255. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/LICENSE.txt +0 -0
  256. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/Makefile +0 -0
  257. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/RELEASE.md +0 -0
  258. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/Makefile +0 -0
  259. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/_static/.gitignore +0 -0
  260. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/conf.py +0 -0
  261. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/index.rst +0 -0
  262. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/project/authors.rst +0 -0
  263. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/project/changelog.rst +0 -0
  264. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/project/contributing.rst +0 -0
  265. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/docs/project/license.rst +0 -0
  266. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/.ruff.toml +0 -0
  267. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/air_filter_reset.py +0 -0
  268. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/anti_legionella.py +0 -0
  269. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/auto_recovery.py +0 -0
  270. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/combined_callbacks.py +0 -0
  271. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/demand_response.py +0 -0
  272. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/device_capabilities.py +0 -0
  273. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/device_status_debug.py +0 -0
  274. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/energy_analytics.py +0 -0
  275. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/error_code_demo.py +0 -0
  276. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/firmware_payload_capture.py +0 -0
  277. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/mqtt_diagnostics.py +0 -0
  278. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/power_control.py +0 -0
  279. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/reconnection_demo.py +0 -0
  280. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/reservation_schedule.py +0 -0
  281. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/simple_auto_recovery.py +0 -0
  282. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/token_restoration.py +0 -0
  283. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/tou_openei.py +0 -0
  284. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/tou_schedule.py +0 -0
  285. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/advanced/water_reservation.py +0 -0
  286. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/beginner/01_authentication.py +0 -0
  287. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/beginner/02_list_devices.py +0 -0
  288. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/beginner/03_get_status.py +0 -0
  289. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/beginner/04_set_temperature.py +0 -0
  290. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/intermediate/command_queue.py +0 -0
  291. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/intermediate/device_status_callback.py +0 -0
  292. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/intermediate/error_handling.py +0 -0
  293. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/intermediate/event_driven_control.py +0 -0
  294. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/intermediate/improved_auth.py +0 -0
  295. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
  296. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  297. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/intermediate/periodic_requests.py +0 -0
  298. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/intermediate/set_mode.py +0 -0
  299. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/intermediate/vacation_mode.py +0 -0
  300. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/mask.py +0 -0
  301. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/testing/periodic_device_info.py +0 -0
  302. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/testing/simple_periodic_info.py +0 -0
  303. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/testing/test_api_client.py +0 -0
  304. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/testing/test_mqtt_connection.py +0 -0
  305. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/testing/test_mqtt_messaging.py +0 -0
  306. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/examples/testing/test_periodic_minimal.py +0 -0
  307. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/scripts/README.md +0 -0
  308. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/scripts/bump_version.py +0 -0
  309. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/scripts/diagnose_mqtt_connection.py +0 -0
  310. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/scripts/extract_changelog.py +0 -0
  311. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/scripts/format.py +0 -0
  312. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/scripts/lint.py +0 -0
  313. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/scripts/setup-dev.py +0 -0
  314. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/scripts/validate_version.py +0 -0
  315. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/setup.py +0 -0
  316. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/_base.py +0 -0
  317. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/cli/__init__.py +0 -0
  318. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/cli/monitoring.py +0 -0
  319. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/cli/presentation.py +0 -0
  320. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/cli/rich_output.py +0 -0
  321. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/cli/token_storage.py +0 -0
  322. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/command_decorators.py +0 -0
  323. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/device_capabilities.py +0 -0
  324. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/device_info_cache.py +0 -0
  325. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/events.py +0 -0
  326. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/exceptions.py +0 -0
  327. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/factory.py +0 -0
  328. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/field_factory.py +0 -0
  329. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/models/_converters.py +0 -0
  330. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/models/energy.py +0 -0
  331. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/models/feature.py +0 -0
  332. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/models/firmware.py +0 -0
  333. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/__init__.py +0 -0
  334. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/diagnostics.py +0 -0
  335. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/mqtt/state_tracker.py +0 -0
  336. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/openei.py +0 -0
  337. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/py.typed +0 -0
  338. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/temperature.py +0 -0
  339. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/tou_schedule.py +0 -0
  340. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/unit_system.py +0 -0
  341. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500/utils.py +0 -0
  342. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  343. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  344. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  345. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
  346. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_device_capabilities.py +0 -0
  347. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/tests/test_tou_schedule.py +0 -0
  348. {nwp500_python-9.4.2 → nwp500_python-9.5.0}/uv.lock +0 -0
@@ -2,8 +2,848 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
- Unreleased
6
- ==========
5
+ Version 9.5.0 (2026-10-20)
6
+ ==========================
7
+
8
+ Added
9
+ -----
10
+ - **Bluetooth LE access to the controller** (``nwp500.ble``, optional
11
+ ``ble`` extra). The NWP500 exposes a GATT service that carries the
12
+ controller's native class-code protocol once the heater is put in pairing
13
+ mode (with the heater off, hold Back and Power for five seconds; it
14
+ advertises as ``HPWH1…``). The scan also lists a heater in Wi-Fi setup
15
+ mode (``HPWH4610``), which answers nothing, and the CLIs pick an
16
+ ``HPWH1`` heater over it.
17
+ The package frames and parses those packets (header, class code, address,
18
+ CRC-16/MODBUS), scans for the heater, and reads data classes with a
19
+ matched request/reply client; ``python -m nwp500.ble scan`` and
20
+ ``python -m nwp500.ble read`` dump the blocks. Checked on a unit: it
21
+ answers a DIP-switch block, error history, four diagnostics counters, the stored
22
+ time-of-use plan, device info, the controller serial numbers and a
23
+ software-data block; the boiler parameter classes 51-54 do not answer, so
24
+ installer parameters are not reachable yet. ``write_block()`` exists for
25
+ the eventual parameter write and is marked experimental. ``read``
26
+ dumps the classes the heater answers by default and exits 1 when none
27
+ answered; the readers that decode a block raise ``CrcMismatchError``
28
+ on a reply that failed its checksum.
29
+ ``read_tou_records()`` decodes the stored time-of-use plan into
30
+ ``TOUPeriod``, the cloud's model, reading the controller's 40-slot table
31
+ whole (a ``TOU_PLAN`` read's payload byte is the last slot, not a
32
+ length; ``build_tou_read()``). ``write_tou_records()`` stores a plan of
33
+ up to twelve intervals (``build_tou_write()``: the NaviLink app's clock
34
+ header, then the records in the read layout) and reads it back. It
35
+ refuses to write over a longer stored plan, whose extra intervals no
36
+ Bluetooth write can clear, and raises ``TouWriteError`` when the
37
+ heater refuses or stores something else. ``TOU_PLAN`` is the only
38
+ class written so far (``NWP500_WRITABLE_CLASSES``). A read that gets no reply
39
+ is sent once more, since the first request on a new connection
40
+ sometimes goes unanswered. ``DeviceInfoBlock`` (``DID``), ``ControllerSerials``
41
+ (``MSCD``) and ``SoftwareInfoBlock`` (``MSWD``) use the cloud models'
42
+ field names for the fields matched against them.
43
+ ``read_sub_status()`` decodes the ``SSD`` block into two DIP-switch
44
+ words (``SubStatus``), using the layout Navien's boiler service toolkit
45
+ reads; it is unconfirmed on the NWP500.
46
+ ``read_status()`` returns live status: the class-50 session command 200
47
+ makes the heater push the ``RTMD`` block about every 300 ms until the
48
+ connection closes, and ``RealTimeStatus`` decodes the fields checked
49
+ against the MQTT status report, in standby and while heating (operation
50
+ mode, state number, DHW temperature and setpoint, the six thermistors,
51
+ superheat and EEV step), under the ``DeviceStatus`` names and
52
+ conversions. A read ends the stream, and the next ``read_status()``
53
+ starts it again. The
54
+ heater's display unit is in no Bluetooth block, so Bluetooth
55
+ temperatures follow the unit-system preference.
56
+ - ``nwp-cli --backend ble`` (with ``--ble-address`` or a scan) runs
57
+ ``serial``, ``info`` and ``status`` over Bluetooth without signing in,
58
+ and refuses other commands. Without ``--backend``, commands use the cloud as before.
59
+ The handlers run unchanged against ``nwp500.ble.BleMqttAdapter``, which
60
+ offers the MQTT client methods the heater serves over Bluetooth and
61
+ raises ``BleUnsupportedError`` for the rest. Bluetooth temperatures use
62
+ the cloud models' conversion and unit-system preference;
63
+ ``DeviceInfoBlock.min_operation_temperature()`` follows the preference
64
+ when ``celsius`` is not given (Fahrenheit without one, as before).
65
+ - **New REST endpoints.** ``NavienAPIClient`` gains
66
+ ``get_push_settings()`` / ``set_push_settings()``
67
+ (``/user/get-push-settings``, ``/user/set-push-settings``),
68
+ ``record_filter_change()`` / ``record_cip_change()`` and
69
+ ``get_filter_history()`` / ``get_cip_history()`` (the cloud's maintenance
70
+ log; dates are sent ``YYYY-M-D`` without zero padding, as the app does),
71
+ ``get_display_preferences()`` / ``set_display_preferences()``
72
+ (``/device/preferences/format``; the cloud rejects it for the NWP500,
73
+ the app only uses it for the IoT-module hydro unit), ``get_user_profile()`` (``/user/info``),
74
+ ``get_firmware_detail()`` (``/firmware/info``; ``run_time`` is the
75
+ estimated update time in minutes), ``delete_tou()``
76
+ (``DELETE /device/tou``), ``get_manufacture_id()``,
77
+ ``get_error_history()`` (``/device/error/list``; installer accounts
78
+ only, an owner account gets ``COMMON_NOT_AUTHORIZED``), ``update_device_info()``
79
+ (``/device/change-info``), ``list_simple_devices()`` and
80
+ ``get_app_version()`` (unauthenticated; a 403 maintenance notice is
81
+ returned as ``server_check_text``; ``nwp500.get_app_version()`` asks
82
+ before sign-in, without a client). ``set_push_settings()`` keeps the
83
+ current value of a flag left ``None``, since the app always sends
84
+ every flag. ``get_error_history()`` entries carry the list's
85
+ ``timeZone`` (``time_zone``, ``occurred_at_local``). New models: ``PushSettings``,
86
+ ``DeviceInstaller``, ``FirmwareDeviceGroup``, ``FirmwareDetail``,
87
+ ``DisplayPreferences``, ``SimpleDevice``, ``DeviceErrorHistoryEntry``,
88
+ ``MaintenanceHistoryEntry``, ``UserProfile``, ``AppVersionInfo``. Push
89
+ flags are 1 = on, 2 = off, the reverse of the device's on/off flags, and
90
+ decode to ``True``/``False``/``None``. ``Device.installer`` and
91
+ ``Device.alarm_info`` are parsed from ``/device/info`` as the app does;
92
+ ``TOUSchedule.nickname`` is modelled.
93
+ - **AWS credentials renew without the password.**
94
+ ``NavienAuthClient.refresh_session()`` posts to ``/auth/session/refresh``
95
+ with the access token and updates only the AWS credential fields the
96
+ server returns, as the app does when the MQTT connection fails with an
97
+ auth error. ``secured_sign_in()`` posts to ``/user/secured-sign-in`` with
98
+ the access token and overlays whatever token fields come back (observed
99
+ live: the AWS credentials only; the JWTs are kept), which the app uses
100
+ for auto-login. When the AWS credentials expire, ``ensure_valid_token()``
101
+ and the context manager's stored-token check now try session refresh,
102
+ then secured sign-in, and only then the password sign-in; both token
103
+ steps are skipped when the access token has expired and cannot be
104
+ refreshed. ``re_authenticate()`` tries secured sign-in while the access
105
+ token is live, then the password sign-in, so a client restored from
106
+ tokens alone can use it too.
107
+ The MQTT client's deep reconnect calls ``refresh_session()`` after the
108
+ forced token refresh. ``AuthTokens`` gained an optional
109
+ ``aws_credentials_issued_at`` so a renewed AWS expiry survives
110
+ ``to_dict()``/restore. Stored tokens without the field load as before.
111
+ - **Control failures are parsed and surfaced.** When the device refuses a
112
+ control command it publishes on ``cmd/52/navilink-{mac}/res/ctrl-fail``
113
+ instead of acknowledging it; the library silently dropped that message.
114
+ It is now parsed into ``ControlFailResponse``
115
+ (``ControlFailCode.DHW_CONTROL_INTERVAL_EXCEEDED`` = 2 is the only code
116
+ the app interprets: the previous DHW control was too recent) and emitted
117
+ as the ``control_failed`` client event (``ControlFailedEvent`` with
118
+ ``own_command`` and ``reason``) for every device subscription, with a
119
+ WARNING log. ``subscribe_control_fail_response()`` /
120
+ ``unsubscribe_control_fail_response()`` add a plain callback.
121
+ - **MQTT Last-Will and app-connection presence.** Set
122
+ ``MqttConnectionConfig.last_will_device`` to register the app's Last-Will
123
+ on ``evt/52/navilink-{mac}/app-connection`` (``event.connection.status``
124
+ 0, QoS 1) so other clients learn when this one drops; every rebuilt
125
+ connection carries it. ``subscribe_app_connection()`` /
126
+ ``unsubscribe_app_connection()`` parse the topic into
127
+ ``AppConnectionEvent`` and emit ``app_connection_received``
128
+ (``AppConnectionReceivedEvent``). New ``MqttTopicBuilder`` helpers
129
+ ``control_fail_response_topic()`` and ``app_connection_topic()``.
130
+ - **``DeviceStatus.air_filter_remaining_hours``**: the app's remaining
131
+ filter life (``None`` when the reminder period is 0, else clamped at 0).
132
+ - **``DeviceStatus.minor_code``** (``minorCode``): the secondary alarm code
133
+ the app shows as ``R%03d``; 101 is the one it lets the user reset.
134
+ Defaults to 0 for payloads that omit it.
135
+ - **``DhwOperationSetting.STANDBY`` (0)**, the app's
136
+ ``MgppOperationMode.STANDBY``. ``DeviceStatus.dhw_operation_setting``
137
+ previously rejected a 0. It is status-only: ``set_dhw_mode()`` refuses
138
+ it, as it now refuses any id outside 1-6. ``sim_adapters.settings()``
139
+ refuses a Standby status, which the simulator does not model.
140
+ - **``DREvent.GRID_EMERGENCY`` (6).** The app declares CPE and Grid
141
+ Emergency both as id 5 but decodes ``drEventStatus`` by array position,
142
+ so raw 6 is Grid Emergency; ``CPE`` (5) stays "Customer Peak Event".
143
+ - **``UnitType``** NCB700 (22), NFB700_H (24), CAS_NFB700_H (25),
144
+ NPF_SYSTEM (514); **``DeviceType``** IOT_MODULE (8), NFB700_MAIN (50),
145
+ NFB700_SUB (51) (the app's names; ``NPF700_MAIN``/``NPF700_SUB`` remain
146
+ as aliases) and ``HPWH`` as an alias of 52, which is the NWP500
147
+ (``DefineData.HPWH_MODEL``), not an NPF700 WiFi module. A dumped device
148
+ of type 50 or 51 now names it ``NFB700_MAIN`` or ``NFB700_SUB``.
149
+ - **A recovery model's inputs, through ``nwp500.sim.simulate``** (#165).
150
+ Beside ``recovery=``, ``simulate`` takes what a learned model was
151
+ trained with and brings: ``start_hold`` (a start's hold by the
152
+ minutes since the compressor last stopped), ``running_hold`` (what
153
+ is left of a running compressor's), ``lead_draws`` (draws recorded
154
+ before the start), ``draw_hold_factor`` and ``physics_usable``. Each
155
+ goes to the engine as given. Left at their defaults, no answer
156
+ changes.
157
+ Lead draws move the tank as the scenario's own draws do. Like them,
158
+ they are refused on parameters with no fitted draw response
159
+ (``draw_model_unsupported``), and those the model reads carry
160
+ ``draw_uncalibrated`` and ``inlet_temp_modelled`` where they apply.
161
+ - **The air, its humidity and the inlet, live from Home Assistant**
162
+ (#166).
163
+
164
+ - ``nwp500.sim_home_assistant`` reads Home Assistant's REST API,
165
+ read-only. ``HomeAssistantEnvironment`` is a ``sim_live``
166
+ environment source:
167
+
168
+ - the air is the hourly forecast projected through a
169
+ ``location_air`` model, anchored to a reading beside the heater at
170
+ most an hour old (without a model, that reading held);
171
+ - the humidity is a reading at most an hour old;
172
+ - the inlet is the plateau of the last 72 hours' long draws, from
173
+ the inlet probe, the meter and the circulation's runs.
174
+
175
+ Units come from each entity's ``unit_of_measurement``. A part that
176
+ can't be read keeps its default, and the source says why.
177
+ ``record_inputs`` reads the same entities over a recorded history.
178
+ - ``nwp500.sim.meter.flow_gpm`` is a meter's flow from its volume
179
+ reports and its forward rate: the net volume over a trailing 90 s
180
+ where it holds at the draw line for 120 s, the rate elsewhere. It
181
+ finds the long draws the inlet plateau needs where the rate alone,
182
+ silent through a steady draw, misses some.
183
+ - ``nwp-cli simulate run``, ``snapshot`` and ``calibrate`` take
184
+ ``--sources FILE`` (or ``NWP500_SOURCES``), a TOML file naming the
185
+ entities, with the token in an environment variable.
186
+ ``calibrate`` reads the air, the humidity, the draws and the
187
+ circulation over the record, which makes the quiet test exact.
188
+ - ``sim_live.compose`` takes the heater's own air reading when the
189
+ environment source has none.
190
+ - **An optional heater simulator, ``nwp500.sim``** (#150, first part). The
191
+ core of a physics simulation of the NWP500: an R-134a compressor model
192
+ coupled to a two-zone tank, standby loss, the heating elements, draws,
193
+ and the heater's own controller (start triggers, the TOU hold and
194
+ recovery cap, the restart lockout, reservation entries, setpoint and
195
+ mode writes), walked as events by ``nwp500.sim.engine.simulate()``. It
196
+ is a port of an engine checked against recorded data from one
197
+ NWP500-65, and returns the same answers to the last bit. Install it
198
+ with the ``sim`` extra (numpy, pandas, scipy). It performs no I/O,
199
+ reads no clock and holds no module state, and ``import nwp500`` does
200
+ not load it. An import-linter contract in ``tox -e lint`` keeps it
201
+ from importing the MQTT or REST clients, aiohttp, the AWS IoT SDK or
202
+ ``logging``.
203
+ - **The simulator's API** (#150, second part).
204
+
205
+ - ``nwp500.sim.simulate(scenario, params)`` walks a frozen
206
+ ``Scenario``: the heater's state, settings, history, schedules,
207
+ environment, draws and the commands under evaluation.
208
+ - ``recovery()`` answers "when will the tank be hot?", and
209
+ ``standby()`` projects the tank coasting.
210
+ - The TOU periods become the windows the heater holds, and the
211
+ reservations become firings, with the local times in the
212
+ scenario's zone (``sim.schedules``).
213
+ - Results name every default they used.
214
+ - ``default_params(volume_code)`` carries one NWP500-65's parameters,
215
+ with the tank volume scaled for the 50 and 80 gal models.
216
+ - ``sim.codec`` reads and writes scenarios as JSON documents.
217
+ - ``nwp500.sim_adapters`` builds the inputs from ``DeviceStatus``,
218
+ ``DeviceFeature``, ``ReservationSchedule`` and ``TOUInfo``, reading
219
+ the raw fields so the unit preference cannot change them. It is
220
+ where units are crossed: ``environment``, ``loads``, ``write`` and
221
+ ``reservation_entry`` take a caller's values in the preferred units
222
+ (degC and litres under ``metric``), and ``path``, ``temperature``
223
+ and ``volume`` give a result's back in them. The simulator itself
224
+ works in degF and US gallons.
225
+ - Docs: a how-to (``docs/how-to/simulate-heater.rst``), from fetching
226
+ the heater's data to evaluating a change before sending it; a
227
+ reference of every input, its unit, default and source
228
+ (``docs/reference/simulator-inputs.rst``); an explanation of the
229
+ model and its accuracy (``docs/explanation/simulator.rst``); and the
230
+ API reference.
231
+
232
+ - Hot water at the tap: ``nwp500.sim.readiness(state, settings,
233
+ params)`` gives deliverable gallons now; ``Loads.windows`` judge a
234
+ shower or a bath (deliverable gallons and margin at the onset, and
235
+ whether the delivered water ran cold); ``SimulationResult.delivery``
236
+ is the delivered temperature along the walk (``sim.delivery``).
237
+ - The heat book (``sim.book``): the tank's heat by bookkeeping, walked
238
+ through the simulation as the engine's ``HeatLedger`` so a draw that
239
+ spends it takes the zones down. Started from the caller's knowledge
240
+ (``full_tank_book``) or their own record, never guessed from the
241
+ probes.
242
+ - ``HeaterParams`` gains ``readiness`` (the line, the outlet lag, the
243
+ fall rate, the dip tolerance, evidence) and ``book`` parts, with one
244
+ installation's measured defaults and their provenance.
245
+ - Time to ready: ``nwp500.sim.time_to_ready(scenario, params)`` says
246
+ whether the tank can run a bath, when it can in the heat pump and
247
+ in each element mode with the energy to get there, and how many
248
+ showers it is ready for, along the recovery the heater runs next
249
+ (``next_recovery``) (``sim.ready``).
250
+ - ``standby(..., settings=)`` says when the heater would start itself
251
+ on the coast and why; ``element_mode`` projects a recovery in an
252
+ element mode.
253
+ - A walk with draws outside the range the draw terms were fitted over
254
+ says ``draw_out_of_domain`` (``DrawParams.domain_upper_f``,
255
+ ``domain_volume_gal``); a write or a written reservation outside the
256
+ setpoint range the simulation runs is refused.
257
+
258
+ **Behaviour notes.** Answers that a client carrying an earlier copy of
259
+ this engine will see change at the switch:
260
+
261
+ - When no ``RecoveryModel`` answers (none given, or it declines) and
262
+ the physics integration stops at its bound short of the target,
263
+ there is no recovery and ``simulate`` raises ``SimulationError``.
264
+ The bound is no longer served as a duration.
265
+ - A recovery's reasons keep the served recovery's own (a truncated
266
+ integration, a model's diagnostics) beside ``recovery_low_confidence``.
267
+ - ``mode_recovery``'s ``upper_end_f`` is the zone's own temperature
268
+ when the element does not start or has nothing to do.
269
+ - A draw during a recovery honours ``draws_on`` and is priced on the
270
+ configured tank, not the 63.2 gal default.
271
+ - No ``hp_lower_on`` disables only the lower trigger; the upper
272
+ zone's at a TOU window's opening still starts the heater.
273
+ - A draw at a draw window's onset counts its cold side when the window
274
+ is judged.
275
+ - TOU prices are compared decoded at their own ``decimal_point``; each
276
+ period is laid out on the dates its day and season masks select
277
+ before windows that touch are joined across midnight.
278
+ - Scenario instants are held in UTC, so a horizon is elapsed time
279
+ across a daylight-saving change; a scenario document keeps which
280
+ fields were left to their defaults.
281
+ - ``sim.book.SpanLoss`` lets a client charge a loss the book does not
282
+ model; without one, the book charges none.
283
+ - Circulation runs within 10 minutes of each other are one run over
284
+ the whole schedule, so a coast boundary between them no longer
285
+ charges each its own displaced volume.
286
+ - ``default_params()`` has no mixing valve; ``site_example_params()``
287
+ is the installation the values were fitted at, its valve included
288
+ (``with_valve``, ``SITE_VALVE``). The core's draw functions default
289
+ to no valve as well. Each parameter says whether it describes the
290
+ heater or the installation (``scope_of``), and the frame it was
291
+ fitted in (``note_of``).
292
+ - Circulation (``sim.circulation``): an installation's runs
293
+ (``Loads.circulation``, ``sim_adapters.circulation`` for a weekly
294
+ schedule) charged as flowing draws and to the heat book while they
295
+ run, on ``HeaterParams.circulation``; with none, nothing is charged.
296
+ - The air where the heater stands, from the heater alone and a public
297
+ forecast: ``sim_adapters.location_air_reading`` (the heater's own
298
+ reading, only while the compressor runs) and
299
+ ``sim_adapters.location_air`` (an outdoor forecast projected by
300
+ ``sim.location_air``, anchored to that reading, with ``fit`` for
301
+ one's own model). ``time_to_ready`` and ``next_recovery`` take a
302
+ ``RecoveryModel``.
303
+ - Calibration from the heater's own record (``sim.calibration``,
304
+ ``sim_adapters.history`` to build one from ``DeviceStatus``
305
+ readings): the standby loss law and the zones' mixing constant from
306
+ quiet windows, then the compressor constant and evaporator approach
307
+ from heat-pump recoveries that reached their target, then input
308
+ power and apparent COP against the heater's own power readings, each
309
+ reported
310
+ with what it rested on (held-out error, windows used, a fit pinned
311
+ on a bound). An element run counts as busy in the quiet test, not
312
+ only the compressor and hot water in use.
313
+ - ``forecast``: when a recovery must start to have the tank ready at
314
+ a time, and whether it can (``feasible``, ``shortfall_minutes``),
315
+ to the heater's own stop threshold or a partial charge, with
316
+ ``no_heating_required`` (the lower zone at its target, or a zero
317
+ energy count outside a TOU window), ``not_scheduled``, and the
318
+ trajectory's electricity, stored heat and COP.
319
+ ``active_cycle``: what is left of a running recovery, its physics
320
+ estimate on the evaporator coil's live reading.
321
+ ``Settings.hp_lower_off_f``, ``hp_upper_on_f`` and
322
+ ``HeaterState.energy_to_setpoint_wh`` carry what they need, filled
323
+ by the adapters; ``recovery`` says ``not_scheduled`` in Vacation and
324
+ Power Off.
325
+ - The heater's profile (``sim.profile``): its modes and their energy,
326
+ setpoint range and step, depletion threshold and tank, under a
327
+ revision digest; and ``recovery_as_of``, a kept recovery aged to
328
+ now.
329
+ - The element terms from a heater's raw record (``sim.calibration``):
330
+ the upper probe's lag and rate as one line from clean stints
331
+ (``element_stints``, ``fit_element_lag``), where each element
332
+ switches off and what it draws per metered stint
333
+ (``element_off_points``, ``element_power``), and the zones' rates
334
+ and element power on the one-minute grid (``element_grid``,
335
+ ``fit_element_rates``), each with day-bootstrap intervals.
336
+ - Draws from a weekly draw forecast (``sim.draw_forecast``): an issue
337
+ checked whole, its expected draws (rate x per-draw mean, the
338
+ per-draw volume kept), the heater's draw windows, and the worst case
339
+ of each window's long draws first (``forecast_loads``,
340
+ ``simulate(withheld_windows=)``). A draw's domain is now judged by
341
+ its per-draw volume where it carries one (``Draw.per_draw_gal``),
342
+ not its fractional expectation.
343
+ - The mains from an inlet probe (``sim.cold_supply``,
344
+ ``sim_adapters.cold_supply``): the median plateau of recent long
345
+ draws with circulation off and the meter reading flow, from minute 9
346
+ of each; ``on_the_mains`` converts a book's gallons at it.
347
+ - What the heater did over a range, from its record, as the model
348
+ reads it: ``sim.record.Record`` (each signal's change rows, its
349
+ writes and the heater's own drops, TOU switches and window edges,
350
+ starts with elements corroborated by power;
351
+ ``sim_adapters.record``), ``sim.attribution`` (why each recorded
352
+ start happened) and ``sim.observed`` (modes and whether each was in
353
+ effect, when each source ran, counted and attributed starts, energy
354
+ per slot, the delivered temperature, and the record's gaps;
355
+ ``sim_adapters.observe``). ``replay`` reads its writes from the same
356
+ record.
357
+ - The readiness evidence and the heat book's own terms from a metered
358
+ record (``sim.calibration``, ``sim_adapters.draw_frame``): clean
359
+ draws and their demonstrated volumes reduced to the frontier, with a
360
+ time-ordered held-out check, and the cap after a cold lower zone;
361
+ the book's compressor rate, its slope with the air, standby UA and
362
+ circulation's loss fitted on re-anchors of full tanks
363
+ (``RecordedBook``, ``fit_book_terms``), its error by anchor age, and
364
+ the empty points in the book's frame and the drawable one, and the
365
+ error bands (``book_error_btu``: the p90 pooled over anchors an hour
366
+ to 18 h old, and the 18-24 h band's, each rounded up to 100 BTU).
367
+ With a meter, the COP fit prices the heat each cycle's draws and
368
+ circulation runs took, and the draws just before it
369
+ (``metered_withdrawn``, ``metered_lead``; ``calibrate(draws=,
370
+ circulation=)``), as the model's fit did, each run at the flow it
371
+ ran at (``circulation_flow_at=``, ``flow_lookup``). What could not be fitted
372
+ keeps the base's value and is named in ``HeaterParams.unfitted``;
373
+ every answer it reaches says ``physics_uncalibrated``,
374
+ ``energy_uncalibrated``, ``standby_uncalibrated`` or
375
+ ``tau_gap_prior``.
376
+ The probes' share of circulation's heat
377
+ (``circulation_windows``, ``fit_ungated_observability``), fitted
378
+ against the displaced volume it is used with.
379
+ Circulation's terms from its own telemetry: each run's measured heat
380
+ (``measure_runs``), the displaced volume that prices them
381
+ (``fit_displaced_gal``), its recent flow and the flow a past run ran
382
+ at (``recent_flow_gpm``, ``flow_lookup``), as the model fitted them.
383
+ The book's response to a draw, fitted on the record's long draws by
384
+ the procedures its values were fitted with: the cold front's place
385
+ and width (``fit_front``), the lower zone's settling after a draw
386
+ (``fit_front_settle``) and the upper zone's mixing
387
+ (``fit_zone_mix``), each with a day bootstrap.
388
+ - The heat book from a recorded history, as the model kept it:
389
+ ``sim.book.Step``, ``book_steps`` (the anchor on a quiet full tank
390
+ after a recovery, or a recovery's end during a draw; every step
391
+ booked from it; the inlet the median of 3 h of a probe), and
392
+ ``sim_adapters.book_from_history``, which grids the heater's
393
+ statuses, a hot-water meter's flow, circulation's runs and flow and
394
+ an inlet probe onto 10 s steps and books 18 h, else 72 h aged.
395
+ ``CirculationParams.meter_share`` is the share of a run's flow the
396
+ meter also reads; ``BookParams`` gains ``history_h``, ``draw_gpm``
397
+ and ``inlet_window_s``.
398
+ - ``sim_adapters.replay``: a past scenario from the statuses the
399
+ heater reported, as the model's replay read its record: the state
400
+ then, the compressor's last stop and the last heating from their
401
+ edges, the recorded setpoint and mode changes as writes (but the
402
+ heater's own drop to Heat Pump at a window), and the TOU switch's
403
+ changes (``Schedules.tou_switches``, which the walk applies).
404
+ - The heater's own TOU marker (``Settings.hp_upper_on_f`` raised):
405
+ ``time_to_ready`` and ``element_mode`` read "in a window now" from
406
+ it, and ``next_recovery`` lets it override the schedule for a
407
+ recovery running now (``api.marked_windows``), as the model did;
408
+ an unknown marker counts as in force. ``simulate(tou_windows_in_force=)``
409
+ takes the windows to keep, and TOU on with no periods says
410
+ ``tou_schedule_unknown``. ``element_mode(setpoint_f=)`` projects to a
411
+ what-if setpoint while the heater's own decides the window test.
412
+ - ``time_to_ready`` says ``inlet_temp_modelled`` when a booked answer
413
+ converts at an inlet nobody measured (``inlet_measured=``, or a
414
+ book whose steps measured it); ``next_recovery``'s coast loses the
415
+ heat circulation takes on it as well as standby; a served recovery
416
+ under 0.6 confidence says ``recovery_low_confidence``. ``standby``
417
+ reports the gallons drawn and the heat they took, circulation's
418
+ minutes and heat, and the mean air over the coast.
419
+ - ``default_params()`` names the installation's book, readiness and
420
+ draw terms in ``HeaterParams.unfitted`` (``INSTALLATION_TERMS``):
421
+ they are one installation's fits, so answers they reach say
422
+ ``book_uncalibrated``, ``readiness_uncalibrated`` or
423
+ ``draw_uncalibrated``. A ``Record`` drops missing readings on entry
424
+ and takes its installation's cadences and the heater's resting
425
+ threshold (``drop_within``, ``element_power_slack``,
426
+ ``resting_upper_on_f``). ``observe(profile=, revision_since=)``
427
+ names modes by a heater profile. ``schedules.rule_windows`` lays out
428
+ TOU rules that have no prices, such as a learned schedule. The
429
+ engine, the tank physics and the physics models are documented as
430
+ public (:doc:`/reference/python_api/sim`).
431
+ - **``nwp-cli simulate``, and the live sources it composes** (#151).
432
+
433
+ - ``nwp500.sim_live`` reads what a scenario needs from the device:
434
+ ``LiveHeaterSource`` (the state, the settings, the air and the
435
+ tank size, from one status and one feature read) and
436
+ ``LiveScheduleSource`` (the reservations over MQTT, the TOU
437
+ periods over REST, the TOU switch from the status).
438
+ ``compose()`` awaits them, and any other source with an ``async
439
+ read()`` (``Given`` for a value in hand), into a ``Scenario``. A
440
+ read with no reply raises ``LiveReadError``, as does a TOU read
441
+ that fails while TOU is on. The simulator still imports none of
442
+ it.
443
+ - ``nwp-cli simulate run`` walks the heater as it is now, a scenario
444
+ file, or the heater now with a scenario file laid on it
445
+ (``--live``). Each part is chosen on its own: a flag
446
+ (``--start``, ``--hours``, ``--tz``, ``--ambient``,
447
+ ``--environment``, ``--inlet``, ``--draws``, ``--plan``,
448
+ ``--params``), then the scenario file, then the device. It prints
449
+ a table (the heating intervals, the energy by source, readiness,
450
+ the draw windows and which ran cold, the defaults used), the
451
+ result document (``--format json``) or the path (``--format
452
+ csv``). ``nwp-cli simulate snapshot`` writes the heater and its
453
+ schedules now as a scenario file to edit and replay. Without the
454
+ ``sim`` extra the commands stop with an install hint.
455
+ ``examples/simulator/`` has a scenario, a plan and an overlay to
456
+ start from; :doc:`/how-to/simulate-heater` shows each use.
457
+ - **Record a heater's history and calibrate the simulator to it**
458
+ (#152).
459
+
460
+ - ``nwp500.status_history`` is the history's format: one JSON line
461
+ per status, ``{"t": <ISO 8601 instant>, "status": <raw payload>}``.
462
+ ``line()`` writes one and ``read()`` reads them back as
463
+ ``(instant, DeviceStatus)`` pairs. It does no I/O and needs no
464
+ extra.
465
+ - ``nwp-cli simulate record HISTORY`` appends the heater's statuses
466
+ to a file until stopped, by default asking for one every 30 s.
467
+ - ``nwp-cli simulate calibrate HISTORY`` fits the standby loss, the
468
+ compressor and the power to that history
469
+ (``sim.calibration.calibrate``). It prints a row for each term: the
470
+ value, what it was fitted from, its error, and whether it was
471
+ fitted or kept the base's value, and why. ``-o`` writes the fitted
472
+ ``HeaterParams`` document for ``simulate run --params``. The
473
+ standby fit needs the air beside the heater, from a sensor
474
+ (``--air``, a CSV file) or a constant (``--ambient``). Given, that
475
+ air is the recoveries' air too. The heater's own reading, filled
476
+ across the gaps between compressor runs, and an outdoor projection
477
+ both biased the fitted loss, so neither stands in for the air.
478
+ Without ``--humidity`` the fits get 50 %, their documented default.
479
+ A gap between statuses of more than 4 times the record's usual
480
+ spacing (at least 5 minutes) is unobserved, so no quiet window or
481
+ recovery spans a stopped recording. If a write fails, ``record``
482
+ stops with an error.
483
+ - ``sim_adapters.history(..., max_gap=)`` marks such gaps: the
484
+ sample before one is set to water in use and excluded.
485
+ - :doc:`/how-to/calibrate-simulator` covers the recording, the air,
486
+ the report, and what a history cannot tell (the draws, a
487
+ circulation loop the heater does not run).
488
+ - Three helpers the issue proposed are left out:
489
+
490
+ - **A mains-temperature estimate from the air.** The draw terms
491
+ were fitted against a measured inlet, so an estimated inlet
492
+ would bias every draw.
493
+ - **A draw-timing profile from the "hot water in use" flag.** The
494
+ flag also fires on circulation, and the expected draws from such
495
+ a profile never predicted better than no draws.
496
+ - **A check against the daily energy totals.** Power and COP are
497
+ fitted cycle by cycle.
498
+
499
+ Changed
500
+ -------
501
+ - **Recirculation mode values follow the NaviLink app.** ``RecirculationMode``
502
+ had 2 and 3 swapped and 4 wrong: the app's
503
+ ``MgppRecirculationOperationMode`` (sent by ``recirc-mode`` and decoded
504
+ from ``recircOperationMode``) is 1 Always on, 2 Weekly, 3 HotButton only,
505
+ 4 No recirculation. The members are now ``ALWAYS_ON``, ``WEEKLY``,
506
+ ``HOT_BUTTON_ONLY`` and ``OFF``; ``ALWAYS`` remains as an alias of
507
+ ``ALWAYS_ON``. **Behaviour change:** ``BUTTON``, ``SCHEDULE`` and
508
+ ``TEMPERATURE`` are removed, since their meanings were wrong; code that
509
+ sent ``RecirculationMode.BUTTON`` (2) was selecting the weekly schedule.
510
+ ``RECIRC_MODE_TEXT``, the ``recirc`` CLI command, the recirculation
511
+ example and the protocol docs follow. ``model_dump()`` writes enum
512
+ names, so a dumped status now says ``"ALWAYS_ON"`` for 1 (was
513
+ ``"ALWAYS"``) and ``"WEEKLY"`` for 2 (was ``"BUTTON"``); code keyed on
514
+ the old strings must follow.
515
+ - **Reservation mode 5 is Power Off, not Vacation.** Weekly-schedule
516
+ entries carry the app's ``MgppReservationMode`` (0 not reserved, 1 Heat
517
+ Pump, 2 Electric, 3 Energy Saver, 4 High Demand, 5 Power Off; there is
518
+ no vacation entry), now the ``ReservationMode`` enum.
519
+ ``ReservationEntry.mode_name`` resolves through it, so a mode-5 entry
520
+ reads "Power Off" instead of "Vacation" and mode 0 reads "Not Reserved".
521
+ **Behaviour change:** reservation writes (``add_reservation()``,
522
+ ``update_reservation()``, ``build_reservation_entry()`` and the MQTT
523
+ reservation validator) now accept mode 1-5 and reject 6, which was never
524
+ a valid entry mode. An entry already on the heater with mode 6 (earlier
525
+ versions documented 6 as Power Off) does not block the reservation
526
+ helpers: they send the entries they did not change as they read them
527
+ (``update_reservations(kept=)``). Changing such an entry needs a mode
528
+ of 1-5. The simulator's reservation entries take the same numbering
529
+ (``sim.inputs.EntryMode``); only 1-4 are modelled, as before.
530
+ - **``dr_override_status`` is an on/off flag, not hours.** The app decodes
531
+ it with its 0/1/2 on-off enum and shows "DR applied" when it is 2. The
532
+ field stays an ``int`` but no longer carries an ``hours`` unit; the new
533
+ ``DeviceStatus.dr_override_active`` is True when it is 2.
534
+ - **Air-filter life comes from ``cumulated_op_time_eva_fan``.** The app
535
+ computes remaining filter life as ``air_filter_alarm_period -
536
+ cumulated_op_time_eva_fan`` and never reads ``air_filter_alarm_elapsed``.
537
+ The two descriptions were the wrong way round; ``cumulated_op_time_eva_fan``
538
+ is fan hours since the last filter reset and ``air_filter_alarm_elapsed``
539
+ is unconfirmed.
540
+ - **``signal_app_connection()`` sends the app's message shape.** It
541
+ published ``{clientID, timestamp}``, a shape nothing reads. It now
542
+ publishes the NaviLink app's ``RequestLastWill`` envelope with
543
+ ``event.connection.status`` 1.
544
+ - Sign-in and ``/auth/refresh`` requests now send the library's
545
+ ``User-Agent`` and ``Content-Type`` headers, as every other request does.
546
+ - **The heater's own air reading is not as close as documented in
547
+ every season.** ``sim_adapters.location_air_reading``,
548
+ ``sim.location_air`` and the inputs reference said it read the air
549
+ beside the heater within about 0.35 °F while the compressor runs.
550
+ Measured against a sensor beside the heater, that held from August
551
+ to October. From May to July the reading was up to about 2 °F warm,
552
+ and in a run's first 2 minutes about 2.7 °F warm. The idle reading
553
+ is about 6 °F warm, not 5.
554
+ - **The CLI logs to stderr.** It logged to stdout, so a warning could
555
+ land in the middle of ``--json``, ``--raw`` or CSV output piped to
556
+ another program. Output stays on stdout.
557
+ - **The package root loads its exports on first use** (PEP 562).
558
+ ``from nwp500 import NavienMqttClient``, ``nwp500.DeviceStatus`` and
559
+ ``nwp500.mqtt`` work as before, and type checkers read the same names,
560
+ but ``import nwp500.sim`` and ``nwp500.sim_adapters`` no longer load
561
+ the clients, aiohttp or the AWS IoT SDK (tested in a fresh
562
+ interpreter).
563
+ - **Superheat is converted as a difference** (#153).
564
+ ``DeviceStatus.target_super_heat`` and ``current_super_heat`` converted
565
+ their raw tenths of a degree Celsius as absolute temperatures, so in
566
+ Fahrenheit they added 32. Superheat is a temperature difference (suction
567
+ minus evaporator), so they now use ``DeciCelsiusDelta``, as the
568
+ ``*_diff_temp_setting`` properties already do. Raw 50 now reads 9.0 degF
569
+ instead of 41.0 degF. **Behaviour change:** every Fahrenheit value drops
570
+ by 32, so stored or charted Fahrenheit superheat from earlier versions is
571
+ 32 degF too high. Celsius values are unchanged. The protocol docs now
572
+ describe both fields as differences.
573
+ - **Vacation accepts 1-99 days** (#155). ``set_dhw_mode()`` (vacation),
574
+ ``set_vacation_days()`` and ``set_vacation_duration()`` rejected anything
575
+ over 30 days. The NaviLink app's vacation picker and the front panel
576
+ (User's Information Manual, section 3.4.5) both offer 1-99, so the
577
+ library now accepts the same. The ``vacation`` and
578
+ ``vacation-duration`` CLI commands follow.
579
+
580
+ Fixed
581
+ -----
582
+ - ``FirmwareInfo.device_group`` was typed as a string; the app reads an
583
+ object ``{groupSeq, unitNum}`` (plus a top-level ``deviceGroupUnitNo``).
584
+ It is now ``FirmwareDeviceGroup``, with a bare string still accepted.
585
+ - ``convert_tou()`` hard-coded ``userType: "O"``; it now sends the
586
+ signed-in user's type from the sign-in response, defaulting to ``"O"``.
587
+ - **A reservation entry turns a powered-off heater on in the
588
+ simulator.** The controller skipped entries while powered off, as in
589
+ Vacation, on a misreading of the device test, which found that a
590
+ powered-off heater fires the entry and turns on in its mode (two
591
+ runs). ``Settings.powered_off`` tells power-off from Vacation; the
592
+ entry is judged as a mode write out of power-off, and the result
593
+ carries ``powered_on_by_entry``. In Vacation entries are still
594
+ skipped.
595
+
596
+ Documentation
597
+ -------------
598
+ - **What the device tests and the record measured.** Three new
599
+ explanation pages: :doc:`/explanation/recovery-speed` (heat output,
600
+ minutes per degF, compressor power and efficiency, start-up energy,
601
+ the air's effect, how much of the tank heats),
602
+ :doc:`/explanation/standby-loss` (the jacket loss, a circulation run's
603
+ heat and where it shows) and :doc:`/explanation/draws` (the delivered
604
+ temperature, the probes through a draw, how much hot water is left,
605
+ reading a cold-supply probe). The element pages gain hours and energy
606
+ by mode, the off point and the probe's lag; scheduling gains write and
607
+ entry timing; the simulator's Accuracy section gains time to ready and
608
+ the lower zone after a draw.
609
+ - **Where the simulator's defaults come from.** A table in
610
+ :doc:`/reference/simulator-inputs` gives each default parameter's
611
+ evidence: what it was measured or fitted on, its error, and how far
612
+ the per-retrain values move between retrains.
613
+ - **Accuracy figures corrected.** ``simulator.rst`` gave 18 % and the
614
+ 13 and 45 min deficit-band errors as physics alone; they scored the
615
+ physics with a learned correction, and are removed. The "cold starts
616
+ about 55 min short" figure is replaced by the recorded errors by time
617
+ left. The lower trigger's in-window wait is given with its full count.
618
+ - ``unknown-values``, ``tank-energy`` and the MQTT protocol reference now
619
+ cite the current NaviLink app release they were checked against.
620
+ - ``docs/openapi.yaml`` and ``rest_api.rst`` describe every endpoint above
621
+ plus ``POST /auth/session/refresh`` and ``POST /user/secured-sign-in``;
622
+ ``mqtt_protocol.rst`` documents ``res/ctrl-fail`` and the
623
+ ``app-connection`` event / Last-Will payload; ``diagnose-mqtt.rst`` gains
624
+ "Why was my command rejected?"; ``maintenance.rst`` gains a "Maintenance
625
+ Log" section; ``authenticate.rst`` explains the password-free renewal
626
+ chain.
627
+ - **Less noise.** Implementation notes, the docs' own revision history
628
+ and where things were ported from are gone from the pages, and
629
+ section titles are plainer. The ``sim`` extra is listed under
630
+ :doc:`/reference/installation`'s optional dependencies.
631
+ - **Corrections from the device tests and the recorded history.** The
632
+ explanation pages, the how-tos, the protocol reference and the
633
+ simulator's docstrings said things later measurements contradict:
634
+
635
+ - High Demand's element starts somewhere in (0.2, 0.4] degC short,
636
+ not (0.2, 0.7]; Electric ran upper first in the commanded runs, but
637
+ the record also has the lower running alone; the element-run and
638
+ mid-stint counts are the record's.
639
+ - Inside a TOU window the upper tank starts a cycle when it is more
640
+ than 14.4 degF short, at the opening or later. A setpoint write
641
+ starts one in 120 of 122 writes leaving the upper tank short (not
642
+ about 70 %), a lowering to the upper tank or below in none, and in
643
+ ``HEAT_PUMP`` a draw does not start one while the upper tank is at
644
+ or above the setpoint. The restart lockout measured 5.5 min.
645
+ - The simulator's accuracy figures are the ones measured with
646
+ start-up charged once (3.6 % and 4.4 % low; starts 31 of 40 held
647
+ out), and the heater's idle air reading is about 6 degF warm.
648
+ - ``HYBRID_BOOST_MODE`` is also Energy Saver with an element running;
649
+ ``touStatus`` means TOU is enabled, not that a period is in force.
650
+ - A device has held at least 32 reservation entries and 20 TOU
651
+ periods; a list write can be lost with no error, so read it back;
652
+ a powered-off heater runs its reservation entries; forcing a full
653
+ recovery with ``tou_override_status`` is untested.
654
+ - The ambient effect on the compressor is about 1.18x, not 1.7x; the
655
+ standby soak's figures, the 60 h gap prior and the circulation runs
656
+ behind the lower-zone drop are stated as measured.
657
+ - **Docs that contradicted the code or the vendor's sources** (#155). Each
658
+ now has one value with its source:
659
+
660
+ - The mode-ID table in ``device_features.rst`` numbered Energy Saver 2,
661
+ High Demand 3 and Electric 4. The IDs are Electric 2, Energy Saver 3,
662
+ High Demand 4, as in ``DhwOperationSetting`` and the NaviLink app's
663
+ ``MgppOperationMode``.
664
+ - ``track-energy.rst`` used ``comp_running_minute_total`` and
665
+ ``heater1/2_running_minute_total``, which do not exist. Lifetime run
666
+ times come from the diagnostics query, in hours
667
+ (``DeviceDiagnostics.ta_data.cumulated_op_time_comp``, ``_uhe``,
668
+ ``_lhe``).
669
+ - ``currentHeatUse`` is the ``HeatSource`` enum (0-3), not a bool.
670
+ - ``drEventStatus`` is the ``DREvent`` enum, not a bitfield.
671
+ - ``tou-recovery-cap.rst`` named ``dhw_outlet_temperature``, Home
672
+ Assistant's name for ``dhw_temperature``.
673
+ - Vacation is 1-99 days; the docs said 0-99 in some places, and the
674
+ library said 1-30.
675
+ - Anti-legionella heats to 140 degF (60 degC), defaults to a 7-day
676
+ period, is adjustable from 1 to 30 days and ships disabled (User's
677
+ Information Manual, section 3.14.2). The scheduling how-to said 158
678
+ degF, a 14-day default, periods up to 90 days and "mandatory".
679
+ - ``dhwTemperatureMin`` is the installer's minimum operation
680
+ temperature: 95-113 degF, factory default 95 degF. 104.9 degF was one
681
+ unit's setting, not a contradiction.
682
+ - ``ambientTemperature`` is the air around the unit, per the manual;
683
+ neither "intake" nor "outlet" is sourced. The heat pump runs only
684
+ between 41 and 113 degF.
685
+ - ``docs/explanation/advanced-features.rst`` is removed. Its weather
686
+ thresholds, COP figures, DR bitfield, stratification tables and code
687
+ sample were unsourced or wrong, and the sourced material is covered
688
+ by the protocol reference, ``what-starts-a-recovery`` and
689
+ ``tank-energy``.
690
+
691
+ - **Flow-rate units corrected** (#154). The protocol reference said
692
+ ``currentDhwFlowRate`` and ``recircDhwFlowRate`` were tenths of a GPM and
693
+ ``cumulatedDhwFlowRate`` was gallons, while the properties treated them as
694
+ tenths of an LPM and as litres. The properties are right. Navien's
695
+ NaviLink app (2.01.02, decompiled) shows ``currentDhwFlowRate`` as
696
+ ``raw / 10`` LPM in Celsius and ``raw / 3.785 / 10`` GPM in Fahrenheit.
697
+ The NWP500 manual lists the panel's water-flow reading in LPM even though
698
+ the panel defaults to GPM. The app's diagnostics screen shows the heater's
699
+ hot-water volume totals in whole litres in either setting. Flow rates are
700
+ 0.1 LPM and the cumulative counter is whole litres in both unit systems.
701
+ ``device_status.rst``, ``data_conversions.rst`` and ``mqtt_protocol.rst``
702
+ now say so, and ``DiagnosticsDhwUsage`` documents its units (litres, hours,
703
+ minutes, counts) instead of calling them unknown. ``unknown-values.rst``
704
+ notes that on the unit checked, ``cumulatedDhwFlowRate`` and every draw
705
+ counter read 0 after 392 days in use, so a 0 there does not mean no water
706
+ was used. Library output is unchanged. The raw fields no longer carry a
707
+ fixed ``"GPM"`` or ``"gal"`` ``unit_of_measurement``, which contradicted
708
+ their raw units. ``get_field_unit()``, which was already driven by
709
+ ``device_class``, returns the same units as before. New tests pin both
710
+ flow properties to the app's formula in each unit system.
711
+
712
+ Version 9.4.3 (2026-10-01)
713
+ ==========================
714
+
715
+ Changed
716
+ -------
717
+ - **Raw reservation writes are now validated** (#148).
718
+ ``update_reservations()`` and ``update_reservations_confirmed()`` sent
719
+ their entry dicts unchecked, so an unknown mode such as 7, an hour of 99
720
+ or a setpoint of 200 (100 degC) went to the device as-is. A Python
721
+ ``True`` for ``enable`` was worse: the confirmed helper read it as 1,
722
+ which means disabled. Every entry is now checked before anything is
723
+ sent, with the same rules as ``build_reservation_entry()``: the six
724
+ protocol fields present and plain integers, ``enable`` 1 or 2, ``week``
725
+ a day bitfield, ``hour`` 0-23, ``min`` 0-59 and ``mode`` a
726
+ ``DhwOperationSetting`` id. ``param`` is held to the setpoint range the
727
+ heater reports in its feature data (``dhw_temperature_min_raw`` to
728
+ ``dhw_temperature_max_raw``), which is requested if it is not cached;
729
+ clearing the schedule with an empty list needs none. A bad entry raises
730
+ ``ParameterValidationError`` or ``RangeValidationError``, and missing
731
+ feature data raises ``DeviceCapabilityError``. Callers that wrote
732
+ entries outside these ranges and relied on the heater to clamp them now
733
+ get an error instead. The checks are also available on their own as
734
+ ``nwp500.mqtt.control.validate_reservation_entries()``.
735
+
736
+ Documentation
737
+ -------------
738
+ - **New: what starts a recovery**
739
+ (``docs/explanation/what-starts-a-recovery.rst``). The lower-tank
740
+ turn-on setting is one trigger among several. Measured on one unit:
741
+ outside a TOU window the lower setting reads 104.9 degF at every
742
+ setpoint and never follows it; at a window's opening the upper tank
743
+ starts a cycle when it is well below the setpoint, and the lower trigger
744
+ waits for the close. **A control write re-evaluates**: outside a window,
745
+ in ``HEAT_PUMP`` with the compressor off, a setpoint write leaving the
746
+ upper tank below the new setpoint started the compressor within
747
+ 2 minutes in 112 of 117 writes (a median 30 s later), lowerings
748
+ included. 46 of 81 mode writes made with the compressor off did the
749
+ same. A setpoint dropped well below the upper tank stopped a running
750
+ compressor within 5 s (one run). A lowered setpoint did not hold off a
751
+ draw-driven start (one run, in ``ENERGY_SAVER``). The page also covers
752
+ traps for anyone re-deriving this from history.
753
+ - **TOU window: the two open cases measured, and three more results.** In
754
+ ``docs/how-to/schedule-operation.rst`` (one or two runs each): an entry
755
+ scheduled after a window fires on its minute; a mode held in a window
756
+ takes effect when the window ends naturally; writing ``HEAT_PUMP`` over
757
+ a held mode withdraws it; turning TOU back on inside a window drops a
758
+ running element mode; and the operation setting reports the configured
759
+ mode, not activity. The section's first measurement date is corrected to
760
+ local time (2026-09-19).
761
+ - **New: what the heating elements do**
762
+ (``docs/explanation/heating-elements.rst``). The four
763
+ ``he*TempSetting`` fields describe the thermostat, and **on entry to
764
+ a mode the thermostat is not what runs the element**: in
765
+ Electric and High Demand the on and off settings are the same value,
766
+ and every ``he*DiffTempSetting`` reads 0. Measured on one unit by
767
+ commanding the modes and watching, the start differential is
768
+ **mode-dependent**, one bracket per mode: ``ELECTRIC`` at most
769
+ **0.3 degC** below the setpoint, ``HIGH_DEMAND`` within
770
+ **(0.2, 0.7] degC**, ``ENERGY_SAVER`` within **(0.8, 1.0] degC**.
771
+ Whether the first two share one threshold is an open question, not a
772
+ finding. At 0.7 degC short, High Demand runs the element and Energy
773
+ Saver does not. These
774
+ are **entry** thresholds, not thermostats: in High Demand's steady
775
+ state this unit's history has the element on in 1 of 47 minutes
776
+ 0.2-0.3 degC short and 168 of 169 more than 2.2 degC short.
777
+ Neither differential is published anywhere in the status message.
778
+
779
+ **Later in a stint it depends on the mode.** From one
780
+ unit's recorded history, 17 of 23 mid-stint upper-element starts in
781
+ ``ENERGY_SAVER`` have the probe in the 39.4-41.1 degC band where
782
+ ``heUpperOnTempSetting`` rests, a median 0.2 degC below the field -
783
+ the element comes on as the probe reaches it. ``HIGH_DEMAND``, whose
784
+ ON setting tracks the setpoint, re-engages a median 2.1 degC *below*
785
+ it, which the field does not describe. So in ``ENERGY_SAVER`` entry is
786
+ the exception and the reference description is the rule; in
787
+ ``HIGH_DEMAND`` it is not; ``ELECTRIC`` is uncharacterised.
788
+ ``data_conversions.rst``'s warning now says which applies when.
789
+ A sampling note with it: the element runs in two very different
790
+ lengths. Of 115 upper-element runs over eight months, 20 lasted under
791
+ a minute (median 16 s), carrying 0.6 kWh against 93 kWh for the rest.
792
+ They are real elements, and not negligible to the probe: a 20 s burst
793
+ is ~0.3 degC. Because ``currentPower`` samples at a median 30 s, a
794
+ one-minute-resampled power series can miss one entirely, making a
795
+ 5.5 kW element look like 0.5 kW. Resample power with a maximum.
796
+
797
+ Confirmed on the device, one unit, 2026-09-21: in ``ENERGY_SAVER``
798
+ the upper element engages near ``heUpperOnTempSetting`` rather than a
799
+ fixed gap below the setpoint (setpoint lowered to 42.0 degC; a gap
800
+ predicted 22 degC; it engaged at 39.8, three times). It then runs to
801
+ ~0.9 degC under the setpoint-tracking ``heUpperOffTempSetting``, not
802
+ to a fixed point. Raising the setpoint in that mode starts the element
803
+ through the entry rule. And ``heUpperOnTempSetting`` rests at 40.5
804
+ degC reliably only in ``ENERGY_SAVER``; in ``HEAT_PUMP``, about half
805
+ the time.
806
+
807
+ Also documents Energy Saver's entry behaviour (the element engages
808
+ although ``heUpperOnTempSetting`` rests 33 degC below the tank), Electric's
809
+ upper-to-lower handover and the upper probe **falling** 1.9 degC while
810
+ the lower element runs, measured element power of 5,219 W against the
811
+ 5,000 W rating, and that a mode write may not take effect while a TOU
812
+ window is in force. ``device_status.rst``'s Heating Elements notes now
813
+ point at it.
814
+ - **currentInstPower includes the heating elements.** The field
815
+ table and *Power and Energy Fields* in the protocol reference, and the
816
+ ``current_inst_power`` field description, said it excludes element
817
+ power; ``track-energy.rst`` said it includes it. On a 240 V NWP500-65
818
+ it includes it: element-only minutes read about 5.1 kW, compressor
819
+ and element together about 5.5 kW, and the tank's heat gain agrees.
820
+ All four now say so, with the measurement and its limits (one model,
821
+ 240 V only). (#139)
822
+ - **TOU does not suspend reservations.** ``schedule-operation.rst``
823
+ said twice that it does, and its own priority table said otherwise. On
824
+ one NWP500, inside an active peak window, a reservation entry moved the
825
+ setpoint at its scheduled minute, twice. What the window holds back is
826
+ the entry's **mode**, as it does a direct ``set_operation_mode()``.
827
+ The mode is held, not discarded: a mode written in the window took
828
+ effect when TOU was switched off, and the upper element came on. The
829
+ priority table and Important Notes are corrected, and a new section
830
+ gives the measurements and their limits. ``heating-elements.rst`` now
831
+ links to it instead of to the issue. (#141)
832
+ - **tank-energy: what selects the second branch, and where usable_energy
833
+ under-reads.** ``tank-energy.rst`` left open what selects
834
+ ``full_recovery_energy``'s second branch. On one NWP500-65 over 50,819
835
+ ``HEAT_PUMP`` minutes, ``hpUpperOnTempSetting`` separates the branches
836
+ with no exceptions: 104.9 degF on the primary, raised to about the
837
+ setpoint during the afternoon TOU window on the secondary.
838
+ ``energy_to_setpoint`` shifts by the same 2 degC. Because of that,
839
+ ``usable_energy`` does not always hold up across the branch. In the window it
840
+ caps at ``full_recovery_energy`` once ``energy_to_setpoint`` hits 0,
841
+ up to about 3.6 degF short of the setpoint, and under-reads by up to
842
+ 561 Wh. The page and the ``usable_energy`` docstring now say so and
843
+ recommend the thermistors in that case. The ``full_recovery_energy``
844
+ and ``energy_to_setpoint`` descriptions in the model, the protocol
845
+ reference, the API reference and ``track-energy.rst`` now note the
846
+ window too. (#140)
7
847
 
8
848
  Version 9.4.2 (2026-09-17)
9
849
  ==========================
@@ -39,7 +879,7 @@ Version 9.4.0 (2026-09-12)
39
879
  ==========================
40
880
 
41
881
  This release realigns the MQTT command set with what the NaviLink app
42
- actually sends. The app (version 2.03.00) was decompiled and its request
882
+ sends. The app (version 2.03.00) was decompiled and its request
43
883
  builder compared with the library; every query it sends now has a
44
884
  method, six commands it never sends were removed, and the recirculation
45
885
  schedule write was corrected. Breaking changes are listed under
@@ -202,8 +1042,8 @@ Fixed
202
1042
  The cloud returns ``"error": {"errorCode": null}`` on some devices.
203
1043
  :class:`~nwp500.models.DeviceErrorSummary` typed the field
204
1044
  ``ErrorCode | int``, so validation of the whole listing failed and every
205
- caller - including the Home Assistant integration, which could no longer
206
- set up - lost the device entirely. ``error_code`` is now
1045
+ caller (including the Home Assistant integration, which could no longer
1046
+ set up) lost the device entirely. ``error_code`` is now
207
1047
  ``ErrorCode | int | None`` and defaults to ``None``: a null means the
208
1048
  cloud reported no code, which is not the same claim as ``NO_ERROR``.
209
1049
  Callers that treated the field as always-present should handle ``None``.
@@ -240,8 +1080,8 @@ Added
240
1080
  :class:`~nwp500.models.DeviceInfo` gains ``model_type_code`` and
241
1081
  ``installer_id``. ``error`` makes the device's last recorded fault
242
1082
  readable without an MQTT connection, including while the device is
243
- offline. Every new field is optional, so responses that omit them - such
244
- as ``/device/info``, which carries no ``error`` block - parse unchanged.
1083
+ offline. Every new field is optional, so responses that omit them (such
1084
+ as ``/device/info``, which carries no ``error`` block) parse unchanged.
245
1085
  ``error_code`` is typed ``ErrorCode | int`` (validated left to right, so a
246
1086
  known code becomes an ``ErrorCode`` member and only an unknown one stays a
247
1087
  plain int) so an unrecognised code cannot make a whole listing unparseable. ``docs/openapi.yaml`` is
@@ -258,17 +1098,17 @@ Removed
258
1098
  seconds. ``ctrl/tou/rd`` with that command code is the *write* - it is what
259
1099
  :meth:`~nwp500.NavienMqttClient.configure_tou_schedule` publishes and what
260
1100
  the vendor app publishes from its TOU editor
261
- (``TouScheduleViewmodel.setPublishMgppControlTou``) - and the device answers
1101
+ (``TouScheduleViewmodel.setPublishMgppControlTou``), and the device answers
262
1102
  on ``res/tou/rd`` only to confirm such a write. The vendor app reads TOU
263
1103
  over REST, which is the only TOU read the protocol has.
264
1104
 
265
1105
  Beyond returning nothing, the call published a write-shaped command with no
266
- schedule attached. This device ignored it - ``touStatus`` was unchanged
267
- across repeated calls - but a firmware that took it at face value could read
1106
+ schedule attached. This device ignored it (``touStatus`` was unchanged
1107
+ across repeated calls), but a firmware that took it at face value could read
268
1108
  it as "store an empty TOU schedule".
269
1109
 
270
1110
  **Migration**: use :meth:`~nwp500.NavienAPIClient.get_tou_info`, which
271
- returns the stored plan - rate name, utility, ZIP code and the seasonal
1111
+ returns the stored plan: rate name, utility, ZIP code and the seasonal
272
1112
  pricing intervals. The read itself is pure REST; it is keyed by the
273
1113
  controller serial number, which only the MQTT device-info response
274
1114
  publishes, so fetch that once and cache it.
@@ -284,12 +1124,12 @@ Fixed
284
1124
  month list, so ``--months 5`` took the single-month branch and printed
285
1125
  the same daily breakdown as ``--month 5`` instead of the monthly
286
1126
  summary. The handler now takes an explicit ``daily`` flag set by the
287
- option the user actually passed. ``energy`` also rejects ``--month``
1127
+ option the user passed. ``energy`` also rejects ``--month``
288
1128
  and ``--months`` together rather than silently preferring ``--month``,
289
1129
  and validates the ``--months`` range and format the way ``--month``
290
1130
  already did. All of that validation now happens while Click parses the
291
1131
  arguments, so a bad invocation is a usage error (exit code 2) before any
292
- authentication or MQTT connection is attempted - previously the command
1132
+ authentication or MQTT connection is attempted. Previously the command
293
1133
  body raised ``ClickException`` after connecting, where ``async_command``'s
294
1134
  catch-all reported it as an "Unexpected Error" with a traceback, and
295
1135
  anyone without working credentials saw an authentication failure instead
@@ -309,11 +1149,11 @@ Fixed
309
1149
  - **``firmware_payload_capture.py`` captured almost nothing.** Its wildcards
310
1150
  covered ``cmd/{type}/{client_id}/res/#`` and the event topic, but the device
311
1151
  acknowledges control and query commands on
312
- ``cmd/{type}/navilink-{mac}/{client_id}/res`` - which neither pattern
313
- matches - so the device status and device info responses were missed, along
1152
+ ``cmd/{type}/navilink-{mac}/{client_id}/res``, which neither pattern
1153
+ matches, so the device status and device info responses were missed, along
314
1154
  with every request published on the device path. A capture run recorded 2
315
1155
  payloads where it should have recorded 11. It now also subscribes to
316
- ``cmd/{type}/navilink-{mac}/#``, which additionally picks up traffic from
1156
+ ``cmd/{type}/navilink-{mac}/#``, which also picks up traffic from
317
1157
  other clients on the same device. Query results the device routes back to a
318
1158
  client-keyed topic, such as reservations and energy usage, were already
319
1159
  covered by the existing response wildcard.
@@ -323,8 +1163,8 @@ Version 9.3.0 (2026-08-03)
323
1163
 
324
1164
  **BREAKING CHANGES**: two independent corrections land together.
325
1165
 
326
- Tank energy values were wrong in two ways - a 2.5x unit-scale error and
327
- two actively misleading field names - and both are corrected. Reported
1166
+ Tank energy values were wrong in two ways, a 2.5x unit-scale error and
1167
+ two actively misleading field names, and both are corrected. Reported
328
1168
  tank energy is now 2.5x smaller and two public field names are removed.
329
1169
 
330
1170
  Separately, eight status flags change type from ``bool`` to
@@ -338,10 +1178,10 @@ Changed
338
1178
  not. Because ``totalEnergyCapacity`` is a whole-tank quantity, its slope
339
1179
  against the setpoint measures the quantum with no stratification
340
1180
  assumption: on a 65-gallon NWP500 that is 70.25 raw counts per Kelvin.
341
- The field turns out to be bimodal - at a fixed setpoint it takes one of
342
- two values exactly 2 degC apart - but both branches give the same slope
1181
+ The field is bimodal (at a fixed setpoint it takes one of
1182
+ two values exactly 2 degC apart), but both branches give the same slope
343
1183
  to within 0.2%, so the quantum is unaffected. Converting to Watt-hours needs a
344
- water mass, and a "65 gallon" tank does not hold 65 gallons - so taking
1184
+ water mass, and a "65 gallon" tank does not hold 65 gallons, so taking
345
1185
  the quantum to be round, as every other conversion in this protocol is,
346
1186
  4 Wh/count is the only candidate implying a water volume below the
347
1187
  nameplate (241.7 L). Two further checks agree: 183 individual heating
@@ -353,7 +1193,7 @@ Changed
353
1193
  be comparable.
354
1194
 
355
1195
  - **Energy fields renamed.**
356
- ``availableEnergyCapacity`` is not available energy - it is the energy
1196
+ ``availableEnergyCapacity`` is not available energy: it is the energy
357
1197
  still *needed* to reach the setpoint. It falls as the tank heats and
358
1198
  reaches zero when the tank is fully charged, so code treating it as
359
1199
  stored energy had the signal backwards (regression against mean tank
@@ -379,7 +1219,7 @@ Changed
379
1219
 
380
1220
  - **Status flags now preserve the device's unknown state.** The protocol
381
1221
  encodes these flags as ``0 = unknown, 1 = OFF, 2 = ON``, and the library
382
- was collapsing 0 to ``False`` - inventing an OFF the device never claimed.
1222
+ was collapsing 0 to ``False``, inventing an OFF the device never claimed.
383
1223
  Confirmed against Navien's own NaviLink app (2.03.00, versionCode 141),
384
1224
  which decodes exactly this set of fields through an enum declared
385
1225
  ``UNKNOWN(0), OFF(1), ON(2)``; two sibling enums render their zero as
@@ -409,7 +1249,7 @@ Added
409
1249
  fields are measured from the setpoint, so neither is a state of charge;
410
1250
  subtracting them cancels the setpoint and leaves the tank's heat above
411
1251
  the device's minimum operating temperature (104.9 degF), which is about
412
- the lowest temperature usable for a shower. Robust despite
1252
+ the lowest temperature usable for a shower. It holds despite
413
1253
  ``full_recovery_energy`` being bimodal, since both fields shift
414
1254
  together: the implied tank temperature tracks the thermistor mean to a
415
1255
  standard deviation of 0.57 degF over 12275 samples. Rendered by the CLI
@@ -468,13 +1308,13 @@ Fixed
468
1308
  in the changelog, the model field description and the protocol
469
1309
  reference.
470
1310
  - New ``docs/explanation/tank-energy.rst`` records what the two fields
471
- actually measure and the calibration evidence behind the scale change,
1311
+ measure and the calibration evidence behind the scale change,
472
1312
  including that Navien's own NaviLink app (2.03.00, versionCode 141)
473
1313
  reads neither field, so no vendor-side corroboration exists.
474
1314
 
475
1315
  - Documented that **temperature fields carry no sentinel at all**. The app
476
1316
  has no out-of-band constant (no ``0xFFFF``/``-999``/``-1``), no zero-guard
477
- in any display path, and formats whatever arrives - so a temperature of
1317
+ in any display path, and formats whatever arrives, so a temperature of
478
1318
  zero means zero. This closes a recurring source of bugs where zero-as-none
479
1319
  was applied to temperature converters and reported working sensors as
480
1320
  missing during cold-weather operation.
@@ -539,7 +1379,7 @@ Documentation
539
1379
  (``models/schedule.py``, ``models/_converters.py``). Corrected to
540
1380
  "target temperature, half-degrees Celsius; °C = param/2", and fixed the
541
1381
  accompanying doctest-style example, which showed a ``minute`` key where
542
- the function actually returns ``min``.
1382
+ the function returns ``min``.
543
1383
 
544
1384
  Version 9.2.0 (2026-07-06)
545
1385
  ==========================
@@ -572,7 +1412,7 @@ Changed
572
1412
  (``mqtt/_control_commands.py``) and ``DeviceSubscriptionsMixin``
573
1413
  (``mqtt/_device_subscriptions.py``). ``NavienMqttClient`` now inherits
574
1414
  both, so its public API is unchanged, while ``mqtt/client.py`` shrinks
575
- from ~1572 to ~1196 lines and reads more clearly as connection
1415
+ from ~1572 to ~1196 lines and reads as connection
576
1416
  orchestration plus a public façade. No behavior change.
577
1417
  - **Event system relationship clarified and de-duplicated** (`#102
578
1418
  <https://github.com/eman/nwp500-python/issues/102>`_): ``events.py`` and
@@ -757,8 +1597,8 @@ Bug Fixes
757
1597
  ``configure_recirculation_schedule()`` dumped the schedule models
758
1598
  as-is, double-nesting the request (``request.reservation.reservation``
759
1599
  / ``request.schedule.schedule``) and leaking pydantic computed display
760
- fields — including a unit-converted ``temperature`` alongside the raw
761
- half-Celsius ``param`` — into device commands. Both now send the flat,
1600
+ fields (including a unit-converted ``temperature`` alongside the raw
1601
+ half-Celsius ``param``) into device commands. Both now send the flat,
762
1602
  raw protocol shape used by ``update_reservations()``. A new
763
1603
  ``NavienBaseModel.to_protocol_dict()`` dumps only declared protocol
764
1604
  fields.
@@ -786,7 +1626,7 @@ Bug Fixes
786
1626
  so consumers kept displaying the stale error.
787
1627
  - **Fix TOU price encoding**: ``encode_price()`` used banker's rounding,
788
1628
  under-encoding exact half values at even boundaries (0.125 at
789
- ``decimal_point=2`` encoded to 12 instead of 13) — now uses
1629
+ ``decimal_point=2`` encoded to 12 instead of 13). It now uses
790
1630
  ``Decimal`` half-up rounding. ``build_tou_period()`` also treated
791
1631
  ``bool`` prices as pre-encoded integers (``True`` sent as price 1).
792
1632
  - **Fix protocol documentation errors**: ``decode_reservation_hex()``
@@ -801,7 +1641,7 @@ Bug Fixes
801
1641
  an undocumented SDK thread where asyncio operations are unsafe, and the
802
1642
  handler registry could be mutated on the event loop while the CRT
803
1643
  thread iterated it (``RuntimeError: dictionary changed size during
804
- iteration``, dropping the message — most likely during the
1644
+ iteration``, dropping the message, most likely during the
805
1645
  reconnect/resubscribe window). The awscrt callback now only marshals
806
1646
  the raw payload onto the event loop; parsing and dispatch run there,
807
1647
  iterating snapshots of the handler registries.
@@ -814,7 +1654,7 @@ Bug Fixes
814
1654
  ``set_unit_system()`` stored the preference in a ``ContextVar`` set in
815
1655
  the caller's task. Tasks scheduled from AWS CRT callback threads never
816
1656
  inherit that context, so the preference silently reverted to
817
- auto-detect for all MQTT-delivered data — ``nwp-cli --unit-system
1657
+ auto-detect for all MQTT-delivered data: ``nwp-cli --unit-system
818
1658
  metric monitor`` logged temperatures in the device's native unit while
819
1659
  labeling them °C. The preference is now a process-wide setting visible
820
1660
  from every task and thread.
@@ -830,7 +1670,7 @@ Bug Fixes
830
1670
  ``finally`` block.
831
1671
  - **Fix wait_for() docstring examples**: examples showed
832
1672
  ``args, _ = await emitter.wait_for(...)``, but ``wait_for`` returns
833
- just the args tuple — following the documented pattern raised
1673
+ just the args tuple, so following the documented pattern raised
834
1674
  ``ValueError`` or silently mis-assigned.
835
1675
  - **Serialize concurrent token refresh**: ``ensure_valid_token()`` and
836
1676
  ``refresh_token()`` had no lock, so concurrent callers (API 401 retry,
@@ -855,7 +1695,7 @@ Bug Fixes
855
1695
  - **Make** ``NavienAuthClient.__aenter__`` **idempotent**:
856
1696
  ``create_navien_clients()`` pre-enters the context and its docstring
857
1697
  instructs users to enter again with ``async with auth:``; the second
858
- entry created a fresh session and orphaned the first — which the API
1698
+ entry created a fresh session and orphaned the first, which the API
859
1699
  client was still pinned to. Re-entering now reuses the existing
860
1700
  session.
861
1701
  - **Fall back to full sign-in when stored-token refresh fails**:
@@ -964,7 +1804,7 @@ Bug Fixes
964
1804
  (e.g. the sleeping task's timer callback had already been enqueued), the
965
1805
  cancellation was silently dropped. The stale ``_reconnect_with_backoff`` task
966
1806
  would then complete its sleep, call ``_reconnect_func``, and tear down an
967
- otherwise healthy connection — restarting the entire disconnect → reconnect →
1807
+ otherwise healthy connection, restarting the entire disconnect → reconnect →
968
1808
  ``AWS_ERROR_MQTT_UNEXPECTED_HANGUP`` cycle. Fixed by replacing the direct
969
1809
  ``task.cancel()`` call with a ``_cancel_pending_reconnect()`` coroutine scheduled
970
1810
  via ``_schedule_coroutine``, so the cancellation runs on the event loop where
@@ -982,7 +1822,7 @@ Bug Fixes
982
1822
  SDK connection's built-in auto-reconnect would eventually succeed, creating two
983
1823
  active connections sharing the same client ID. Because AWS IoT allows only one
984
1824
  connection per client ID, the broker would kick one off, triggering
985
- ``on_connection_interrupted`` and starting yet another reconnection — an
1825
+ ``on_connection_interrupted`` and starting yet another reconnection: an
986
1826
  infinite connect/disconnect loop. Fixed by adding ``MqttConnection.close()``
987
1827
  (unconditional teardown regardless of ``_connected`` state) and calling it
988
1828
  before creating the replacement connection in both ``_active_reconnect()`` and
@@ -1014,9 +1854,9 @@ Bug Fixes
1014
1854
  a different loop in multi-loop test setups.
1015
1855
 
1016
1856
  - **Reservation temperature validation was US-only**: ``build_reservation_entry``
1017
- validated set-point temperatures against hardcoded Fahrenheit bounds (95–150 °F)
1857
+ validated set-point temperatures against hardcoded Fahrenheit bounds (95-150 °F)
1018
1858
  regardless of the active unit system. Validation now uses the current unit system
1019
- context: 35–65 °C in metric mode, 95–150 °F in US mode. Celsius users previously
1859
+ context: 35-65 °C in metric mode, 95-150 °F in US mode. Celsius users previously
1020
1860
  received spurious ``ValueError`` rejections for valid temperatures.
1021
1861
 
1022
1862
  - **Malformed reservation data silently dropped**: ``build_reservation_entry`` now
@@ -1104,11 +1944,11 @@ integrations like Home Assistant:
1104
1944
  2. **Refactor Control Calls**: Remove ``.control`` from all device command invocations.
1105
1945
  Instead of ``await mqtt.control.set_power(...)``, use ``await mqtt.set_power(...)``.
1106
1946
  3. **Handle Unit Conversions**: If your integration previously performed its own
1107
- conversions or relied on the library's eager conversion, note that
1947
+ conversions or relied on the library's eager conversion:
1108
1948
  ``DeviceStatus`` fields like ``dhw_temperature`` are now properties. They return
1109
1949
  values based on the global unit system context (``us_customary`` by default).
1110
1950
 
1111
- * **Home Assistant Tip**: To ensure your state tracking is immune to unit system
1951
+ * **Home Assistant Tip**: To keep your state tracking immune to unit system
1112
1952
  toggles within the library, use the new ``*_raw`` fields (e.g.,
1113
1953
  ``status.dhw_temperature_raw``) for comparison logic, and use the properties
1114
1954
  only for display or when a converted value is explicitly needed.
@@ -1193,7 +2033,7 @@ Fixed
1193
2033
  ``are_aws_credentials_expired``, and ``time_until_expiry`` used
1194
2034
  ``datetime.now()`` (naive, local time). During DST transitions or timezone
1195
2035
  changes this could cause incorrect expiry detection, leading to premature
1196
- re-authentication or use of an actually-expired token. Fixed by using
2036
+ re-authentication or use of an expired token. Fixed by using
1197
2037
  ``datetime.now(UTC)`` throughout, switching the ``issued_at`` field default
1198
2038
  to ``datetime.now(UTC)``, and adding a field validator to normalize any
1199
2039
  timezone-naive ``issued_at`` values loaded from old stored token files to UTC
@@ -1203,11 +2043,11 @@ Fixed
1203
2043
  format written by ``to_dict()`` for tokens stored before this fix.
1204
2044
  - **Vacation mode sent wrong MQTT command**: ``set_vacation_days()`` used
1205
2045
  ``CommandCode.GOOUT_DAY`` (33554466), which the device silently accepted
1206
- but did not activate vacation mode — the operating mode remained unchanged.
2046
+ but did not activate vacation mode; the operating mode remained unchanged.
1207
2047
  HAR capture of the official Navien app confirms the correct command is
1208
2048
  ``DHW_MODE`` (33554437) with ``param=[5, days]``
1209
2049
  (``DhwOperationSetting.VACATION``). The valid range has also been corrected
1210
- from 1–365 to 1–30 to match the device's actual constraint.
2050
+ from 1-365 to 1-30 to match the device's actual constraint.
1211
2051
  - **Duplicate AWS IoT subscribe calls on reconnect**: ``resubscribe_all()``
1212
2052
  called ``connection.subscribe()`` (a network round-trip to AWS IoT) once per
1213
2053
  handler per topic. If a topic had N handlers, N identical subscribe requests
@@ -1230,7 +2070,7 @@ Fixed
1230
2070
  - **Once-Listener Becomes Permanent With Duplicate Callbacks**: ``emit()``
1231
2071
  identified once-listeners via a ``set`` of ``(event, callback)`` tuples. If
1232
2072
  the same callback was registered twice with ``once=True``, the set
1233
- deduplicated the tuple — after the first emit the second listener lost its
2073
+ deduplicated the tuple: after the first emit the second listener lost its
1234
2074
  once-status and became permanent. Fixed by checking ``listener.once``
1235
2075
  directly on the ``EventListener`` object.
1236
2076
  - **Auth Session Leaked on Client Construction Failure**: In
@@ -1239,10 +2079,10 @@ Fixed
1239
2079
  ``auth_client.__aenter__()``, the auth session and its underlying
1240
2080
  ``aiohttp`` session would leak. Client construction is now wrapped in a
1241
2081
  ``try/except`` that calls ``auth_client.__aexit__()`` on failure.
1242
- Additionally, both ``except BaseException`` blocks have been replaced with
2082
+ Both ``except BaseException`` blocks have been replaced with
1243
2083
  ``except Exception`` (passing real exception info to ``__aexit__``) plus a
1244
2084
  separate ``except asyncio.CancelledError`` block that uses
1245
- ``asyncio.shield()`` to ensure cleanup completes even when the task is
2085
+ ``asyncio.shield()`` so cleanup completes even when the task is
1246
2086
  being cancelled.
1247
2087
  - **Hypothesis Tests Broke All Test Collection**: ``test_mqtt_hypothesis.py``
1248
2088
  imported ``hypothesis`` at module level; when it was not installed, pytest
@@ -1291,7 +2131,7 @@ Changed
1291
2131
 
1292
2132
  Fixed
1293
2133
  -----
1294
- - **Week Bitfield Encoding (CRITICAL)**: 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.
2134
+ - **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.
1295
2135
  - **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``.
1296
2136
  - **Reservation Set Command Timeout**: Fixed ``reservations set`` subscription pattern that had extra wildcards preventing response matching. Command now receives confirmations correctly.
1297
2137
  - **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.
@@ -1431,13 +2271,13 @@ Changed
1431
2271
 
1432
2272
  Fixed
1433
2273
  -----
1434
- - **Critical: Temperature Unit Bug in Set Operations**: Fixed incorrect temperature conversion when setting DHW temperature and reservations
2274
+ - **Temperature Unit Bug in Set Operations**: Fixed incorrect temperature conversion when setting DHW temperature and reservations
1435
2275
 
1436
2276
  - ``set_dhw_temperature()`` was calling ``fahrenheit_to_half_celsius()`` with unit-agnostic temperature parameter
1437
2277
  - ``build_reservation_entry()`` had the same issue
1438
2278
  - **Impact**: If user preferred Celsius, temperature would be interpreted as Fahrenheit, causing wrong setpoints
1439
2279
  - **Fix**: Use new ``preferred_to_half_celsius()`` that respects unit system context
1440
- - This was a critical data correctness bug that would cause incorrect device behavior for Celsius users
2280
+ - This was a data correctness bug that would cause incorrect device behavior for Celsius users
1441
2281
 
1442
2282
  Version 7.3.2 (2026-01-25)
1443
2283
  ==========================
@@ -1487,7 +2327,7 @@ Added
1487
2327
  - Available devices and regions determine conversion support (validated at runtime)
1488
2328
  - ``get_field_unit()`` method retrieves the correct unit suffix for any field
1489
2329
  - All conversions use Pydantic ``WrapValidator`` for transparent, automatic conversion
1490
- - Comprehensive documentation in ``docs/guides/unit_conversion.rst``
2330
+ - Documentation in ``docs/guides/unit_conversion.rst``
1491
2331
  - Example usage:
1492
2332
 
1493
2333
  .. code-block:: python
@@ -1542,7 +2382,7 @@ Fixed
1542
2382
 
1543
2383
  - **Issue**: Running ``nwp-cli --unit-system metric status`` displayed values in device's native format (imperial) with metric unit strings (e.g., "104.9 °C" instead of "40.5 °C")
1544
2384
  - **Root Cause**: MQTT callbacks from AWS CRT execute on different threads where context variables are not set. Since Python's context variables are task-local, the unit_system preference was not visible to validators
1545
- - **Solution**: Store unit_system in ``NavienMqttClient`` and ``MqttSubscriptionManager``. Before parsing MQTT messages, explicitly set the context variable in message handlers to ensure validators use the correct unit system regardless of thread context
2385
+ - **Solution**: Store unit_system in ``NavienMqttClient`` and ``MqttSubscriptionManager``. Before parsing MQTT messages, explicitly set the context variable in message handlers so validators use the correct unit system regardless of thread context
1546
2386
  - **Result**: Values and units now correctly convert when ``--unit-system`` override is specified
1547
2387
  - **Testing**: All 393 tests pass including new unit system context override tests
1548
2388
 
@@ -1704,7 +2544,7 @@ Removed
1704
2544
  - ``DeviceCapabilityChecker`` → ``MqttDeviceCapabilityChecker``
1705
2545
  - ``DeviceInfoCache`` → ``MqttDeviceInfoCache``
1706
2546
 
1707
- All functionality remains identical - only the class names changed.
2547
+ All functionality remains identical; only the class names changed.
1708
2548
 
1709
2549
  Added
1710
2550
  -----
@@ -1725,7 +2565,7 @@ Added
1725
2565
  - Simplified initialization for common use case (API + MQTT)
1726
2566
  - Proper async context manager support
1727
2567
  - Reduces boilerplate in application code
1728
- - Comprehensive documentation in ``docs/guides/authentication.rst``
2568
+ - Documentation in ``docs/guides/authentication.rst``
1729
2569
  - Example: ``examples/intermediate/advanced_auth_patterns.py``
1730
2570
 
1731
2571
  - **VolumeCode Enum**: Tank capacity identification with gallon values
@@ -1760,7 +2600,7 @@ Added
1760
2600
  - ``tou_override_to_python()``: TOU override status conversion
1761
2601
  - ``div_10()``: Divide by 10.0 utility
1762
2602
  - ``enum_validator()``: Generic enum factory
1763
- - Comprehensive documentation explaining device protocol quirks
2603
+ - Documentation explaining device protocol quirks
1764
2604
  - New ``converters.py`` module replacing scattered validators
1765
2605
 
1766
2606
  - **MQTT Event System**: Structured event handling for MQTT operations
@@ -1786,7 +2626,7 @@ Added
1786
2626
  - Eliminates hardcoded units in output formatter
1787
2627
  - Single source of truth: model metadata drives CLI display
1788
2628
 
1789
- - **Comprehensive Protocol Documentation**: Complete protocol reference documentation
2629
+ - **Protocol Documentation**: Complete protocol reference documentation
1790
2630
 
1791
2631
  - New ``docs/protocol/quick_reference.rst`` with command codes, field formats, and conversions
1792
2632
  - Converted protocol documentation to RST format for Sphinx integration
@@ -1852,7 +2692,7 @@ Changed
1852
2692
  - Added factory function patterns and examples
1853
2693
  - Improved context manager documentation
1854
2694
  - Added best practices and common patterns
1855
- - More comprehensive code examples
2695
+ - More code examples
1856
2696
 
1857
2697
  - **Model Refactoring**: Updated to use new converter modules
1858
2698
 
@@ -1932,7 +2772,7 @@ Added
1932
2772
  - ``configure_reservation_water_program()``: Water program reservation management
1933
2773
  - ``set_recirculation_mode()`` / ``configure_recirculation_schedule()`` / ``trigger_recirculation_hot_button()``: Recirculation pump control and scheduling
1934
2774
 
1935
- - **CLI Documentation Updates**: Comprehensive documentation updates for subcommand-based CLI
2775
+ - **CLI Documentation Updates**: Documentation updates for subcommand-based CLI
1936
2776
  - Complete rewrite of ``docs/python_api/cli.rst`` with full command reference
1937
2777
  - Updated README.rst with new subcommand syntax and examples
1938
2778
  - Added 8+ practical usage examples (cron jobs, automation, monitoring)
@@ -2015,14 +2855,14 @@ Removed
2015
2855
  Added
2016
2856
  -----
2017
2857
 
2018
- - **Python 3.12+ Optimizations**: Leverage latest Python features
2858
+ - **Python 3.12+ Optimizations**: Use the latest Python features
2019
2859
 
2020
2860
  - PEP 695: New type parameter syntax (``def func[T](...)`` instead of ``TypeVar``)
2021
2861
  - Use ``datetime.UTC`` constant instead of ``datetime.timezone.utc``
2022
2862
  - Native union syntax (``X | Y`` instead of ``Union[X, Y]``)
2023
2863
  - Cleaner generic type annotations throughout codebase
2024
2864
 
2025
- - **Enumerations Module (``src/nwp500/enums.py``)**: Comprehensive type-safe enums for device control and status
2865
+ - **Enumerations Module (``src/nwp500/enums.py``)**: Type-safe enums for device control and status
2026
2866
 
2027
2867
  - Status value enums: ``OnOffFlag``, ``Operation``, ``DhwOperationSetting``, ``CurrentOperationMode``, ``HeatSource``, ``DREvent``, ``WaterLevel``, ``FilterChange``, ``RecirculationMode``
2028
2868
  - Time of Use enums: ``TouWeekType``, ``TouRateType``
@@ -2031,7 +2871,7 @@ Added
2031
2871
  - Error code enum: ``ErrorCode`` with complete error code mappings
2032
2872
  - Human-readable text mappings for all enums (e.g., ``DHW_OPERATION_SETTING_TEXT``, ``ERROR_CODE_TEXT``)
2033
2873
  - Exported from main package: ``from nwp500 import OnOffFlag, ErrorCode, CommandCode``
2034
- - Comprehensive documentation in ``docs/enumerations.rst``
2874
+ - Documentation in ``docs/enumerations.rst``
2035
2875
  - Example usage in ``examples/error_code_demo.py``
2036
2876
 
2037
2877
  Changed
@@ -2058,7 +2898,7 @@ Changed
2058
2898
  - CLI and other consumers benefit from automatic enum name serialization
2059
2899
  - Text mappings available for custom formatting (e.g., `DHW_OPERATION_TEXT[enum]` → "Heat Pump Only")
2060
2900
 
2061
- - **Documentation**: Comprehensive updates across protocol and API documentation
2901
+ - **Documentation**: Updates across protocol and API documentation
2062
2902
 
2063
2903
  - ``docs/guides/time_of_use.rst``: Clarified TOU override status behavior (1=OFF/override active, 2=ON/normal operation)
2064
2904
  - ``docs/protocol/data_conversions.rst``: Updated TOU field descriptions with correct enum values
@@ -2099,7 +2939,7 @@ Added
2099
2939
  - Export diagnostics to JSON for analysis and debugging
2100
2940
  - Home Assistant integration support for custom components
2101
2941
  - ``examples/mqtt_diagnostics_example.py`` demonstrating usage patterns
2102
- - Comprehensive documentation in ``docs/MQTT_DIAGNOSTICS.rst``
2942
+ - Documentation in ``docs/MQTT_DIAGNOSTICS.rst``
2103
2943
  - Exported from main package: ``from nwp500 import MqttDiagnosticsCollector``
2104
2944
 
2105
2945
  Version 6.1.0 (2025-12-03)
@@ -2187,7 +3027,7 @@ Version 6.0.6 (2025-11-24)
2187
3027
  Added
2188
3028
  -----
2189
3029
 
2190
- - **Field Descriptions**: Added comprehensive Field descriptions to ``DeviceStatus`` and ``DeviceFeature`` models with full documentation details including units, ranges, and usage context
3030
+ - **Field Descriptions**: Added Field descriptions to ``DeviceStatus`` and ``DeviceFeature`` models with full documentation details including units, ranges, and usage context
2191
3031
 
2192
3032
  Fixed
2193
3033
  -----
@@ -2205,7 +3045,7 @@ Version 6.0.5 (2025-11-21)
2205
3045
  Fixed
2206
3046
  -----
2207
3047
 
2208
- - **CRITICAL Temperature Conversion Bug**: Corrected temperature conversion formula for 8 sensor fields that were displaying values ~100°F higher than expected. The v6.0.4 change incorrectly used division by 5 (pentacelsius) instead of division by 10 (decicelsius) for these fields:
3048
+ - **Temperature Conversion Bug**: Corrected temperature conversion formula for 8 sensor fields that were displaying values ~100°F higher than expected. The v6.0.4 change incorrectly used division by 5 (pentacelsius) instead of division by 10 (decicelsius) for these fields:
2209
3049
 
2210
3050
  - ``tank_upper_temperature`` - Water tank upper sensor
2211
3051
  - ``tank_lower_temperature`` - Water tank lower sensor
@@ -2432,7 +3272,7 @@ Fixed
2432
3272
  - Applied consistent cancellation handling across all MQTT operations (connect, disconnect, subscribe, unsubscribe, publish)
2433
3273
  - AWS CRT callbacks can now complete independently without raising InvalidStateError
2434
3274
  - Added debug logging when operations are cancelled for better diagnostics
2435
- - Ensures clean shutdown without spurious exception messages
3275
+ - Clean shutdown without spurious exception messages
2436
3276
 
2437
3277
  Version 5.0.1 (2025-10-27)
2438
3278
  ==========================
@@ -2455,7 +3295,7 @@ Changed
2455
3295
  Version 5.0.0 (2025-10-27)
2456
3296
  ==========================
2457
3297
 
2458
- **BREAKING CHANGES**: This release introduces a comprehensive enterprise exception architecture.
3298
+ **BREAKING CHANGES**: This release introduces an enterprise exception architecture.
2459
3299
  See migration guide below for details on updating your code.
2460
3300
 
2461
3301
  Added
@@ -2463,7 +3303,7 @@ Added
2463
3303
 
2464
3304
  - **Enterprise Exception Architecture**: Complete exception hierarchy for better error handling
2465
3305
 
2466
- - Created ``exceptions.py`` module with comprehensive exception hierarchy
3306
+ - Created ``exceptions.py`` module with an exception hierarchy
2467
3307
  - Added ``Nwp500Error`` as base exception for all library errors
2468
3308
  - Added MQTT-specific exceptions: ``MqttError``, ``MqttConnectionError``, ``MqttNotConnectedError``,
2469
3309
  ``MqttPublishError``, ``MqttSubscriptionError``, ``MqttCredentialsError``
@@ -2472,9 +3312,9 @@ Added
2472
3312
  ``DeviceOperationError``
2473
3313
  - All exceptions now include ``error_code``, ``details``, and ``retriable`` attributes
2474
3314
  - Added ``to_dict()`` method to all exceptions for structured logging
2475
- - Added comprehensive test suite in ``tests/test_exceptions.py``
2476
- - **Added comprehensive exception handling example** (``examples/exception_handling_example.py``)
2477
- - **Updated key examples** to demonstrate new exception handling patterns
3315
+ - Added a test suite in ``tests/test_exceptions.py``
3316
+ - **Added an exception handling example** (``examples/exception_handling_example.py``)
3317
+ - **Updated the main examples** to demonstrate new exception handling patterns
2478
3318
 
2479
3319
  Changed
2480
3320
  -------
@@ -2721,7 +3561,7 @@ Fixed
2721
3561
  - Preserves AWS credential expiration timestamps during token refresh
2722
3562
  - Prevents reconnection failures when connection interrupts after AWS credentials expire but before JWT tokens expire
2723
3563
  - Resolves AWS_ERROR_HTTP_WEBSOCKET_UPGRADE_FAILURE errors during reconnection attempts
2724
- - Improved test coverage for auth module from 31% to 60% with comprehensive test suite
3564
+ - Improved test coverage for auth module from 31% to 60%
2725
3565
 
2726
3566
  Version 3.1.3 (2025-10-24)
2727
3567
  ==========================
@@ -2769,7 +3609,7 @@ Fixed
2769
3609
 
2770
3610
  - Updated callback to match latest AWS IoT SDK signature: ``(connection, error, **kwargs)``
2771
3611
  - Fixed type annotations in ``MqttConnection`` for proper type checking
2772
- - Resolves mypy type checking errors and ensures AWS SDK compatibility
3612
+ - Resolves mypy type checking errors and keeps AWS SDK compatibility
2773
3613
  - Fixed E501 line length linting issue in connection interruption handler
2774
3614
 
2775
3615
  Version 3.0.0 (Unreleased)
@@ -2813,7 +3653,7 @@ Added
2813
3653
  - Prevents accidental comparison of user preferences with real-time states
2814
3654
  - Better IDE support with more specific enum types
2815
3655
 
2816
- - **Migration Support**: Comprehensive tools for smooth migration
3656
+ - **Migration Support**: Tools for migration
2817
3657
 
2818
3658
  - ``migrate_operation_mode_usage()`` helper function with programmatic guidance
2819
3659
  - ``MIGRATION.md`` with step-by-step migration instructions
@@ -2865,12 +3705,12 @@ Version 0.2 (Unreleased)
2865
3705
  Added
2866
3706
  -----
2867
3707
 
2868
- - **Local/CI Linting Synchronization**: Complete tooling to ensure consistent linting results
3708
+ - **Local/CI Linting Synchronization**: Tooling for consistent linting results
2869
3709
 
2870
3710
  - Multiple sync methods: tox (recommended), direct scripts, pre-commit hooks, Makefile commands
2871
3711
  - CI-identical scripts: ``scripts/lint.py`` and ``scripts/format.py`` mirror ``tox -e lint`` and ``tox -e format``
2872
3712
  - Pre-commit hooks configuration for automatic checking
2873
- - Comprehensive documentation: ``LINTING_SETUP.md``, ``DEVELOPMENT.md``, ``FIX_LINTING.md``
3713
+ - Documentation: ``LINTING_SETUP.md``, ``DEVELOPMENT.md``, ``FIX_LINTING.md``
2874
3714
  - Makefile commands: ``make ci-lint``, ``make ci-format``, ``make ci-check``
2875
3715
  - Standardized ruff configuration across all environments
2876
3716
  - Eliminates "passes locally but fails in CI" issues
@@ -2921,7 +3761,7 @@ Added
2921
3761
  - Enabled by default for reliability
2922
3762
  - ``queued_commands_count`` property for monitoring
2923
3763
  - ``clear_command_queue()`` method for manual management
2924
- - Integrates seamlessly with automatic reconnection
3764
+ - Works with automatic reconnection
2925
3765
  - Example: ``command_queue_demo.py``
2926
3766
  - Documentation: ``COMMAND_QUEUE.rst``
2927
3767
 
@@ -2960,7 +3800,7 @@ Added
2960
3800
  - ``subscribe_device_feature()`` - Automatic parsing of feature messages into ``DeviceFeature`` objects
2961
3801
  - ``subscribe_energy_usage()`` - Automatic parsing of energy usage responses into ``EnergyUsageResponse`` objects
2962
3802
  - Type-safe callbacks with IDE autocomplete support
2963
- - Comprehensive error handling and logging
3803
+ - Error handling and logging
2964
3804
  - Example scripts demonstrating usage patterns
2965
3805
 
2966
3806
  - **Energy Usage API (EMS)**: Historical energy consumption data
@@ -2975,7 +3815,7 @@ Added
2975
3815
  - Energy consumption data (Watt-hours)
2976
3816
  - Efficiency percentage calculations
2977
3817
 
2978
- - **Data Models**: Comprehensive type-safe models
3818
+ - **Data Models**: Type-safe models
2979
3819
 
2980
3820
  - ``DeviceStatus`` dataclass with 125 sensor and operational fields
2981
3821
  - ``DeviceFeature`` dataclass with 46 capability and configuration fields
@@ -3014,7 +3854,7 @@ Added
3014
3854
  - Typed callbacks implementation guide
3015
3855
  - Control command reference with verified command codes
3016
3856
  - Example scripts for common use cases
3017
- - Comprehensive troubleshooting guides
3857
+ - Troubleshooting guides
3018
3858
  - Complete energy data reference (ENERGY_DATA_SUMMARY.md)
3019
3859
 
3020
3860
  - **Examples**: Production-ready example scripts
@@ -3056,14 +3896,14 @@ Changed
3056
3896
  - Removed "Optional Feature" and "not required for basic operation" phrases
3057
3897
  - Fixed table rendering in DEVICE_STATUS_FIELDS.rst
3058
3898
  - Fixed JSON syntax in code examples
3059
- - Added comprehensive reconnection documentation
3060
- - Added comprehensive command queue documentation
3899
+ - Added reconnection documentation
3900
+ - Added command queue documentation
3061
3901
  - Cleaned up backward compatibility references (new library)
3062
3902
 
3063
3903
  Fixed
3064
3904
  -----
3065
3905
 
3066
- - **Critical Bug**: Thread-safe reconnection task creation from MQTT callbacks
3906
+ - **Bug**: Thread-safe reconnection task creation from MQTT callbacks
3067
3907
 
3068
3908
  - Fixed ``RuntimeError: no running event loop`` when connection is interrupted
3069
3909
  - Fixed ``RuntimeWarning: coroutine '_reconnect_with_backoff' was never awaited``
@@ -3071,9 +3911,9 @@ Fixed
3071
3911
  - Implemented ``_start_reconnect_task()`` helper method to properly create reconnection tasks
3072
3912
  - Uses existing ``_schedule_coroutine()`` method for thread-safe task scheduling
3073
3913
  - Prevents crashes during automatic reconnection after connection interruptions
3074
- - Ensures reconnection tasks are properly awaited and executed
3914
+ - Reconnection tasks are awaited and executed
3075
3915
 
3076
- - **Critical Bug**: Thread-safe event emission from MQTT callbacks
3916
+ - **Bug**: Thread-safe event emission from MQTT callbacks
3077
3917
 
3078
3918
  - Fixed ``RuntimeError: no running event loop in thread 'Dummy-1'``
3079
3919
  - MQTT callbacks run in separate threads created by AWS IoT SDK
@@ -3098,7 +3938,7 @@ Fixed
3098
3938
  - Updated built HTML documentation files
3099
3939
  - Protects privacy in public documentation
3100
3940
 
3101
- - **Critical Bug**: Device control command codes
3941
+ - **Bug**: Device control command codes
3102
3942
 
3103
3943
  - Fixed incorrect command code usage causing unintended power-off
3104
3944
  - Power-off now uses command code ``33554433``
@@ -3106,7 +3946,7 @@ Fixed
3106
3946
  - DHW mode control now uses command code ``33554437``
3107
3947
  - Discovered through network traffic analysis of official app
3108
3948
 
3109
- - **Critical Bug**: MQTT topic pattern matching with wildcards
3949
+ - **Bug**: MQTT topic pattern matching with wildcards
3110
3950
 
3111
3951
  - Fixed ``_topic_matches_pattern()`` to correctly handle ``#`` wildcard
3112
3952
  - Topics now match when message arrives on base topic (e.g., ``cmd/52/device/res``)
@@ -3126,9 +3966,9 @@ Fixed
3126
3966
  - Examples now subscribe to broader topic patterns
3127
3967
  - Subscribe to ``cmd/{device_type}/{device_topic}/#`` to catch all command messages
3128
3968
  - Subscribe to ``evt/{device_type}/{device_topic}/#`` to catch all event messages
3129
- - Ensures all device responses are received
3969
+ - All device responses are received
3130
3970
 
3131
- - **Enhancement**: Robust enum conversion with fallbacks
3971
+ - **Enhancement**: Enum conversion with fallbacks
3132
3972
 
3133
3973
  - Added try/except blocks for all enum conversions in ``DeviceStatus.from_dict()``
3134
3974
  - Added try/except blocks for all enum conversions in ``DeviceFeature.from_dict()``