nwp500-python 9.4.3__tar.gz → 9.5.1__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.3 → nwp500_python-9.5.1}/CHANGELOG.rst +811 -93
  2. {nwp500_python-9.4.3/src/nwp500_python.egg-info → nwp500_python-9.5.1}/PKG-INFO +48 -16
  3. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/README.rst +35 -15
  4. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/explanation/architecture.rst +3 -3
  5. nwp500_python-9.5.1/docs/explanation/draws.rst +292 -0
  6. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/explanation/heating-elements.rst +202 -96
  7. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/explanation/index.rst +4 -1
  8. nwp500_python-9.5.1/docs/explanation/recovery-speed.rst +339 -0
  9. nwp500_python-9.5.1/docs/explanation/simulator.rst +401 -0
  10. nwp500_python-9.5.1/docs/explanation/standby-loss.rst +237 -0
  11. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/explanation/tank-energy.rst +58 -61
  12. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/explanation/tou-recovery-cap.rst +38 -29
  13. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/explanation/unknown-values.rst +37 -32
  14. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/explanation/what-starts-a-recovery.rst +98 -61
  15. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/how-to/authenticate.rst +35 -2
  16. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/how-to/auto-recovery.rst +2 -2
  17. nwp500_python-9.5.1/docs/how-to/bluetooth.rst +329 -0
  18. nwp500_python-9.5.1/docs/how-to/calibrate-simulator.rst +214 -0
  19. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/how-to/diagnose-mqtt.rst +54 -29
  20. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/how-to/home-assistant.rst +7 -7
  21. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/how-to/index.rst +3 -0
  22. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/how-to/maintenance.rst +63 -16
  23. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/how-to/manage-units.rst +19 -20
  24. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/how-to/monitor-status.rst +9 -3
  25. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/how-to/optimize-tou.rst +14 -14
  26. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/how-to/queue-commands.rst +20 -20
  27. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/how-to/schedule-operation.rst +122 -96
  28. nwp500_python-9.5.1/docs/how-to/simulate-heater.rst +1174 -0
  29. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/how-to/track-energy.rst +36 -28
  30. nwp500_python-9.5.1/docs/openapi.yaml +1658 -0
  31. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/project/history.rst +161 -108
  32. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/configuration.rst +12 -12
  33. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/enumerations.rst +41 -16
  34. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/index.rst +2 -0
  35. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/installation.rst +18 -12
  36. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/protocol/data_conversions.rst +60 -42
  37. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/protocol/device_features.rst +44 -45
  38. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/protocol/device_status.rst +57 -52
  39. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/protocol/error_codes.rst +6 -6
  40. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/protocol/mqtt_protocol.rst +167 -43
  41. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/protocol/quick_reference.rst +4 -2
  42. nwp500_python-9.5.1/docs/reference/protocol/rest_api.rst +996 -0
  43. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/python_api/api_client.rst +311 -20
  44. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/python_api/auth_client.rst +76 -10
  45. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/python_api/cli.rst +20 -20
  46. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/python_api/events.rst +87 -21
  47. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/python_api/exceptions.rst +28 -28
  48. nwp500_python-9.5.1/docs/reference/python_api/models.rst +1053 -0
  49. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/reference/python_api/mqtt_client.rst +140 -27
  50. nwp500_python-9.5.1/docs/reference/python_api/sim.rst +309 -0
  51. nwp500_python-9.5.1/docs/reference/simulator-inputs.rst +1010 -0
  52. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/requirements.txt +3 -0
  53. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/tutorials/getting-started.rst +6 -6
  54. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/README.md +17 -0
  55. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/recirculation_control.py +8 -6
  56. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/intermediate/advanced_auth_patterns.py +2 -2
  57. nwp500_python-9.5.1/examples/simulator/lower-setpoint.json +8 -0
  58. nwp500_python-9.5.1/examples/simulator/plan.json +9 -0
  59. nwp500_python-9.5.1/examples/simulator/scenario.json +91 -0
  60. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/pyproject.toml +16 -0
  61. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/setup.cfg +68 -0
  62. nwp500_python-9.5.1/src/nwp500/__init__.py +464 -0
  63. nwp500_python-9.5.1/src/nwp500/api_client.py +1433 -0
  64. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/auth.py +394 -40
  65. nwp500_python-9.5.1/src/nwp500/ble/__init__.py +110 -0
  66. nwp500_python-9.5.1/src/nwp500/ble/__main__.py +163 -0
  67. nwp500_python-9.5.1/src/nwp500/ble/client.py +546 -0
  68. nwp500_python-9.5.1/src/nwp500/ble/mqtt_adapter.py +211 -0
  69. nwp500_python-9.5.1/src/nwp500/ble/protocol.py +890 -0
  70. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/cli/__main__.py +474 -100
  71. nwp500_python-9.5.1/src/nwp500/cli/ble_backend.py +196 -0
  72. nwp500_python-9.5.1/src/nwp500/cli/calibration.py +488 -0
  73. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/cli/handlers.py +2 -2
  74. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/cli/output_formatters.py +1 -1
  75. nwp500_python-9.5.1/src/nwp500/cli/recording.py +126 -0
  76. nwp500_python-9.5.1/src/nwp500/cli/simulate.py +507 -0
  77. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/config.py +2 -0
  78. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/converters.py +2 -2
  79. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/encoding.py +6 -5
  80. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/enums.py +79 -19
  81. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/models/__init__.py +40 -1
  82. nwp500_python-9.5.1/src/nwp500/models/account.py +61 -0
  83. nwp500_python-9.5.1/src/nwp500/models/device.py +325 -0
  84. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/models/diagnostics.py +5 -2
  85. nwp500_python-9.5.1/src/nwp500/models/maintenance.py +31 -0
  86. nwp500_python-9.5.1/src/nwp500/models/mqtt_models.py +232 -0
  87. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/models/schedule.py +15 -8
  88. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/models/status.py +69 -21
  89. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/models/tou.py +3 -0
  90. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/_control_commands.py +9 -4
  91. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/_device_subscriptions.py +76 -0
  92. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/client.py +55 -27
  93. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/command_queue.py +1 -1
  94. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/connection.py +30 -5
  95. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/control.py +114 -32
  96. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/periodic.py +1 -1
  97. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/reconnection.py +1 -1
  98. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/subscriptions.py +214 -4
  99. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/types.py +34 -0
  100. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/utils.py +17 -1
  101. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt_events.py +82 -1
  102. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/reservations.py +32 -18
  103. nwp500_python-9.5.1/src/nwp500/sim/__init__.py +130 -0
  104. nwp500_python-9.5.1/src/nwp500/sim/api.py +1934 -0
  105. nwp500_python-9.5.1/src/nwp500/sim/attribution.py +195 -0
  106. nwp500_python-9.5.1/src/nwp500/sim/book.py +1361 -0
  107. nwp500_python-9.5.1/src/nwp500/sim/calibration.py +3702 -0
  108. nwp500_python-9.5.1/src/nwp500/sim/circulation.py +191 -0
  109. nwp500_python-9.5.1/src/nwp500/sim/codec.py +92 -0
  110. nwp500_python-9.5.1/src/nwp500/sim/cold_supply.py +167 -0
  111. nwp500_python-9.5.1/src/nwp500/sim/controller.py +1584 -0
  112. nwp500_python-9.5.1/src/nwp500/sim/decay.py +1121 -0
  113. nwp500_python-9.5.1/src/nwp500/sim/defaults.py +197 -0
  114. nwp500_python-9.5.1/src/nwp500/sim/delivery.py +711 -0
  115. nwp500_python-9.5.1/src/nwp500/sim/draw_forecast.py +494 -0
  116. nwp500_python-9.5.1/src/nwp500/sim/engine.py +2308 -0
  117. nwp500_python-9.5.1/src/nwp500/sim/extensions.py +212 -0
  118. nwp500_python-9.5.1/src/nwp500/sim/heat_pump_recovery.py +268 -0
  119. nwp500_python-9.5.1/src/nwp500/sim/heating_trajectory.py +342 -0
  120. nwp500_python-9.5.1/src/nwp500/sim/inputs.py +352 -0
  121. nwp500_python-9.5.1/src/nwp500/sim/location_air.py +167 -0
  122. nwp500_python-9.5.1/src/nwp500/sim/meter.py +130 -0
  123. nwp500_python-9.5.1/src/nwp500/sim/observed.py +568 -0
  124. nwp500_python-9.5.1/src/nwp500/sim/params.py +1205 -0
  125. nwp500_python-9.5.1/src/nwp500/sim/physics/__init__.py +2 -0
  126. nwp500_python-9.5.1/src/nwp500/sim/physics/compressor.py +450 -0
  127. nwp500_python-9.5.1/src/nwp500/sim/physics/draw.py +474 -0
  128. nwp500_python-9.5.1/src/nwp500/sim/physics/element.py +623 -0
  129. nwp500_python-9.5.1/src/nwp500/sim/physics/refrigerant.py +168 -0
  130. nwp500_python-9.5.1/src/nwp500/sim/physics/tank.py +72 -0
  131. nwp500_python-9.5.1/src/nwp500/sim/profile.py +238 -0
  132. nwp500_python-9.5.1/src/nwp500/sim/ready.py +728 -0
  133. nwp500_python-9.5.1/src/nwp500/sim/record.py +333 -0
  134. nwp500_python-9.5.1/src/nwp500/sim/result.py +257 -0
  135. nwp500_python-9.5.1/src/nwp500/sim/schedules.py +243 -0
  136. nwp500_python-9.5.1/src/nwp500/sim/tank_physics.py +504 -0
  137. nwp500_python-9.5.1/src/nwp500/sim/tou_triggers.py +308 -0
  138. nwp500_python-9.5.1/src/nwp500/sim/units.py +20 -0
  139. nwp500_python-9.5.1/src/nwp500/sim_adapters.py +1323 -0
  140. nwp500_python-9.5.1/src/nwp500/sim_home_assistant.py +1157 -0
  141. nwp500_python-9.5.1/src/nwp500/sim_live.py +360 -0
  142. nwp500_python-9.5.1/src/nwp500/status_history.py +68 -0
  143. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/topic_builder.py +36 -0
  144. {nwp500_python-9.4.3 → nwp500_python-9.5.1/src/nwp500_python.egg-info}/PKG-INFO +48 -16
  145. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500_python.egg-info/SOURCES.txt +110 -2
  146. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500_python.egg-info/requires.txt +14 -0
  147. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500_python.egg-info/scm_file_list.json +109 -1
  148. nwp500_python-9.5.1/src/nwp500_python.egg-info/scm_version.json +8 -0
  149. nwp500_python-9.5.1/tests/_sim_site.py +168 -0
  150. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/conftest.py +20 -0
  151. nwp500_python-9.5.1/tests/fixtures/sim/engine_golden.json +377 -0
  152. nwp500_python-9.5.1/tests/fixtures/sim/heater_params.json +65 -0
  153. nwp500_python-9.5.1/tests/fixtures/sim/heater_params_fitted.json +65 -0
  154. nwp500_python-9.5.1/tests/test_api_endpoints_201.py +821 -0
  155. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_api_helpers.py +45 -13
  156. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_app_queries.py +51 -45
  157. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_auth.py +85 -56
  158. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_auth_session_lifecycle.py +24 -16
  159. nwp500_python-9.5.1/tests/test_auth_session_refresh.py +847 -0
  160. nwp500_python-9.5.1/tests/test_ble_mqtt_adapter.py +547 -0
  161. nwp500_python-9.5.1/tests/test_ble_protocol.py +803 -0
  162. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_bug_fixes.py +26 -20
  163. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_canonical_schedule.py +10 -10
  164. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_cli_basic.py +8 -7
  165. nwp500_python-9.5.1/tests/test_cli_calibrate.py +646 -0
  166. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_cli_commands.py +167 -97
  167. nwp500_python-9.5.1/tests/test_cli_simulate.py +527 -0
  168. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_cli_tristate_presentation.py +28 -9
  169. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_command_decorators.py +1 -1
  170. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_command_queue.py +7 -7
  171. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_device_info_cache.py +23 -16
  172. nwp500_python-9.5.1/tests/test_device_rest_models.py +241 -0
  173. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_events.py +37 -37
  174. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_exceptions.py +30 -30
  175. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_model_converters.py +69 -65
  176. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_models.py +78 -42
  177. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_mqtt_clean_session_resume.py +21 -18
  178. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_mqtt_client_init.py +73 -67
  179. nwp500_python-9.5.1/tests/test_mqtt_control_fail.py +423 -0
  180. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_mqtt_events.py +17 -8
  181. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_mqtt_hypothesis.py +5 -2
  182. nwp500_python-9.5.1/tests/test_mqtt_last_will.py +524 -0
  183. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_mqtt_reconnection.py +39 -17
  184. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_mqtt_reconnection_storm.py +55 -33
  185. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_mqtt_reliability.py +288 -64
  186. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_multi_device.py +26 -15
  187. nwp500_python-9.5.1/tests/test_navilink_201_enums.py +253 -0
  188. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_openei.py +2 -1
  189. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_protocol_correctness.py +200 -93
  190. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_public_api.py +37 -7
  191. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_reservation_validation.py +39 -4
  192. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_reservations.py +87 -1
  193. nwp500_python-9.5.1/tests/test_sim_adapters.py +389 -0
  194. nwp500_python-9.5.1/tests/test_sim_api.py +800 -0
  195. nwp500_python-9.5.1/tests/test_sim_book.py +488 -0
  196. nwp500_python-9.5.1/tests/test_sim_book_history.py +250 -0
  197. nwp500_python-9.5.1/tests/test_sim_calibration.py +863 -0
  198. nwp500_python-9.5.1/tests/test_sim_circulation.py +395 -0
  199. nwp500_python-9.5.1/tests/test_sim_codec.py +165 -0
  200. nwp500_python-9.5.1/tests/test_sim_cold_supply.py +145 -0
  201. nwp500_python-9.5.1/tests/test_sim_controller.py +1227 -0
  202. nwp500_python-9.5.1/tests/test_sim_defaults.py +201 -0
  203. nwp500_python-9.5.1/tests/test_sim_draw_forecast.py +174 -0
  204. nwp500_python-9.5.1/tests/test_sim_draw_loss.py +225 -0
  205. nwp500_python-9.5.1/tests/test_sim_element.py +869 -0
  206. nwp500_python-9.5.1/tests/test_sim_element_fit.py +135 -0
  207. nwp500_python-9.5.1/tests/test_sim_forecast.py +288 -0
  208. nwp500_python-9.5.1/tests/test_sim_golden.py +359 -0
  209. nwp500_python-9.5.1/tests/test_sim_home_assistant.py +1223 -0
  210. nwp500_python-9.5.1/tests/test_sim_hot_water.py +361 -0
  211. nwp500_python-9.5.1/tests/test_sim_import_contract.py +413 -0
  212. nwp500_python-9.5.1/tests/test_sim_live.py +474 -0
  213. nwp500_python-9.5.1/tests/test_sim_location_air.py +226 -0
  214. nwp500_python-9.5.1/tests/test_sim_meter.py +163 -0
  215. nwp500_python-9.5.1/tests/test_sim_no_clock_no_state.py +363 -0
  216. nwp500_python-9.5.1/tests/test_sim_observed.py +348 -0
  217. nwp500_python-9.5.1/tests/test_sim_profile.py +86 -0
  218. nwp500_python-9.5.1/tests/test_sim_readiness.py +879 -0
  219. nwp500_python-9.5.1/tests/test_sim_readiness_fit.py +296 -0
  220. nwp500_python-9.5.1/tests/test_sim_ready.py +406 -0
  221. nwp500_python-9.5.1/tests/test_sim_ready_api.py +770 -0
  222. nwp500_python-9.5.1/tests/test_sim_replay.py +232 -0
  223. nwp500_python-9.5.1/tests/test_sim_schedules.py +336 -0
  224. nwp500_python-9.5.1/tests/test_sim_tank_physics.py +184 -0
  225. nwp500_python-9.5.1/tests/test_status_history.py +236 -0
  226. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_temperature_converters.py +52 -40
  227. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_threading_model.py +32 -28
  228. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_token_storage.py +10 -5
  229. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_tou_api.py +1 -1
  230. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_unit_switching.py +86 -15
  231. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_unit_system_process_wide.py +4 -4
  232. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_utility_modules.py +38 -22
  233. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_utils.py +35 -22
  234. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tox.ini +6 -1
  235. nwp500_python-9.4.3/docs/explanation/advanced-features.rst +0 -497
  236. nwp500_python-9.4.3/docs/openapi.yaml +0 -805
  237. nwp500_python-9.4.3/docs/reference/protocol/rest_api.rst +0 -435
  238. nwp500_python-9.4.3/docs/reference/python_api/models.rst +0 -860
  239. nwp500_python-9.4.3/src/nwp500/__init__.py +0 -253
  240. nwp500_python-9.4.3/src/nwp500/api_client.py +0 -558
  241. nwp500_python-9.4.3/src/nwp500/models/device.py +0 -104
  242. nwp500_python-9.4.3/src/nwp500/models/mqtt_models.py +0 -31
  243. nwp500_python-9.4.3/src/nwp500_python.egg-info/scm_version.json +0 -8
  244. nwp500_python-9.4.3/tests/test_device_rest_models.py +0 -128
  245. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/.coveragerc +0 -0
  246. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  247. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/.github/copilot-instructions.md +0 -0
  248. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/.github/workflows/ci.yml +0 -0
  249. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/.github/workflows/release.yml +0 -0
  250. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/.gitignore +0 -0
  251. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/.pre-commit-config.yaml +0 -0
  252. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/.readthedocs.yml +0 -0
  253. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/AUTHORS.rst +0 -0
  254. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/CONTRIBUTING.rst +0 -0
  255. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/LICENSE.txt +0 -0
  256. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/Makefile +0 -0
  257. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/RELEASE.md +0 -0
  258. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/Makefile +0 -0
  259. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/_static/.gitignore +0 -0
  260. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/conf.py +0 -0
  261. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/index.rst +0 -0
  262. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/project/authors.rst +0 -0
  263. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/project/changelog.rst +0 -0
  264. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/project/contributing.rst +0 -0
  265. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/docs/project/license.rst +0 -0
  266. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/.ruff.toml +0 -0
  267. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/air_filter_reset.py +0 -0
  268. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/anti_legionella.py +0 -0
  269. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/auto_recovery.py +0 -0
  270. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/combined_callbacks.py +0 -0
  271. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/demand_response.py +0 -0
  272. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/device_capabilities.py +0 -0
  273. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/device_status_debug.py +0 -0
  274. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/energy_analytics.py +0 -0
  275. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/error_code_demo.py +0 -0
  276. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/firmware_payload_capture.py +0 -0
  277. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/mqtt_diagnostics.py +0 -0
  278. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/power_control.py +0 -0
  279. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/reconnection_demo.py +0 -0
  280. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/reservation_schedule.py +0 -0
  281. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/simple_auto_recovery.py +0 -0
  282. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/token_restoration.py +0 -0
  283. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/tou_openei.py +0 -0
  284. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/tou_schedule.py +0 -0
  285. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/advanced/water_reservation.py +0 -0
  286. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/beginner/01_authentication.py +0 -0
  287. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/beginner/02_list_devices.py +0 -0
  288. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/beginner/03_get_status.py +0 -0
  289. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/beginner/04_set_temperature.py +0 -0
  290. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/intermediate/command_queue.py +0 -0
  291. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/intermediate/device_status_callback.py +0 -0
  292. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/intermediate/error_handling.py +0 -0
  293. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/intermediate/event_driven_control.py +0 -0
  294. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/intermediate/improved_auth.py +0 -0
  295. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/intermediate/legacy_auth_constructor.py +0 -0
  296. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  297. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/intermediate/periodic_requests.py +0 -0
  298. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/intermediate/set_mode.py +0 -0
  299. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/intermediate/vacation_mode.py +0 -0
  300. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/mask.py +0 -0
  301. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/testing/periodic_device_info.py +0 -0
  302. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/testing/simple_periodic_info.py +0 -0
  303. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/testing/test_api_client.py +0 -0
  304. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/testing/test_mqtt_connection.py +0 -0
  305. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/testing/test_mqtt_messaging.py +0 -0
  306. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/examples/testing/test_periodic_minimal.py +0 -0
  307. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/scripts/README.md +0 -0
  308. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/scripts/bump_version.py +0 -0
  309. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/scripts/diagnose_mqtt_connection.py +0 -0
  310. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/scripts/extract_changelog.py +0 -0
  311. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/scripts/format.py +0 -0
  312. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/scripts/lint.py +0 -0
  313. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/scripts/setup-dev.py +0 -0
  314. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/scripts/validate_version.py +0 -0
  315. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/setup.py +0 -0
  316. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/_base.py +0 -0
  317. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/cli/__init__.py +0 -0
  318. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/cli/monitoring.py +0 -0
  319. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/cli/presentation.py +0 -0
  320. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/cli/rich_output.py +0 -0
  321. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/cli/token_storage.py +0 -0
  322. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/command_decorators.py +0 -0
  323. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/device_capabilities.py +0 -0
  324. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/device_info_cache.py +0 -0
  325. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/events.py +0 -0
  326. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/exceptions.py +0 -0
  327. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/factory.py +0 -0
  328. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/field_factory.py +0 -0
  329. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/models/_converters.py +0 -0
  330. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/models/energy.py +0 -0
  331. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/models/feature.py +0 -0
  332. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/models/firmware.py +0 -0
  333. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/__init__.py +0 -0
  334. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/diagnostics.py +0 -0
  335. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/mqtt/state_tracker.py +0 -0
  336. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/openei.py +0 -0
  337. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/py.typed +0 -0
  338. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/temperature.py +0 -0
  339. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/tou_schedule.py +0 -0
  340. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/unit_system.py +0 -0
  341. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500/utils.py +0 -0
  342. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  343. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  344. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  345. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/src/nwp500_python.egg-info/top_level.txt +0 -0
  346. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_device_capabilities.py +0 -0
  347. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/tests/test_tou_schedule.py +0 -0
  348. {nwp500_python-9.4.3 → nwp500_python-9.5.1}/uv.lock +0 -0
@@ -2,8 +2,726 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
- Unreleased
6
- ==========
5
+ Version 9.5.1 (2026-10-21)
6
+ ==========================
7
+
8
+ Fixed
9
+ -----
10
+ - **A steady Home Assistant sensor no longer goes stale in the
11
+ simulator** (#174). ``sim_home_assistant.parse_state`` dated a state by
12
+ ``last_updated``, which moves only when the value or its attributes
13
+ change. An air sensor holding the same value for over an hour failed
14
+ ``fresh()``, and the simulation fell back to the default air
15
+ temperature. A state is now dated by ``last_reported`` (Home Assistant
16
+ 2024.3 and later), then ``last_updated``, then ``last_changed``. The
17
+ history parsers still date each change row by its change.
18
+
19
+ Version 9.5.0 (2026-10-20)
20
+ ==========================
21
+
22
+ Added
23
+ -----
24
+ - **Bluetooth LE access to the controller** (``nwp500.ble``, optional
25
+ ``ble`` extra). The NWP500 exposes a GATT service that carries the
26
+ controller's native class-code protocol once the heater is put in pairing
27
+ mode (with the heater off, hold Back and Power for five seconds; it
28
+ advertises as ``HPWH1…``). The scan also lists a heater in Wi-Fi setup
29
+ mode (``HPWH4610``), which answers nothing, and the CLIs pick an
30
+ ``HPWH1`` heater over it.
31
+ The package frames and parses those packets (header, class code, address,
32
+ CRC-16/MODBUS), scans for the heater, and reads data classes with a
33
+ matched request/reply client; ``python -m nwp500.ble scan`` and
34
+ ``python -m nwp500.ble read`` dump the blocks. Checked on a unit: it
35
+ answers a DIP-switch block, error history, four diagnostics counters, the stored
36
+ time-of-use plan, device info, the controller serial numbers and a
37
+ software-data block; the boiler parameter classes 51-54 do not answer, so
38
+ installer parameters are not reachable yet. ``write_block()`` exists for
39
+ the eventual parameter write and is marked experimental. ``read``
40
+ dumps the classes the heater answers by default and exits 1 when none
41
+ answered; the readers that decode a block raise ``CrcMismatchError``
42
+ on a reply that failed its checksum.
43
+ ``read_tou_records()`` decodes the stored time-of-use plan into
44
+ ``TOUPeriod``, the cloud's model, reading the controller's 40-slot table
45
+ whole (a ``TOU_PLAN`` read's payload byte is the last slot, not a
46
+ length; ``build_tou_read()``). ``write_tou_records()`` stores a plan of
47
+ up to twelve intervals (``build_tou_write()``: the NaviLink app's clock
48
+ header, then the records in the read layout) and reads it back. It
49
+ refuses to write over a longer stored plan, whose extra intervals no
50
+ Bluetooth write can clear, and raises ``TouWriteError`` when the
51
+ heater refuses or stores something else. ``TOU_PLAN`` is the only
52
+ class written so far (``NWP500_WRITABLE_CLASSES``). A read that gets no reply
53
+ is sent once more, since the first request on a new connection
54
+ sometimes goes unanswered. ``DeviceInfoBlock`` (``DID``), ``ControllerSerials``
55
+ (``MSCD``) and ``SoftwareInfoBlock`` (``MSWD``) use the cloud models'
56
+ field names for the fields matched against them.
57
+ ``read_sub_status()`` decodes the ``SSD`` block into two DIP-switch
58
+ words (``SubStatus``), using the layout Navien's boiler service toolkit
59
+ reads; it is unconfirmed on the NWP500.
60
+ ``read_status()`` returns live status: the class-50 session command 200
61
+ makes the heater push the ``RTMD`` block about every 300 ms until the
62
+ connection closes, and ``RealTimeStatus`` decodes the fields checked
63
+ against the MQTT status report, in standby and while heating (operation
64
+ mode, state number, DHW temperature and setpoint, the six thermistors,
65
+ superheat and EEV step), under the ``DeviceStatus`` names and
66
+ conversions. A read ends the stream, and the next ``read_status()``
67
+ starts it again. The
68
+ heater's display unit is in no Bluetooth block, so Bluetooth
69
+ temperatures follow the unit-system preference.
70
+ - ``nwp-cli --backend ble`` (with ``--ble-address`` or a scan) runs
71
+ ``serial``, ``info`` and ``status`` over Bluetooth without signing in,
72
+ and refuses other commands. Without ``--backend``, commands use the cloud as before.
73
+ The handlers run unchanged against ``nwp500.ble.BleMqttAdapter``, which
74
+ offers the MQTT client methods the heater serves over Bluetooth and
75
+ raises ``BleUnsupportedError`` for the rest. Bluetooth temperatures use
76
+ the cloud models' conversion and unit-system preference;
77
+ ``DeviceInfoBlock.min_operation_temperature()`` follows the preference
78
+ when ``celsius`` is not given (Fahrenheit without one, as before).
79
+ - **New REST endpoints.** ``NavienAPIClient`` gains
80
+ ``get_push_settings()`` / ``set_push_settings()``
81
+ (``/user/get-push-settings``, ``/user/set-push-settings``),
82
+ ``record_filter_change()`` / ``record_cip_change()`` and
83
+ ``get_filter_history()`` / ``get_cip_history()`` (the cloud's maintenance
84
+ log; dates are sent ``YYYY-M-D`` without zero padding, as the app does),
85
+ ``get_display_preferences()`` / ``set_display_preferences()``
86
+ (``/device/preferences/format``; the cloud rejects it for the NWP500,
87
+ the app only uses it for the IoT-module hydro unit), ``get_user_profile()`` (``/user/info``),
88
+ ``get_firmware_detail()`` (``/firmware/info``; ``run_time`` is the
89
+ estimated update time in minutes), ``delete_tou()``
90
+ (``DELETE /device/tou``), ``get_manufacture_id()``,
91
+ ``get_error_history()`` (``/device/error/list``; installer accounts
92
+ only, an owner account gets ``COMMON_NOT_AUTHORIZED``), ``update_device_info()``
93
+ (``/device/change-info``), ``list_simple_devices()`` and
94
+ ``get_app_version()`` (unauthenticated; a 403 maintenance notice is
95
+ returned as ``server_check_text``; ``nwp500.get_app_version()`` asks
96
+ before sign-in, without a client). ``set_push_settings()`` keeps the
97
+ current value of a flag left ``None``, since the app always sends
98
+ every flag. ``get_error_history()`` entries carry the list's
99
+ ``timeZone`` (``time_zone``, ``occurred_at_local``). New models: ``PushSettings``,
100
+ ``DeviceInstaller``, ``FirmwareDeviceGroup``, ``FirmwareDetail``,
101
+ ``DisplayPreferences``, ``SimpleDevice``, ``DeviceErrorHistoryEntry``,
102
+ ``MaintenanceHistoryEntry``, ``UserProfile``, ``AppVersionInfo``. Push
103
+ flags are 1 = on, 2 = off, the reverse of the device's on/off flags, and
104
+ decode to ``True``/``False``/``None``. ``Device.installer`` and
105
+ ``Device.alarm_info`` are parsed from ``/device/info`` as the app does;
106
+ ``TOUSchedule.nickname`` is modelled.
107
+ - **AWS credentials renew without the password.**
108
+ ``NavienAuthClient.refresh_session()`` posts to ``/auth/session/refresh``
109
+ with the access token and updates only the AWS credential fields the
110
+ server returns, as the app does when the MQTT connection fails with an
111
+ auth error. ``secured_sign_in()`` posts to ``/user/secured-sign-in`` with
112
+ the access token and overlays whatever token fields come back (observed
113
+ live: the AWS credentials only; the JWTs are kept), which the app uses
114
+ for auto-login. When the AWS credentials expire, ``ensure_valid_token()``
115
+ and the context manager's stored-token check now try session refresh,
116
+ then secured sign-in, and only then the password sign-in; both token
117
+ steps are skipped when the access token has expired and cannot be
118
+ refreshed. ``re_authenticate()`` tries secured sign-in while the access
119
+ token is live, then the password sign-in, so a client restored from
120
+ tokens alone can use it too.
121
+ The MQTT client's deep reconnect calls ``refresh_session()`` after the
122
+ forced token refresh. ``AuthTokens`` gained an optional
123
+ ``aws_credentials_issued_at`` so a renewed AWS expiry survives
124
+ ``to_dict()``/restore. Stored tokens without the field load as before.
125
+ - **Control failures are parsed and surfaced.** When the device refuses a
126
+ control command it publishes on ``cmd/52/navilink-{mac}/res/ctrl-fail``
127
+ instead of acknowledging it; the library silently dropped that message.
128
+ It is now parsed into ``ControlFailResponse``
129
+ (``ControlFailCode.DHW_CONTROL_INTERVAL_EXCEEDED`` = 2 is the only code
130
+ the app interprets: the previous DHW control was too recent) and emitted
131
+ as the ``control_failed`` client event (``ControlFailedEvent`` with
132
+ ``own_command`` and ``reason``) for every device subscription, with a
133
+ WARNING log. ``subscribe_control_fail_response()`` /
134
+ ``unsubscribe_control_fail_response()`` add a plain callback.
135
+ - **MQTT Last-Will and app-connection presence.** Set
136
+ ``MqttConnectionConfig.last_will_device`` to register the app's Last-Will
137
+ on ``evt/52/navilink-{mac}/app-connection`` (``event.connection.status``
138
+ 0, QoS 1) so other clients learn when this one drops; every rebuilt
139
+ connection carries it. ``subscribe_app_connection()`` /
140
+ ``unsubscribe_app_connection()`` parse the topic into
141
+ ``AppConnectionEvent`` and emit ``app_connection_received``
142
+ (``AppConnectionReceivedEvent``). New ``MqttTopicBuilder`` helpers
143
+ ``control_fail_response_topic()`` and ``app_connection_topic()``.
144
+ - **``DeviceStatus.air_filter_remaining_hours``**: the app's remaining
145
+ filter life (``None`` when the reminder period is 0, else clamped at 0).
146
+ - **``DeviceStatus.minor_code``** (``minorCode``): the secondary alarm code
147
+ the app shows as ``R%03d``; 101 is the one it lets the user reset.
148
+ Defaults to 0 for payloads that omit it.
149
+ - **``DhwOperationSetting.STANDBY`` (0)**, the app's
150
+ ``MgppOperationMode.STANDBY``. ``DeviceStatus.dhw_operation_setting``
151
+ previously rejected a 0. It is status-only: ``set_dhw_mode()`` refuses
152
+ it, as it now refuses any id outside 1-6. ``sim_adapters.settings()``
153
+ refuses a Standby status, which the simulator does not model.
154
+ - **``DREvent.GRID_EMERGENCY`` (6).** The app declares CPE and Grid
155
+ Emergency both as id 5 but decodes ``drEventStatus`` by array position,
156
+ so raw 6 is Grid Emergency; ``CPE`` (5) stays "Customer Peak Event".
157
+ - **``UnitType``** NCB700 (22), NFB700_H (24), CAS_NFB700_H (25),
158
+ NPF_SYSTEM (514); **``DeviceType``** IOT_MODULE (8), NFB700_MAIN (50),
159
+ NFB700_SUB (51) (the app's names; ``NPF700_MAIN``/``NPF700_SUB`` remain
160
+ as aliases) and ``HPWH`` as an alias of 52, which is the NWP500
161
+ (``DefineData.HPWH_MODEL``), not an NPF700 WiFi module. A dumped device
162
+ of type 50 or 51 now names it ``NFB700_MAIN`` or ``NFB700_SUB``.
163
+ - **A recovery model's inputs, through ``nwp500.sim.simulate``** (#165).
164
+ Beside ``recovery=``, ``simulate`` takes what a learned model was
165
+ trained with and brings: ``start_hold`` (a start's hold by the
166
+ minutes since the compressor last stopped), ``running_hold`` (what
167
+ is left of a running compressor's), ``lead_draws`` (draws recorded
168
+ before the start), ``draw_hold_factor`` and ``physics_usable``. Each
169
+ goes to the engine as given. Left at their defaults, no answer
170
+ changes.
171
+ Lead draws move the tank as the scenario's own draws do. Like them,
172
+ they are refused on parameters with no fitted draw response
173
+ (``draw_model_unsupported``), and those the model reads carry
174
+ ``draw_uncalibrated`` and ``inlet_temp_modelled`` where they apply.
175
+ - **The air, its humidity and the inlet, live from Home Assistant**
176
+ (#166).
177
+
178
+ - ``nwp500.sim_home_assistant`` reads Home Assistant's REST API,
179
+ read-only. ``HomeAssistantEnvironment`` is a ``sim_live``
180
+ environment source:
181
+
182
+ - the air is the hourly forecast projected through a
183
+ ``location_air`` model, anchored to a reading beside the heater at
184
+ most an hour old (without a model, that reading held);
185
+ - the humidity is a reading at most an hour old;
186
+ - the inlet is the plateau of the last 72 hours' long draws, from
187
+ the inlet probe, the meter and the circulation's runs.
188
+
189
+ Units come from each entity's ``unit_of_measurement``. A part that
190
+ can't be read keeps its default, and the source says why.
191
+ ``record_inputs`` reads the same entities over a recorded history.
192
+ - ``nwp500.sim.meter.flow_gpm`` is a meter's flow from its volume
193
+ reports and its forward rate: the net volume over a trailing 90 s
194
+ where it holds at the draw line for 120 s, the rate elsewhere. It
195
+ finds the long draws the inlet plateau needs where the rate alone,
196
+ silent through a steady draw, misses some.
197
+ - ``nwp-cli simulate run``, ``snapshot`` and ``calibrate`` take
198
+ ``--sources FILE`` (or ``NWP500_SOURCES``), a TOML file naming the
199
+ entities, with the token in an environment variable.
200
+ ``calibrate`` reads the air, the humidity, the draws and the
201
+ circulation over the record, which makes the quiet test exact.
202
+ - ``sim_live.compose`` takes the heater's own air reading when the
203
+ environment source has none.
204
+ - **An optional heater simulator, ``nwp500.sim``** (#150, first part). The
205
+ core of a physics simulation of the NWP500: an R-134a compressor model
206
+ coupled to a two-zone tank, standby loss, the heating elements, draws,
207
+ and the heater's own controller (start triggers, the TOU hold and
208
+ recovery cap, the restart lockout, reservation entries, setpoint and
209
+ mode writes), walked as events by ``nwp500.sim.engine.simulate()``. It
210
+ is a port of an engine checked against recorded data from one
211
+ NWP500-65, and returns the same answers to the last bit. Install it
212
+ with the ``sim`` extra (numpy, pandas, scipy). It performs no I/O,
213
+ reads no clock and holds no module state, and ``import nwp500`` does
214
+ not load it. An import-linter contract in ``tox -e lint`` keeps it
215
+ from importing the MQTT or REST clients, aiohttp, the AWS IoT SDK or
216
+ ``logging``.
217
+ - **The simulator's API** (#150, second part).
218
+
219
+ - ``nwp500.sim.simulate(scenario, params)`` walks a frozen
220
+ ``Scenario``: the heater's state, settings, history, schedules,
221
+ environment, draws and the commands under evaluation.
222
+ - ``recovery()`` answers "when will the tank be hot?", and
223
+ ``standby()`` projects the tank coasting.
224
+ - The TOU periods become the windows the heater holds, and the
225
+ reservations become firings, with the local times in the
226
+ scenario's zone (``sim.schedules``).
227
+ - Results name every default they used.
228
+ - ``default_params(volume_code)`` carries one NWP500-65's parameters,
229
+ with the tank volume scaled for the 50 and 80 gal models.
230
+ - ``sim.codec`` reads and writes scenarios as JSON documents.
231
+ - ``nwp500.sim_adapters`` builds the inputs from ``DeviceStatus``,
232
+ ``DeviceFeature``, ``ReservationSchedule`` and ``TOUInfo``, reading
233
+ the raw fields so the unit preference cannot change them. It is
234
+ where units are crossed: ``environment``, ``loads``, ``write`` and
235
+ ``reservation_entry`` take a caller's values in the preferred units
236
+ (degC and litres under ``metric``), and ``path``, ``temperature``
237
+ and ``volume`` give a result's back in them. The simulator itself
238
+ works in degF and US gallons.
239
+ - Docs: a how-to (``docs/how-to/simulate-heater.rst``), from fetching
240
+ the heater's data to evaluating a change before sending it; a
241
+ reference of every input, its unit, default and source
242
+ (``docs/reference/simulator-inputs.rst``); an explanation of the
243
+ model and its accuracy (``docs/explanation/simulator.rst``); and the
244
+ API reference.
245
+
246
+ - Hot water at the tap: ``nwp500.sim.readiness(state, settings,
247
+ params)`` gives deliverable gallons now; ``Loads.windows`` judge a
248
+ shower or a bath (deliverable gallons and margin at the onset, and
249
+ whether the delivered water ran cold); ``SimulationResult.delivery``
250
+ is the delivered temperature along the walk (``sim.delivery``).
251
+ - The heat book (``sim.book``): the tank's heat by bookkeeping, walked
252
+ through the simulation as the engine's ``HeatLedger`` so a draw that
253
+ spends it takes the zones down. Started from the caller's knowledge
254
+ (``full_tank_book``) or their own record, never guessed from the
255
+ probes.
256
+ - ``HeaterParams`` gains ``readiness`` (the line, the outlet lag, the
257
+ fall rate, the dip tolerance, evidence) and ``book`` parts, with one
258
+ installation's measured defaults and their provenance.
259
+ - Time to ready: ``nwp500.sim.time_to_ready(scenario, params)`` says
260
+ whether the tank can run a bath, when it can in the heat pump and
261
+ in each element mode with the energy to get there, and how many
262
+ showers it is ready for, along the recovery the heater runs next
263
+ (``next_recovery``) (``sim.ready``).
264
+ - ``standby(..., settings=)`` says when the heater would start itself
265
+ on the coast and why; ``element_mode`` projects a recovery in an
266
+ element mode.
267
+ - A walk with draws outside the range the draw terms were fitted over
268
+ says ``draw_out_of_domain`` (``DrawParams.domain_upper_f``,
269
+ ``domain_volume_gal``); a write or a written reservation outside the
270
+ setpoint range the simulation runs is refused.
271
+
272
+ **Behaviour notes.** Answers that a client carrying an earlier copy of
273
+ this engine will see change at the switch:
274
+
275
+ - When no ``RecoveryModel`` answers (none given, or it declines) and
276
+ the physics integration stops at its bound short of the target,
277
+ there is no recovery and ``simulate`` raises ``SimulationError``.
278
+ The bound is no longer served as a duration.
279
+ - A recovery's reasons keep the served recovery's own (a truncated
280
+ integration, a model's diagnostics) beside ``recovery_low_confidence``.
281
+ - ``mode_recovery``'s ``upper_end_f`` is the zone's own temperature
282
+ when the element does not start or has nothing to do.
283
+ - A draw during a recovery honours ``draws_on`` and is priced on the
284
+ configured tank, not the 63.2 gal default.
285
+ - No ``hp_lower_on`` disables only the lower trigger; the upper
286
+ zone's at a TOU window's opening still starts the heater.
287
+ - A draw at a draw window's onset counts its cold side when the window
288
+ is judged.
289
+ - TOU prices are compared decoded at their own ``decimal_point``; each
290
+ period is laid out on the dates its day and season masks select
291
+ before windows that touch are joined across midnight.
292
+ - Scenario instants are held in UTC, so a horizon is elapsed time
293
+ across a daylight-saving change; a scenario document keeps which
294
+ fields were left to their defaults.
295
+ - ``sim.book.SpanLoss`` lets a client charge a loss the book does not
296
+ model; without one, the book charges none.
297
+ - Circulation runs within 10 minutes of each other are one run over
298
+ the whole schedule, so a coast boundary between them no longer
299
+ charges each its own displaced volume.
300
+ - ``default_params()`` has no mixing valve; ``site_example_params()``
301
+ is the installation the values were fitted at, its valve included
302
+ (``with_valve``, ``SITE_VALVE``). The core's draw functions default
303
+ to no valve as well. Each parameter says whether it describes the
304
+ heater or the installation (``scope_of``), and the frame it was
305
+ fitted in (``note_of``).
306
+ - Circulation (``sim.circulation``): an installation's runs
307
+ (``Loads.circulation``, ``sim_adapters.circulation`` for a weekly
308
+ schedule) charged as flowing draws and to the heat book while they
309
+ run, on ``HeaterParams.circulation``; with none, nothing is charged.
310
+ - The air where the heater stands, from the heater alone and a public
311
+ forecast: ``sim_adapters.location_air_reading`` (the heater's own
312
+ reading, only while the compressor runs) and
313
+ ``sim_adapters.location_air`` (an outdoor forecast projected by
314
+ ``sim.location_air``, anchored to that reading, with ``fit`` for
315
+ one's own model). ``time_to_ready`` and ``next_recovery`` take a
316
+ ``RecoveryModel``.
317
+ - Calibration from the heater's own record (``sim.calibration``,
318
+ ``sim_adapters.history`` to build one from ``DeviceStatus``
319
+ readings): the standby loss law and the zones' mixing constant from
320
+ quiet windows, then the compressor constant and evaporator approach
321
+ from heat-pump recoveries that reached their target, then input
322
+ power and apparent COP against the heater's own power readings, each
323
+ reported
324
+ with what it rested on (held-out error, windows used, a fit pinned
325
+ on a bound). An element run counts as busy in the quiet test, not
326
+ only the compressor and hot water in use.
327
+ - ``forecast``: when a recovery must start to have the tank ready at
328
+ a time, and whether it can (``feasible``, ``shortfall_minutes``),
329
+ to the heater's own stop threshold or a partial charge, with
330
+ ``no_heating_required`` (the lower zone at its target, or a zero
331
+ energy count outside a TOU window), ``not_scheduled``, and the
332
+ trajectory's electricity, stored heat and COP.
333
+ ``active_cycle``: what is left of a running recovery, its physics
334
+ estimate on the evaporator coil's live reading.
335
+ ``Settings.hp_lower_off_f``, ``hp_upper_on_f`` and
336
+ ``HeaterState.energy_to_setpoint_wh`` carry what they need, filled
337
+ by the adapters; ``recovery`` says ``not_scheduled`` in Vacation and
338
+ Power Off.
339
+ - The heater's profile (``sim.profile``): its modes and their energy,
340
+ setpoint range and step, depletion threshold and tank, under a
341
+ revision digest; and ``recovery_as_of``, a kept recovery aged to
342
+ now.
343
+ - The element terms from a heater's raw record (``sim.calibration``):
344
+ the upper probe's lag and rate as one line from clean stints
345
+ (``element_stints``, ``fit_element_lag``), where each element
346
+ switches off and what it draws per metered stint
347
+ (``element_off_points``, ``element_power``), and the zones' rates
348
+ and element power on the one-minute grid (``element_grid``,
349
+ ``fit_element_rates``), each with day-bootstrap intervals.
350
+ - Draws from a weekly draw forecast (``sim.draw_forecast``): an issue
351
+ checked whole, its expected draws (rate x per-draw mean, the
352
+ per-draw volume kept), the heater's draw windows, and the worst case
353
+ of each window's long draws first (``forecast_loads``,
354
+ ``simulate(withheld_windows=)``). A draw's domain is now judged by
355
+ its per-draw volume where it carries one (``Draw.per_draw_gal``),
356
+ not its fractional expectation.
357
+ - The mains from an inlet probe (``sim.cold_supply``,
358
+ ``sim_adapters.cold_supply``): the median plateau of recent long
359
+ draws with circulation off and the meter reading flow, from minute 9
360
+ of each; ``on_the_mains`` converts a book's gallons at it.
361
+ - What the heater did over a range, from its record, as the model
362
+ reads it: ``sim.record.Record`` (each signal's change rows, its
363
+ writes and the heater's own drops, TOU switches and window edges,
364
+ starts with elements corroborated by power;
365
+ ``sim_adapters.record``), ``sim.attribution`` (why each recorded
366
+ start happened) and ``sim.observed`` (modes and whether each was in
367
+ effect, when each source ran, counted and attributed starts, energy
368
+ per slot, the delivered temperature, and the record's gaps;
369
+ ``sim_adapters.observe``). ``replay`` reads its writes from the same
370
+ record.
371
+ - The readiness evidence and the heat book's own terms from a metered
372
+ record (``sim.calibration``, ``sim_adapters.draw_frame``): clean
373
+ draws and their demonstrated volumes reduced to the frontier, with a
374
+ time-ordered held-out check, and the cap after a cold lower zone;
375
+ the book's compressor rate, its slope with the air, standby UA and
376
+ circulation's loss fitted on re-anchors of full tanks
377
+ (``RecordedBook``, ``fit_book_terms``), its error by anchor age, and
378
+ the empty points in the book's frame and the drawable one, and the
379
+ error bands (``book_error_btu``: the p90 pooled over anchors an hour
380
+ to 18 h old, and the 18-24 h band's, each rounded up to 100 BTU).
381
+ With a meter, the COP fit prices the heat each cycle's draws and
382
+ circulation runs took, and the draws just before it
383
+ (``metered_withdrawn``, ``metered_lead``; ``calibrate(draws=,
384
+ circulation=)``), as the model's fit did, each run at the flow it
385
+ ran at (``circulation_flow_at=``, ``flow_lookup``). What could not be fitted
386
+ keeps the base's value and is named in ``HeaterParams.unfitted``;
387
+ every answer it reaches says ``physics_uncalibrated``,
388
+ ``energy_uncalibrated``, ``standby_uncalibrated`` or
389
+ ``tau_gap_prior``.
390
+ The probes' share of circulation's heat
391
+ (``circulation_windows``, ``fit_ungated_observability``), fitted
392
+ against the displaced volume it is used with.
393
+ Circulation's terms from its own telemetry: each run's measured heat
394
+ (``measure_runs``), the displaced volume that prices them
395
+ (``fit_displaced_gal``), its recent flow and the flow a past run ran
396
+ at (``recent_flow_gpm``, ``flow_lookup``), as the model fitted them.
397
+ The book's response to a draw, fitted on the record's long draws by
398
+ the procedures its values were fitted with: the cold front's place
399
+ and width (``fit_front``), the lower zone's settling after a draw
400
+ (``fit_front_settle``) and the upper zone's mixing
401
+ (``fit_zone_mix``), each with a day bootstrap.
402
+ - The heat book from a recorded history, as the model kept it:
403
+ ``sim.book.Step``, ``book_steps`` (the anchor on a quiet full tank
404
+ after a recovery, or a recovery's end during a draw; every step
405
+ booked from it; the inlet the median of 3 h of a probe), and
406
+ ``sim_adapters.book_from_history``, which grids the heater's
407
+ statuses, a hot-water meter's flow, circulation's runs and flow and
408
+ an inlet probe onto 10 s steps and books 18 h, else 72 h aged.
409
+ ``CirculationParams.meter_share`` is the share of a run's flow the
410
+ meter also reads; ``BookParams`` gains ``history_h``, ``draw_gpm``
411
+ and ``inlet_window_s``.
412
+ - ``sim_adapters.replay``: a past scenario from the statuses the
413
+ heater reported, as the model's replay read its record: the state
414
+ then, the compressor's last stop and the last heating from their
415
+ edges, the recorded setpoint and mode changes as writes (but the
416
+ heater's own drop to Heat Pump at a window), and the TOU switch's
417
+ changes (``Schedules.tou_switches``, which the walk applies).
418
+ - The heater's own TOU marker (``Settings.hp_upper_on_f`` raised):
419
+ ``time_to_ready`` and ``element_mode`` read "in a window now" from
420
+ it, and ``next_recovery`` lets it override the schedule for a
421
+ recovery running now (``api.marked_windows``), as the model did;
422
+ an unknown marker counts as in force. ``simulate(tou_windows_in_force=)``
423
+ takes the windows to keep, and TOU on with no periods says
424
+ ``tou_schedule_unknown``. ``element_mode(setpoint_f=)`` projects to a
425
+ what-if setpoint while the heater's own decides the window test.
426
+ - ``time_to_ready`` says ``inlet_temp_modelled`` when a booked answer
427
+ converts at an inlet nobody measured (``inlet_measured=``, or a
428
+ book whose steps measured it); ``next_recovery``'s coast loses the
429
+ heat circulation takes on it as well as standby; a served recovery
430
+ under 0.6 confidence says ``recovery_low_confidence``. ``standby``
431
+ reports the gallons drawn and the heat they took, circulation's
432
+ minutes and heat, and the mean air over the coast.
433
+ - ``default_params()`` names the installation's book, readiness and
434
+ draw terms in ``HeaterParams.unfitted`` (``INSTALLATION_TERMS``):
435
+ they are one installation's fits, so answers they reach say
436
+ ``book_uncalibrated``, ``readiness_uncalibrated`` or
437
+ ``draw_uncalibrated``. A ``Record`` drops missing readings on entry
438
+ and takes its installation's cadences and the heater's resting
439
+ threshold (``drop_within``, ``element_power_slack``,
440
+ ``resting_upper_on_f``). ``observe(profile=, revision_since=)``
441
+ names modes by a heater profile. ``schedules.rule_windows`` lays out
442
+ TOU rules that have no prices, such as a learned schedule. The
443
+ engine, the tank physics and the physics models are documented as
444
+ public (:doc:`/reference/python_api/sim`).
445
+ - **``nwp-cli simulate``, and the live sources it composes** (#151).
446
+
447
+ - ``nwp500.sim_live`` reads what a scenario needs from the device:
448
+ ``LiveHeaterSource`` (the state, the settings, the air and the
449
+ tank size, from one status and one feature read) and
450
+ ``LiveScheduleSource`` (the reservations over MQTT, the TOU
451
+ periods over REST, the TOU switch from the status).
452
+ ``compose()`` awaits them, and any other source with an ``async
453
+ read()`` (``Given`` for a value in hand), into a ``Scenario``. A
454
+ read with no reply raises ``LiveReadError``, as does a TOU read
455
+ that fails while TOU is on. The simulator still imports none of
456
+ it.
457
+ - ``nwp-cli simulate run`` walks the heater as it is now, a scenario
458
+ file, or the heater now with a scenario file laid on it
459
+ (``--live``). Each part is chosen on its own: a flag
460
+ (``--start``, ``--hours``, ``--tz``, ``--ambient``,
461
+ ``--environment``, ``--inlet``, ``--draws``, ``--plan``,
462
+ ``--params``), then the scenario file, then the device. It prints
463
+ a table (the heating intervals, the energy by source, readiness,
464
+ the draw windows and which ran cold, the defaults used), the
465
+ result document (``--format json``) or the path (``--format
466
+ csv``). ``nwp-cli simulate snapshot`` writes the heater and its
467
+ schedules now as a scenario file to edit and replay. Without the
468
+ ``sim`` extra the commands stop with an install hint.
469
+ ``examples/simulator/`` has a scenario, a plan and an overlay to
470
+ start from; :doc:`/how-to/simulate-heater` shows each use.
471
+ - **Record a heater's history and calibrate the simulator to it**
472
+ (#152).
473
+
474
+ - ``nwp500.status_history`` is the history's format: one JSON line
475
+ per status, ``{"t": <ISO 8601 instant>, "status": <raw payload>}``.
476
+ ``line()`` writes one and ``read()`` reads them back as
477
+ ``(instant, DeviceStatus)`` pairs. It does no I/O and needs no
478
+ extra.
479
+ - ``nwp-cli simulate record HISTORY`` appends the heater's statuses
480
+ to a file until stopped, by default asking for one every 30 s.
481
+ - ``nwp-cli simulate calibrate HISTORY`` fits the standby loss, the
482
+ compressor and the power to that history
483
+ (``sim.calibration.calibrate``). It prints a row for each term: the
484
+ value, what it was fitted from, its error, and whether it was
485
+ fitted or kept the base's value, and why. ``-o`` writes the fitted
486
+ ``HeaterParams`` document for ``simulate run --params``. The
487
+ standby fit needs the air beside the heater, from a sensor
488
+ (``--air``, a CSV file) or a constant (``--ambient``). Given, that
489
+ air is the recoveries' air too. The heater's own reading, filled
490
+ across the gaps between compressor runs, and an outdoor projection
491
+ both biased the fitted loss, so neither stands in for the air.
492
+ Without ``--humidity`` the fits get 50 %, their documented default.
493
+ A gap between statuses of more than 4 times the record's usual
494
+ spacing (at least 5 minutes) is unobserved, so no quiet window or
495
+ recovery spans a stopped recording. If a write fails, ``record``
496
+ stops with an error.
497
+ - ``sim_adapters.history(..., max_gap=)`` marks such gaps: the
498
+ sample before one is set to water in use and excluded.
499
+ - :doc:`/how-to/calibrate-simulator` covers the recording, the air,
500
+ the report, and what a history cannot tell (the draws, a
501
+ circulation loop the heater does not run).
502
+ - Three helpers the issue proposed are left out:
503
+
504
+ - **A mains-temperature estimate from the air.** The draw terms
505
+ were fitted against a measured inlet, so an estimated inlet
506
+ would bias every draw.
507
+ - **A draw-timing profile from the "hot water in use" flag.** The
508
+ flag also fires on circulation, and the expected draws from such
509
+ a profile never predicted better than no draws.
510
+ - **A check against the daily energy totals.** Power and COP are
511
+ fitted cycle by cycle.
512
+
513
+ Changed
514
+ -------
515
+ - **Recirculation mode values follow the NaviLink app.** ``RecirculationMode``
516
+ had 2 and 3 swapped and 4 wrong: the app's
517
+ ``MgppRecirculationOperationMode`` (sent by ``recirc-mode`` and decoded
518
+ from ``recircOperationMode``) is 1 Always on, 2 Weekly, 3 HotButton only,
519
+ 4 No recirculation. The members are now ``ALWAYS_ON``, ``WEEKLY``,
520
+ ``HOT_BUTTON_ONLY`` and ``OFF``; ``ALWAYS`` remains as an alias of
521
+ ``ALWAYS_ON``. **Behaviour change:** ``BUTTON``, ``SCHEDULE`` and
522
+ ``TEMPERATURE`` are removed, since their meanings were wrong; code that
523
+ sent ``RecirculationMode.BUTTON`` (2) was selecting the weekly schedule.
524
+ ``RECIRC_MODE_TEXT``, the ``recirc`` CLI command, the recirculation
525
+ example and the protocol docs follow. ``model_dump()`` writes enum
526
+ names, so a dumped status now says ``"ALWAYS_ON"`` for 1 (was
527
+ ``"ALWAYS"``) and ``"WEEKLY"`` for 2 (was ``"BUTTON"``); code keyed on
528
+ the old strings must follow.
529
+ - **Reservation mode 5 is Power Off, not Vacation.** Weekly-schedule
530
+ entries carry the app's ``MgppReservationMode`` (0 not reserved, 1 Heat
531
+ Pump, 2 Electric, 3 Energy Saver, 4 High Demand, 5 Power Off; there is
532
+ no vacation entry), now the ``ReservationMode`` enum.
533
+ ``ReservationEntry.mode_name`` resolves through it, so a mode-5 entry
534
+ reads "Power Off" instead of "Vacation" and mode 0 reads "Not Reserved".
535
+ **Behaviour change:** reservation writes (``add_reservation()``,
536
+ ``update_reservation()``, ``build_reservation_entry()`` and the MQTT
537
+ reservation validator) now accept mode 1-5 and reject 6, which was never
538
+ a valid entry mode. An entry already on the heater with mode 6 (earlier
539
+ versions documented 6 as Power Off) does not block the reservation
540
+ helpers: they send the entries they did not change as they read them
541
+ (``update_reservations(kept=)``). Changing such an entry needs a mode
542
+ of 1-5. The simulator's reservation entries take the same numbering
543
+ (``sim.inputs.EntryMode``); only 1-4 are modelled, as before.
544
+ - **``dr_override_status`` is an on/off flag, not hours.** The app decodes
545
+ it with its 0/1/2 on-off enum and shows "DR applied" when it is 2. The
546
+ field stays an ``int`` but no longer carries an ``hours`` unit; the new
547
+ ``DeviceStatus.dr_override_active`` is True when it is 2.
548
+ - **Air-filter life comes from ``cumulated_op_time_eva_fan``.** The app
549
+ computes remaining filter life as ``air_filter_alarm_period -
550
+ cumulated_op_time_eva_fan`` and never reads ``air_filter_alarm_elapsed``.
551
+ The two descriptions were the wrong way round; ``cumulated_op_time_eva_fan``
552
+ is fan hours since the last filter reset and ``air_filter_alarm_elapsed``
553
+ is unconfirmed.
554
+ - **``signal_app_connection()`` sends the app's message shape.** It
555
+ published ``{clientID, timestamp}``, a shape nothing reads. It now
556
+ publishes the NaviLink app's ``RequestLastWill`` envelope with
557
+ ``event.connection.status`` 1.
558
+ - Sign-in and ``/auth/refresh`` requests now send the library's
559
+ ``User-Agent`` and ``Content-Type`` headers, as every other request does.
560
+ - **The heater's own air reading is not as close as documented in
561
+ every season.** ``sim_adapters.location_air_reading``,
562
+ ``sim.location_air`` and the inputs reference said it read the air
563
+ beside the heater within about 0.35 °F while the compressor runs.
564
+ Measured against a sensor beside the heater, that held from August
565
+ to October. From May to July the reading was up to about 2 °F warm,
566
+ and in a run's first 2 minutes about 2.7 °F warm. The idle reading
567
+ is about 6 °F warm, not 5.
568
+ - **The CLI logs to stderr.** It logged to stdout, so a warning could
569
+ land in the middle of ``--json``, ``--raw`` or CSV output piped to
570
+ another program. Output stays on stdout.
571
+ - **The package root loads its exports on first use** (PEP 562).
572
+ ``from nwp500 import NavienMqttClient``, ``nwp500.DeviceStatus`` and
573
+ ``nwp500.mqtt`` work as before, and type checkers read the same names,
574
+ but ``import nwp500.sim`` and ``nwp500.sim_adapters`` no longer load
575
+ the clients, aiohttp or the AWS IoT SDK (tested in a fresh
576
+ interpreter).
577
+ - **Superheat is converted as a difference** (#153).
578
+ ``DeviceStatus.target_super_heat`` and ``current_super_heat`` converted
579
+ their raw tenths of a degree Celsius as absolute temperatures, so in
580
+ Fahrenheit they added 32. Superheat is a temperature difference (suction
581
+ minus evaporator), so they now use ``DeciCelsiusDelta``, as the
582
+ ``*_diff_temp_setting`` properties already do. Raw 50 now reads 9.0 degF
583
+ instead of 41.0 degF. **Behaviour change:** every Fahrenheit value drops
584
+ by 32, so stored or charted Fahrenheit superheat from earlier versions is
585
+ 32 degF too high. Celsius values are unchanged. The protocol docs now
586
+ describe both fields as differences.
587
+ - **Vacation accepts 1-99 days** (#155). ``set_dhw_mode()`` (vacation),
588
+ ``set_vacation_days()`` and ``set_vacation_duration()`` rejected anything
589
+ over 30 days. The NaviLink app's vacation picker and the front panel
590
+ (User's Information Manual, section 3.4.5) both offer 1-99, so the
591
+ library now accepts the same. The ``vacation`` and
592
+ ``vacation-duration`` CLI commands follow.
593
+
594
+ Fixed
595
+ -----
596
+ - ``FirmwareInfo.device_group`` was typed as a string; the app reads an
597
+ object ``{groupSeq, unitNum}`` (plus a top-level ``deviceGroupUnitNo``).
598
+ It is now ``FirmwareDeviceGroup``, with a bare string still accepted.
599
+ - ``convert_tou()`` hard-coded ``userType: "O"``; it now sends the
600
+ signed-in user's type from the sign-in response, defaulting to ``"O"``.
601
+ - **A reservation entry turns a powered-off heater on in the
602
+ simulator.** The controller skipped entries while powered off, as in
603
+ Vacation, on a misreading of the device test, which found that a
604
+ powered-off heater fires the entry and turns on in its mode (two
605
+ runs). ``Settings.powered_off`` tells power-off from Vacation; the
606
+ entry is judged as a mode write out of power-off, and the result
607
+ carries ``powered_on_by_entry``. In Vacation entries are still
608
+ skipped.
609
+
610
+ Documentation
611
+ -------------
612
+ - **What the device tests and the record measured.** Three new
613
+ explanation pages: :doc:`/explanation/recovery-speed` (heat output,
614
+ minutes per degF, compressor power and efficiency, start-up energy,
615
+ the air's effect, how much of the tank heats),
616
+ :doc:`/explanation/standby-loss` (the jacket loss, a circulation run's
617
+ heat and where it shows) and :doc:`/explanation/draws` (the delivered
618
+ temperature, the probes through a draw, how much hot water is left,
619
+ reading a cold-supply probe). The element pages gain hours and energy
620
+ by mode, the off point and the probe's lag; scheduling gains write and
621
+ entry timing; the simulator's Accuracy section gains time to ready and
622
+ the lower zone after a draw.
623
+ - **Where the simulator's defaults come from.** A table in
624
+ :doc:`/reference/simulator-inputs` gives each default parameter's
625
+ evidence: what it was measured or fitted on, its error, and how far
626
+ the per-retrain values move between retrains.
627
+ - **Accuracy figures corrected.** ``simulator.rst`` gave 18 % and the
628
+ 13 and 45 min deficit-band errors as physics alone; they scored the
629
+ physics with a learned correction, and are removed. The "cold starts
630
+ about 55 min short" figure is replaced by the recorded errors by time
631
+ left. The lower trigger's in-window wait is given with its full count.
632
+ - ``unknown-values``, ``tank-energy`` and the MQTT protocol reference now
633
+ cite the current NaviLink app release they were checked against.
634
+ - ``docs/openapi.yaml`` and ``rest_api.rst`` describe every endpoint above
635
+ plus ``POST /auth/session/refresh`` and ``POST /user/secured-sign-in``;
636
+ ``mqtt_protocol.rst`` documents ``res/ctrl-fail`` and the
637
+ ``app-connection`` event / Last-Will payload; ``diagnose-mqtt.rst`` gains
638
+ "Why was my command rejected?"; ``maintenance.rst`` gains a "Maintenance
639
+ Log" section; ``authenticate.rst`` explains the password-free renewal
640
+ chain.
641
+ - **Less noise.** Implementation notes, the docs' own revision history
642
+ and where things were ported from are gone from the pages, and
643
+ section titles are plainer. The ``sim`` extra is listed under
644
+ :doc:`/reference/installation`'s optional dependencies.
645
+ - **Corrections from the device tests and the recorded history.** The
646
+ explanation pages, the how-tos, the protocol reference and the
647
+ simulator's docstrings said things later measurements contradict:
648
+
649
+ - High Demand's element starts somewhere in (0.2, 0.4] degC short,
650
+ not (0.2, 0.7]; Electric ran upper first in the commanded runs, but
651
+ the record also has the lower running alone; the element-run and
652
+ mid-stint counts are the record's.
653
+ - Inside a TOU window the upper tank starts a cycle when it is more
654
+ than 14.4 degF short, at the opening or later. A setpoint write
655
+ starts one in 120 of 122 writes leaving the upper tank short (not
656
+ about 70 %), a lowering to the upper tank or below in none, and in
657
+ ``HEAT_PUMP`` a draw does not start one while the upper tank is at
658
+ or above the setpoint. The restart lockout measured 5.5 min.
659
+ - The simulator's accuracy figures are the ones measured with
660
+ start-up charged once (3.6 % and 4.4 % low; starts 31 of 40 held
661
+ out), and the heater's idle air reading is about 6 degF warm.
662
+ - ``HYBRID_BOOST_MODE`` is also Energy Saver with an element running;
663
+ ``touStatus`` means TOU is enabled, not that a period is in force.
664
+ - A device has held at least 32 reservation entries and 20 TOU
665
+ periods; a list write can be lost with no error, so read it back;
666
+ a powered-off heater runs its reservation entries; forcing a full
667
+ recovery with ``tou_override_status`` is untested.
668
+ - The ambient effect on the compressor is about 1.18x, not 1.7x; the
669
+ standby soak's figures, the 60 h gap prior and the circulation runs
670
+ behind the lower-zone drop are stated as measured.
671
+ - **Docs that contradicted the code or the vendor's sources** (#155). Each
672
+ now has one value with its source:
673
+
674
+ - The mode-ID table in ``device_features.rst`` numbered Energy Saver 2,
675
+ High Demand 3 and Electric 4. The IDs are Electric 2, Energy Saver 3,
676
+ High Demand 4, as in ``DhwOperationSetting`` and the NaviLink app's
677
+ ``MgppOperationMode``.
678
+ - ``track-energy.rst`` used ``comp_running_minute_total`` and
679
+ ``heater1/2_running_minute_total``, which do not exist. Lifetime run
680
+ times come from the diagnostics query, in hours
681
+ (``DeviceDiagnostics.ta_data.cumulated_op_time_comp``, ``_uhe``,
682
+ ``_lhe``).
683
+ - ``currentHeatUse`` is the ``HeatSource`` enum (0-3), not a bool.
684
+ - ``drEventStatus`` is the ``DREvent`` enum, not a bitfield.
685
+ - ``tou-recovery-cap.rst`` named ``dhw_outlet_temperature``, Home
686
+ Assistant's name for ``dhw_temperature``.
687
+ - Vacation is 1-99 days; the docs said 0-99 in some places, and the
688
+ library said 1-30.
689
+ - Anti-legionella heats to 140 degF (60 degC), defaults to a 7-day
690
+ period, is adjustable from 1 to 30 days and ships disabled (User's
691
+ Information Manual, section 3.14.2). The scheduling how-to said 158
692
+ degF, a 14-day default, periods up to 90 days and "mandatory".
693
+ - ``dhwTemperatureMin`` is the installer's minimum operation
694
+ temperature: 95-113 degF, factory default 95 degF. 104.9 degF was one
695
+ unit's setting, not a contradiction.
696
+ - ``ambientTemperature`` is the air around the unit, per the manual;
697
+ neither "intake" nor "outlet" is sourced. The heat pump runs only
698
+ between 41 and 113 degF.
699
+ - ``docs/explanation/advanced-features.rst`` is removed. Its weather
700
+ thresholds, COP figures, DR bitfield, stratification tables and code
701
+ sample were unsourced or wrong, and the sourced material is covered
702
+ by the protocol reference, ``what-starts-a-recovery`` and
703
+ ``tank-energy``.
704
+
705
+ - **Flow-rate units corrected** (#154). The protocol reference said
706
+ ``currentDhwFlowRate`` and ``recircDhwFlowRate`` were tenths of a GPM and
707
+ ``cumulatedDhwFlowRate`` was gallons, while the properties treated them as
708
+ tenths of an LPM and as litres. The properties are right. Navien's
709
+ NaviLink app (2.01.02, decompiled) shows ``currentDhwFlowRate`` as
710
+ ``raw / 10`` LPM in Celsius and ``raw / 3.785 / 10`` GPM in Fahrenheit.
711
+ The NWP500 manual lists the panel's water-flow reading in LPM even though
712
+ the panel defaults to GPM. The app's diagnostics screen shows the heater's
713
+ hot-water volume totals in whole litres in either setting. Flow rates are
714
+ 0.1 LPM and the cumulative counter is whole litres in both unit systems.
715
+ ``device_status.rst``, ``data_conversions.rst`` and ``mqtt_protocol.rst``
716
+ now say so, and ``DiagnosticsDhwUsage`` documents its units (litres, hours,
717
+ minutes, counts) instead of calling them unknown. ``unknown-values.rst``
718
+ notes that on the unit checked, ``cumulatedDhwFlowRate`` and every draw
719
+ counter read 0 after 392 days in use, so a 0 there does not mean no water
720
+ was used. Library output is unchanged. The raw fields no longer carry a
721
+ fixed ``"GPM"`` or ``"gal"`` ``unit_of_measurement``, which contradicted
722
+ their raw units. ``get_field_unit()``, which was already driven by
723
+ ``device_class``, returns the same units as before. New tests pin both
724
+ flow properties to the app's formula in each unit system.
7
725
 
8
726
  Version 9.4.3 (2026-10-01)
9
727
  ==========================
@@ -54,7 +772,7 @@ Documentation
54
772
  running element mode; and the operation setting reports the configured
55
773
  mode, not activity. The section's first measurement date is corrected to
56
774
  local time (2026-09-19).
57
- - **New: what the heating elements actually do**
775
+ - **New: what the heating elements do**
58
776
  (``docs/explanation/heating-elements.rst``). The four
59
777
  ``he*TempSetting`` fields describe the thermostat, and **on entry to
60
778
  a mode the thermostat is not what runs the element**: in
@@ -85,7 +803,7 @@ Documentation
85
803
  A sampling note with it: the element runs in two very different
86
804
  lengths. Of 115 upper-element runs over eight months, 20 lasted under
87
805
  a minute (median 16 s), carrying 0.6 kWh against 93 kWh for the rest.
88
- They are real elements, and not negligible to the probe - a 20 s burst
806
+ They are real elements, and not negligible to the probe: a 20 s burst
89
807
  is ~0.3 degC. Because ``currentPower`` samples at a median 30 s, a
90
808
  one-minute-resampled power series can miss one entirely, making a
91
809
  5.5 kW element look like 0.5 kW. Resample power with a maximum.
@@ -97,7 +815,7 @@ Documentation
97
815
  ~0.9 degC under the setpoint-tracking ``heUpperOffTempSetting``, not
98
816
  to a fixed point. Raising the setpoint in that mode starts the element
99
817
  through the entry rule. And ``heUpperOnTempSetting`` rests at 40.5
100
- degC reliably only in ``ENERGY_SAVER`` - in ``HEAT_PUMP`` about half
818
+ degC reliably only in ``ENERGY_SAVER``; in ``HEAT_PUMP``, about half
101
819
  the time.
102
820
 
103
821
  Also documents Energy Saver's entry behaviour (the element engages
@@ -132,7 +850,7 @@ Documentation
132
850
  with no exceptions: 104.9 degF on the primary, raised to about the
133
851
  setpoint during the afternoon TOU window on the secondary.
134
852
  ``energy_to_setpoint`` shifts by the same 2 degC. Because of that,
135
- ``usable_energy`` is not always robust to the branch. In the window it
853
+ ``usable_energy`` does not always hold up across the branch. In the window it
136
854
  caps at ``full_recovery_energy`` once ``energy_to_setpoint`` hits 0,
137
855
  up to about 3.6 degF short of the setpoint, and under-reads by up to
138
856
  561 Wh. The page and the ``usable_energy`` docstring now say so and
@@ -175,7 +893,7 @@ Version 9.4.0 (2026-09-12)
175
893
  ==========================
176
894
 
177
895
  This release realigns the MQTT command set with what the NaviLink app
178
- actually sends. The app (version 2.03.00) was decompiled and its request
896
+ sends. The app (version 2.03.00) was decompiled and its request
179
897
  builder compared with the library; every query it sends now has a
180
898
  method, six commands it never sends were removed, and the recirculation
181
899
  schedule write was corrected. Breaking changes are listed under
@@ -338,8 +1056,8 @@ Fixed
338
1056
  The cloud returns ``"error": {"errorCode": null}`` on some devices.
339
1057
  :class:`~nwp500.models.DeviceErrorSummary` typed the field
340
1058
  ``ErrorCode | int``, so validation of the whole listing failed and every
341
- caller - including the Home Assistant integration, which could no longer
342
- set up - lost the device entirely. ``error_code`` is now
1059
+ caller (including the Home Assistant integration, which could no longer
1060
+ set up) lost the device entirely. ``error_code`` is now
343
1061
  ``ErrorCode | int | None`` and defaults to ``None``: a null means the
344
1062
  cloud reported no code, which is not the same claim as ``NO_ERROR``.
345
1063
  Callers that treated the field as always-present should handle ``None``.
@@ -376,8 +1094,8 @@ Added
376
1094
  :class:`~nwp500.models.DeviceInfo` gains ``model_type_code`` and
377
1095
  ``installer_id``. ``error`` makes the device's last recorded fault
378
1096
  readable without an MQTT connection, including while the device is
379
- offline. Every new field is optional, so responses that omit them - such
380
- as ``/device/info``, which carries no ``error`` block - parse unchanged.
1097
+ offline. Every new field is optional, so responses that omit them (such
1098
+ as ``/device/info``, which carries no ``error`` block) parse unchanged.
381
1099
  ``error_code`` is typed ``ErrorCode | int`` (validated left to right, so a
382
1100
  known code becomes an ``ErrorCode`` member and only an unknown one stays a
383
1101
  plain int) so an unrecognised code cannot make a whole listing unparseable. ``docs/openapi.yaml`` is
@@ -394,17 +1112,17 @@ Removed
394
1112
  seconds. ``ctrl/tou/rd`` with that command code is the *write* - it is what
395
1113
  :meth:`~nwp500.NavienMqttClient.configure_tou_schedule` publishes and what
396
1114
  the vendor app publishes from its TOU editor
397
- (``TouScheduleViewmodel.setPublishMgppControlTou``) - and the device answers
1115
+ (``TouScheduleViewmodel.setPublishMgppControlTou``), and the device answers
398
1116
  on ``res/tou/rd`` only to confirm such a write. The vendor app reads TOU
399
1117
  over REST, which is the only TOU read the protocol has.
400
1118
 
401
1119
  Beyond returning nothing, the call published a write-shaped command with no
402
- schedule attached. This device ignored it - ``touStatus`` was unchanged
403
- across repeated calls - but a firmware that took it at face value could read
1120
+ schedule attached. This device ignored it (``touStatus`` was unchanged
1121
+ across repeated calls), but a firmware that took it at face value could read
404
1122
  it as "store an empty TOU schedule".
405
1123
 
406
1124
  **Migration**: use :meth:`~nwp500.NavienAPIClient.get_tou_info`, which
407
- returns the stored plan - rate name, utility, ZIP code and the seasonal
1125
+ returns the stored plan: rate name, utility, ZIP code and the seasonal
408
1126
  pricing intervals. The read itself is pure REST; it is keyed by the
409
1127
  controller serial number, which only the MQTT device-info response
410
1128
  publishes, so fetch that once and cache it.
@@ -420,12 +1138,12 @@ Fixed
420
1138
  month list, so ``--months 5`` took the single-month branch and printed
421
1139
  the same daily breakdown as ``--month 5`` instead of the monthly
422
1140
  summary. The handler now takes an explicit ``daily`` flag set by the
423
- option the user actually passed. ``energy`` also rejects ``--month``
1141
+ option the user passed. ``energy`` also rejects ``--month``
424
1142
  and ``--months`` together rather than silently preferring ``--month``,
425
1143
  and validates the ``--months`` range and format the way ``--month``
426
1144
  already did. All of that validation now happens while Click parses the
427
1145
  arguments, so a bad invocation is a usage error (exit code 2) before any
428
- authentication or MQTT connection is attempted - previously the command
1146
+ authentication or MQTT connection is attempted. Previously the command
429
1147
  body raised ``ClickException`` after connecting, where ``async_command``'s
430
1148
  catch-all reported it as an "Unexpected Error" with a traceback, and
431
1149
  anyone without working credentials saw an authentication failure instead
@@ -445,11 +1163,11 @@ Fixed
445
1163
  - **``firmware_payload_capture.py`` captured almost nothing.** Its wildcards
446
1164
  covered ``cmd/{type}/{client_id}/res/#`` and the event topic, but the device
447
1165
  acknowledges control and query commands on
448
- ``cmd/{type}/navilink-{mac}/{client_id}/res`` - which neither pattern
449
- matches - so the device status and device info responses were missed, along
1166
+ ``cmd/{type}/navilink-{mac}/{client_id}/res``, which neither pattern
1167
+ matches, so the device status and device info responses were missed, along
450
1168
  with every request published on the device path. A capture run recorded 2
451
1169
  payloads where it should have recorded 11. It now also subscribes to
452
- ``cmd/{type}/navilink-{mac}/#``, which additionally picks up traffic from
1170
+ ``cmd/{type}/navilink-{mac}/#``, which also picks up traffic from
453
1171
  other clients on the same device. Query results the device routes back to a
454
1172
  client-keyed topic, such as reservations and energy usage, were already
455
1173
  covered by the existing response wildcard.
@@ -459,8 +1177,8 @@ Version 9.3.0 (2026-08-03)
459
1177
 
460
1178
  **BREAKING CHANGES**: two independent corrections land together.
461
1179
 
462
- Tank energy values were wrong in two ways - a 2.5x unit-scale error and
463
- two actively misleading field names - and both are corrected. Reported
1180
+ Tank energy values were wrong in two ways, a 2.5x unit-scale error and
1181
+ two actively misleading field names, and both are corrected. Reported
464
1182
  tank energy is now 2.5x smaller and two public field names are removed.
465
1183
 
466
1184
  Separately, eight status flags change type from ``bool`` to
@@ -474,10 +1192,10 @@ Changed
474
1192
  not. Because ``totalEnergyCapacity`` is a whole-tank quantity, its slope
475
1193
  against the setpoint measures the quantum with no stratification
476
1194
  assumption: on a 65-gallon NWP500 that is 70.25 raw counts per Kelvin.
477
- The field turns out to be bimodal - at a fixed setpoint it takes one of
478
- two values exactly 2 degC apart - but both branches give the same slope
1195
+ The field is bimodal (at a fixed setpoint it takes one of
1196
+ two values exactly 2 degC apart), but both branches give the same slope
479
1197
  to within 0.2%, so the quantum is unaffected. Converting to Watt-hours needs a
480
- water mass, and a "65 gallon" tank does not hold 65 gallons - so taking
1198
+ water mass, and a "65 gallon" tank does not hold 65 gallons, so taking
481
1199
  the quantum to be round, as every other conversion in this protocol is,
482
1200
  4 Wh/count is the only candidate implying a water volume below the
483
1201
  nameplate (241.7 L). Two further checks agree: 183 individual heating
@@ -489,7 +1207,7 @@ Changed
489
1207
  be comparable.
490
1208
 
491
1209
  - **Energy fields renamed.**
492
- ``availableEnergyCapacity`` is not available energy - it is the energy
1210
+ ``availableEnergyCapacity`` is not available energy: it is the energy
493
1211
  still *needed* to reach the setpoint. It falls as the tank heats and
494
1212
  reaches zero when the tank is fully charged, so code treating it as
495
1213
  stored energy had the signal backwards (regression against mean tank
@@ -515,7 +1233,7 @@ Changed
515
1233
 
516
1234
  - **Status flags now preserve the device's unknown state.** The protocol
517
1235
  encodes these flags as ``0 = unknown, 1 = OFF, 2 = ON``, and the library
518
- was collapsing 0 to ``False`` - inventing an OFF the device never claimed.
1236
+ was collapsing 0 to ``False``, inventing an OFF the device never claimed.
519
1237
  Confirmed against Navien's own NaviLink app (2.03.00, versionCode 141),
520
1238
  which decodes exactly this set of fields through an enum declared
521
1239
  ``UNKNOWN(0), OFF(1), ON(2)``; two sibling enums render their zero as
@@ -545,7 +1263,7 @@ Added
545
1263
  fields are measured from the setpoint, so neither is a state of charge;
546
1264
  subtracting them cancels the setpoint and leaves the tank's heat above
547
1265
  the device's minimum operating temperature (104.9 degF), which is about
548
- the lowest temperature usable for a shower. Robust despite
1266
+ the lowest temperature usable for a shower. It holds despite
549
1267
  ``full_recovery_energy`` being bimodal, since both fields shift
550
1268
  together: the implied tank temperature tracks the thermistor mean to a
551
1269
  standard deviation of 0.57 degF over 12275 samples. Rendered by the CLI
@@ -604,13 +1322,13 @@ Fixed
604
1322
  in the changelog, the model field description and the protocol
605
1323
  reference.
606
1324
  - New ``docs/explanation/tank-energy.rst`` records what the two fields
607
- actually measure and the calibration evidence behind the scale change,
1325
+ measure and the calibration evidence behind the scale change,
608
1326
  including that Navien's own NaviLink app (2.03.00, versionCode 141)
609
1327
  reads neither field, so no vendor-side corroboration exists.
610
1328
 
611
1329
  - Documented that **temperature fields carry no sentinel at all**. The app
612
1330
  has no out-of-band constant (no ``0xFFFF``/``-999``/``-1``), no zero-guard
613
- in any display path, and formats whatever arrives - so a temperature of
1331
+ in any display path, and formats whatever arrives, so a temperature of
614
1332
  zero means zero. This closes a recurring source of bugs where zero-as-none
615
1333
  was applied to temperature converters and reported working sensors as
616
1334
  missing during cold-weather operation.
@@ -675,7 +1393,7 @@ Documentation
675
1393
  (``models/schedule.py``, ``models/_converters.py``). Corrected to
676
1394
  "target temperature, half-degrees Celsius; °C = param/2", and fixed the
677
1395
  accompanying doctest-style example, which showed a ``minute`` key where
678
- the function actually returns ``min``.
1396
+ the function returns ``min``.
679
1397
 
680
1398
  Version 9.2.0 (2026-07-06)
681
1399
  ==========================
@@ -708,7 +1426,7 @@ Changed
708
1426
  (``mqtt/_control_commands.py``) and ``DeviceSubscriptionsMixin``
709
1427
  (``mqtt/_device_subscriptions.py``). ``NavienMqttClient`` now inherits
710
1428
  both, so its public API is unchanged, while ``mqtt/client.py`` shrinks
711
- from ~1572 to ~1196 lines and reads more clearly as connection
1429
+ from ~1572 to ~1196 lines and reads as connection
712
1430
  orchestration plus a public façade. No behavior change.
713
1431
  - **Event system relationship clarified and de-duplicated** (`#102
714
1432
  <https://github.com/eman/nwp500-python/issues/102>`_): ``events.py`` and
@@ -893,8 +1611,8 @@ Bug Fixes
893
1611
  ``configure_recirculation_schedule()`` dumped the schedule models
894
1612
  as-is, double-nesting the request (``request.reservation.reservation``
895
1613
  / ``request.schedule.schedule``) and leaking pydantic computed display
896
- fields — including a unit-converted ``temperature`` alongside the raw
897
- half-Celsius ``param`` — into device commands. Both now send the flat,
1614
+ fields (including a unit-converted ``temperature`` alongside the raw
1615
+ half-Celsius ``param``) into device commands. Both now send the flat,
898
1616
  raw protocol shape used by ``update_reservations()``. A new
899
1617
  ``NavienBaseModel.to_protocol_dict()`` dumps only declared protocol
900
1618
  fields.
@@ -922,7 +1640,7 @@ Bug Fixes
922
1640
  so consumers kept displaying the stale error.
923
1641
  - **Fix TOU price encoding**: ``encode_price()`` used banker's rounding,
924
1642
  under-encoding exact half values at even boundaries (0.125 at
925
- ``decimal_point=2`` encoded to 12 instead of 13) — now uses
1643
+ ``decimal_point=2`` encoded to 12 instead of 13). It now uses
926
1644
  ``Decimal`` half-up rounding. ``build_tou_period()`` also treated
927
1645
  ``bool`` prices as pre-encoded integers (``True`` sent as price 1).
928
1646
  - **Fix protocol documentation errors**: ``decode_reservation_hex()``
@@ -937,7 +1655,7 @@ Bug Fixes
937
1655
  an undocumented SDK thread where asyncio operations are unsafe, and the
938
1656
  handler registry could be mutated on the event loop while the CRT
939
1657
  thread iterated it (``RuntimeError: dictionary changed size during
940
- iteration``, dropping the message — most likely during the
1658
+ iteration``, dropping the message, most likely during the
941
1659
  reconnect/resubscribe window). The awscrt callback now only marshals
942
1660
  the raw payload onto the event loop; parsing and dispatch run there,
943
1661
  iterating snapshots of the handler registries.
@@ -950,7 +1668,7 @@ Bug Fixes
950
1668
  ``set_unit_system()`` stored the preference in a ``ContextVar`` set in
951
1669
  the caller's task. Tasks scheduled from AWS CRT callback threads never
952
1670
  inherit that context, so the preference silently reverted to
953
- auto-detect for all MQTT-delivered data — ``nwp-cli --unit-system
1671
+ auto-detect for all MQTT-delivered data: ``nwp-cli --unit-system
954
1672
  metric monitor`` logged temperatures in the device's native unit while
955
1673
  labeling them °C. The preference is now a process-wide setting visible
956
1674
  from every task and thread.
@@ -966,7 +1684,7 @@ Bug Fixes
966
1684
  ``finally`` block.
967
1685
  - **Fix wait_for() docstring examples**: examples showed
968
1686
  ``args, _ = await emitter.wait_for(...)``, but ``wait_for`` returns
969
- just the args tuple — following the documented pattern raised
1687
+ just the args tuple, so following the documented pattern raised
970
1688
  ``ValueError`` or silently mis-assigned.
971
1689
  - **Serialize concurrent token refresh**: ``ensure_valid_token()`` and
972
1690
  ``refresh_token()`` had no lock, so concurrent callers (API 401 retry,
@@ -991,7 +1709,7 @@ Bug Fixes
991
1709
  - **Make** ``NavienAuthClient.__aenter__`` **idempotent**:
992
1710
  ``create_navien_clients()`` pre-enters the context and its docstring
993
1711
  instructs users to enter again with ``async with auth:``; the second
994
- entry created a fresh session and orphaned the first — which the API
1712
+ entry created a fresh session and orphaned the first, which the API
995
1713
  client was still pinned to. Re-entering now reuses the existing
996
1714
  session.
997
1715
  - **Fall back to full sign-in when stored-token refresh fails**:
@@ -1100,7 +1818,7 @@ Bug Fixes
1100
1818
  (e.g. the sleeping task's timer callback had already been enqueued), the
1101
1819
  cancellation was silently dropped. The stale ``_reconnect_with_backoff`` task
1102
1820
  would then complete its sleep, call ``_reconnect_func``, and tear down an
1103
- otherwise healthy connection — restarting the entire disconnect → reconnect →
1821
+ otherwise healthy connection, restarting the entire disconnect → reconnect →
1104
1822
  ``AWS_ERROR_MQTT_UNEXPECTED_HANGUP`` cycle. Fixed by replacing the direct
1105
1823
  ``task.cancel()`` call with a ``_cancel_pending_reconnect()`` coroutine scheduled
1106
1824
  via ``_schedule_coroutine``, so the cancellation runs on the event loop where
@@ -1118,7 +1836,7 @@ Bug Fixes
1118
1836
  SDK connection's built-in auto-reconnect would eventually succeed, creating two
1119
1837
  active connections sharing the same client ID. Because AWS IoT allows only one
1120
1838
  connection per client ID, the broker would kick one off, triggering
1121
- ``on_connection_interrupted`` and starting yet another reconnection — an
1839
+ ``on_connection_interrupted`` and starting yet another reconnection: an
1122
1840
  infinite connect/disconnect loop. Fixed by adding ``MqttConnection.close()``
1123
1841
  (unconditional teardown regardless of ``_connected`` state) and calling it
1124
1842
  before creating the replacement connection in both ``_active_reconnect()`` and
@@ -1150,9 +1868,9 @@ Bug Fixes
1150
1868
  a different loop in multi-loop test setups.
1151
1869
 
1152
1870
  - **Reservation temperature validation was US-only**: ``build_reservation_entry``
1153
- validated set-point temperatures against hardcoded Fahrenheit bounds (95–150 °F)
1871
+ validated set-point temperatures against hardcoded Fahrenheit bounds (95-150 °F)
1154
1872
  regardless of the active unit system. Validation now uses the current unit system
1155
- context: 35–65 °C in metric mode, 95–150 °F in US mode. Celsius users previously
1873
+ context: 35-65 °C in metric mode, 95-150 °F in US mode. Celsius users previously
1156
1874
  received spurious ``ValueError`` rejections for valid temperatures.
1157
1875
 
1158
1876
  - **Malformed reservation data silently dropped**: ``build_reservation_entry`` now
@@ -1240,11 +1958,11 @@ integrations like Home Assistant:
1240
1958
  2. **Refactor Control Calls**: Remove ``.control`` from all device command invocations.
1241
1959
  Instead of ``await mqtt.control.set_power(...)``, use ``await mqtt.set_power(...)``.
1242
1960
  3. **Handle Unit Conversions**: If your integration previously performed its own
1243
- conversions or relied on the library's eager conversion, note that
1961
+ conversions or relied on the library's eager conversion:
1244
1962
  ``DeviceStatus`` fields like ``dhw_temperature`` are now properties. They return
1245
1963
  values based on the global unit system context (``us_customary`` by default).
1246
1964
 
1247
- * **Home Assistant Tip**: To ensure your state tracking is immune to unit system
1965
+ * **Home Assistant Tip**: To keep your state tracking immune to unit system
1248
1966
  toggles within the library, use the new ``*_raw`` fields (e.g.,
1249
1967
  ``status.dhw_temperature_raw``) for comparison logic, and use the properties
1250
1968
  only for display or when a converted value is explicitly needed.
@@ -1329,7 +2047,7 @@ Fixed
1329
2047
  ``are_aws_credentials_expired``, and ``time_until_expiry`` used
1330
2048
  ``datetime.now()`` (naive, local time). During DST transitions or timezone
1331
2049
  changes this could cause incorrect expiry detection, leading to premature
1332
- re-authentication or use of an actually-expired token. Fixed by using
2050
+ re-authentication or use of an expired token. Fixed by using
1333
2051
  ``datetime.now(UTC)`` throughout, switching the ``issued_at`` field default
1334
2052
  to ``datetime.now(UTC)``, and adding a field validator to normalize any
1335
2053
  timezone-naive ``issued_at`` values loaded from old stored token files to UTC
@@ -1339,11 +2057,11 @@ Fixed
1339
2057
  format written by ``to_dict()`` for tokens stored before this fix.
1340
2058
  - **Vacation mode sent wrong MQTT command**: ``set_vacation_days()`` used
1341
2059
  ``CommandCode.GOOUT_DAY`` (33554466), which the device silently accepted
1342
- but did not activate vacation mode — the operating mode remained unchanged.
2060
+ but did not activate vacation mode; the operating mode remained unchanged.
1343
2061
  HAR capture of the official Navien app confirms the correct command is
1344
2062
  ``DHW_MODE`` (33554437) with ``param=[5, days]``
1345
2063
  (``DhwOperationSetting.VACATION``). The valid range has also been corrected
1346
- from 1–365 to 1–30 to match the device's actual constraint.
2064
+ from 1-365 to 1-30 to match the device's actual constraint.
1347
2065
  - **Duplicate AWS IoT subscribe calls on reconnect**: ``resubscribe_all()``
1348
2066
  called ``connection.subscribe()`` (a network round-trip to AWS IoT) once per
1349
2067
  handler per topic. If a topic had N handlers, N identical subscribe requests
@@ -1366,7 +2084,7 @@ Fixed
1366
2084
  - **Once-Listener Becomes Permanent With Duplicate Callbacks**: ``emit()``
1367
2085
  identified once-listeners via a ``set`` of ``(event, callback)`` tuples. If
1368
2086
  the same callback was registered twice with ``once=True``, the set
1369
- deduplicated the tuple — after the first emit the second listener lost its
2087
+ deduplicated the tuple: after the first emit the second listener lost its
1370
2088
  once-status and became permanent. Fixed by checking ``listener.once``
1371
2089
  directly on the ``EventListener`` object.
1372
2090
  - **Auth Session Leaked on Client Construction Failure**: In
@@ -1375,10 +2093,10 @@ Fixed
1375
2093
  ``auth_client.__aenter__()``, the auth session and its underlying
1376
2094
  ``aiohttp`` session would leak. Client construction is now wrapped in a
1377
2095
  ``try/except`` that calls ``auth_client.__aexit__()`` on failure.
1378
- Additionally, both ``except BaseException`` blocks have been replaced with
2096
+ Both ``except BaseException`` blocks have been replaced with
1379
2097
  ``except Exception`` (passing real exception info to ``__aexit__``) plus a
1380
2098
  separate ``except asyncio.CancelledError`` block that uses
1381
- ``asyncio.shield()`` to ensure cleanup completes even when the task is
2099
+ ``asyncio.shield()`` so cleanup completes even when the task is
1382
2100
  being cancelled.
1383
2101
  - **Hypothesis Tests Broke All Test Collection**: ``test_mqtt_hypothesis.py``
1384
2102
  imported ``hypothesis`` at module level; when it was not installed, pytest
@@ -1427,7 +2145,7 @@ Changed
1427
2145
 
1428
2146
  Fixed
1429
2147
  -----
1430
- - **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.
2148
+ - **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.
1431
2149
  - **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``.
1432
2150
  - **Reservation Set Command Timeout**: Fixed ``reservations set`` subscription pattern that had extra wildcards preventing response matching. Command now receives confirmations correctly.
1433
2151
  - **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.
@@ -1567,13 +2285,13 @@ Changed
1567
2285
 
1568
2286
  Fixed
1569
2287
  -----
1570
- - **Critical: Temperature Unit Bug in Set Operations**: Fixed incorrect temperature conversion when setting DHW temperature and reservations
2288
+ - **Temperature Unit Bug in Set Operations**: Fixed incorrect temperature conversion when setting DHW temperature and reservations
1571
2289
 
1572
2290
  - ``set_dhw_temperature()`` was calling ``fahrenheit_to_half_celsius()`` with unit-agnostic temperature parameter
1573
2291
  - ``build_reservation_entry()`` had the same issue
1574
2292
  - **Impact**: If user preferred Celsius, temperature would be interpreted as Fahrenheit, causing wrong setpoints
1575
2293
  - **Fix**: Use new ``preferred_to_half_celsius()`` that respects unit system context
1576
- - This was a critical data correctness bug that would cause incorrect device behavior for Celsius users
2294
+ - This was a data correctness bug that would cause incorrect device behavior for Celsius users
1577
2295
 
1578
2296
  Version 7.3.2 (2026-01-25)
1579
2297
  ==========================
@@ -1623,7 +2341,7 @@ Added
1623
2341
  - Available devices and regions determine conversion support (validated at runtime)
1624
2342
  - ``get_field_unit()`` method retrieves the correct unit suffix for any field
1625
2343
  - All conversions use Pydantic ``WrapValidator`` for transparent, automatic conversion
1626
- - Comprehensive documentation in ``docs/guides/unit_conversion.rst``
2344
+ - Documentation in ``docs/guides/unit_conversion.rst``
1627
2345
  - Example usage:
1628
2346
 
1629
2347
  .. code-block:: python
@@ -1678,7 +2396,7 @@ Fixed
1678
2396
 
1679
2397
  - **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")
1680
2398
  - **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
1681
- - **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
2399
+ - **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
1682
2400
  - **Result**: Values and units now correctly convert when ``--unit-system`` override is specified
1683
2401
  - **Testing**: All 393 tests pass including new unit system context override tests
1684
2402
 
@@ -1840,7 +2558,7 @@ Removed
1840
2558
  - ``DeviceCapabilityChecker`` → ``MqttDeviceCapabilityChecker``
1841
2559
  - ``DeviceInfoCache`` → ``MqttDeviceInfoCache``
1842
2560
 
1843
- All functionality remains identical - only the class names changed.
2561
+ All functionality remains identical; only the class names changed.
1844
2562
 
1845
2563
  Added
1846
2564
  -----
@@ -1861,7 +2579,7 @@ Added
1861
2579
  - Simplified initialization for common use case (API + MQTT)
1862
2580
  - Proper async context manager support
1863
2581
  - Reduces boilerplate in application code
1864
- - Comprehensive documentation in ``docs/guides/authentication.rst``
2582
+ - Documentation in ``docs/guides/authentication.rst``
1865
2583
  - Example: ``examples/intermediate/advanced_auth_patterns.py``
1866
2584
 
1867
2585
  - **VolumeCode Enum**: Tank capacity identification with gallon values
@@ -1896,7 +2614,7 @@ Added
1896
2614
  - ``tou_override_to_python()``: TOU override status conversion
1897
2615
  - ``div_10()``: Divide by 10.0 utility
1898
2616
  - ``enum_validator()``: Generic enum factory
1899
- - Comprehensive documentation explaining device protocol quirks
2617
+ - Documentation explaining device protocol quirks
1900
2618
  - New ``converters.py`` module replacing scattered validators
1901
2619
 
1902
2620
  - **MQTT Event System**: Structured event handling for MQTT operations
@@ -1922,7 +2640,7 @@ Added
1922
2640
  - Eliminates hardcoded units in output formatter
1923
2641
  - Single source of truth: model metadata drives CLI display
1924
2642
 
1925
- - **Comprehensive Protocol Documentation**: Complete protocol reference documentation
2643
+ - **Protocol Documentation**: Complete protocol reference documentation
1926
2644
 
1927
2645
  - New ``docs/protocol/quick_reference.rst`` with command codes, field formats, and conversions
1928
2646
  - Converted protocol documentation to RST format for Sphinx integration
@@ -1988,7 +2706,7 @@ Changed
1988
2706
  - Added factory function patterns and examples
1989
2707
  - Improved context manager documentation
1990
2708
  - Added best practices and common patterns
1991
- - More comprehensive code examples
2709
+ - More code examples
1992
2710
 
1993
2711
  - **Model Refactoring**: Updated to use new converter modules
1994
2712
 
@@ -2068,7 +2786,7 @@ Added
2068
2786
  - ``configure_reservation_water_program()``: Water program reservation management
2069
2787
  - ``set_recirculation_mode()`` / ``configure_recirculation_schedule()`` / ``trigger_recirculation_hot_button()``: Recirculation pump control and scheduling
2070
2788
 
2071
- - **CLI Documentation Updates**: Comprehensive documentation updates for subcommand-based CLI
2789
+ - **CLI Documentation Updates**: Documentation updates for subcommand-based CLI
2072
2790
  - Complete rewrite of ``docs/python_api/cli.rst`` with full command reference
2073
2791
  - Updated README.rst with new subcommand syntax and examples
2074
2792
  - Added 8+ practical usage examples (cron jobs, automation, monitoring)
@@ -2151,14 +2869,14 @@ Removed
2151
2869
  Added
2152
2870
  -----
2153
2871
 
2154
- - **Python 3.12+ Optimizations**: Leverage latest Python features
2872
+ - **Python 3.12+ Optimizations**: Use the latest Python features
2155
2873
 
2156
2874
  - PEP 695: New type parameter syntax (``def func[T](...)`` instead of ``TypeVar``)
2157
2875
  - Use ``datetime.UTC`` constant instead of ``datetime.timezone.utc``
2158
2876
  - Native union syntax (``X | Y`` instead of ``Union[X, Y]``)
2159
2877
  - Cleaner generic type annotations throughout codebase
2160
2878
 
2161
- - **Enumerations Module (``src/nwp500/enums.py``)**: Comprehensive type-safe enums for device control and status
2879
+ - **Enumerations Module (``src/nwp500/enums.py``)**: Type-safe enums for device control and status
2162
2880
 
2163
2881
  - Status value enums: ``OnOffFlag``, ``Operation``, ``DhwOperationSetting``, ``CurrentOperationMode``, ``HeatSource``, ``DREvent``, ``WaterLevel``, ``FilterChange``, ``RecirculationMode``
2164
2882
  - Time of Use enums: ``TouWeekType``, ``TouRateType``
@@ -2167,7 +2885,7 @@ Added
2167
2885
  - Error code enum: ``ErrorCode`` with complete error code mappings
2168
2886
  - Human-readable text mappings for all enums (e.g., ``DHW_OPERATION_SETTING_TEXT``, ``ERROR_CODE_TEXT``)
2169
2887
  - Exported from main package: ``from nwp500 import OnOffFlag, ErrorCode, CommandCode``
2170
- - Comprehensive documentation in ``docs/enumerations.rst``
2888
+ - Documentation in ``docs/enumerations.rst``
2171
2889
  - Example usage in ``examples/error_code_demo.py``
2172
2890
 
2173
2891
  Changed
@@ -2194,7 +2912,7 @@ Changed
2194
2912
  - CLI and other consumers benefit from automatic enum name serialization
2195
2913
  - Text mappings available for custom formatting (e.g., `DHW_OPERATION_TEXT[enum]` → "Heat Pump Only")
2196
2914
 
2197
- - **Documentation**: Comprehensive updates across protocol and API documentation
2915
+ - **Documentation**: Updates across protocol and API documentation
2198
2916
 
2199
2917
  - ``docs/guides/time_of_use.rst``: Clarified TOU override status behavior (1=OFF/override active, 2=ON/normal operation)
2200
2918
  - ``docs/protocol/data_conversions.rst``: Updated TOU field descriptions with correct enum values
@@ -2235,7 +2953,7 @@ Added
2235
2953
  - Export diagnostics to JSON for analysis and debugging
2236
2954
  - Home Assistant integration support for custom components
2237
2955
  - ``examples/mqtt_diagnostics_example.py`` demonstrating usage patterns
2238
- - Comprehensive documentation in ``docs/MQTT_DIAGNOSTICS.rst``
2956
+ - Documentation in ``docs/MQTT_DIAGNOSTICS.rst``
2239
2957
  - Exported from main package: ``from nwp500 import MqttDiagnosticsCollector``
2240
2958
 
2241
2959
  Version 6.1.0 (2025-12-03)
@@ -2323,7 +3041,7 @@ Version 6.0.6 (2025-11-24)
2323
3041
  Added
2324
3042
  -----
2325
3043
 
2326
- - **Field Descriptions**: Added comprehensive Field descriptions to ``DeviceStatus`` and ``DeviceFeature`` models with full documentation details including units, ranges, and usage context
3044
+ - **Field Descriptions**: Added Field descriptions to ``DeviceStatus`` and ``DeviceFeature`` models with full documentation details including units, ranges, and usage context
2327
3045
 
2328
3046
  Fixed
2329
3047
  -----
@@ -2341,7 +3059,7 @@ Version 6.0.5 (2025-11-21)
2341
3059
  Fixed
2342
3060
  -----
2343
3061
 
2344
- - **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:
3062
+ - **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:
2345
3063
 
2346
3064
  - ``tank_upper_temperature`` - Water tank upper sensor
2347
3065
  - ``tank_lower_temperature`` - Water tank lower sensor
@@ -2568,7 +3286,7 @@ Fixed
2568
3286
  - Applied consistent cancellation handling across all MQTT operations (connect, disconnect, subscribe, unsubscribe, publish)
2569
3287
  - AWS CRT callbacks can now complete independently without raising InvalidStateError
2570
3288
  - Added debug logging when operations are cancelled for better diagnostics
2571
- - Ensures clean shutdown without spurious exception messages
3289
+ - Clean shutdown without spurious exception messages
2572
3290
 
2573
3291
  Version 5.0.1 (2025-10-27)
2574
3292
  ==========================
@@ -2591,7 +3309,7 @@ Changed
2591
3309
  Version 5.0.0 (2025-10-27)
2592
3310
  ==========================
2593
3311
 
2594
- **BREAKING CHANGES**: This release introduces a comprehensive enterprise exception architecture.
3312
+ **BREAKING CHANGES**: This release introduces an enterprise exception architecture.
2595
3313
  See migration guide below for details on updating your code.
2596
3314
 
2597
3315
  Added
@@ -2599,7 +3317,7 @@ Added
2599
3317
 
2600
3318
  - **Enterprise Exception Architecture**: Complete exception hierarchy for better error handling
2601
3319
 
2602
- - Created ``exceptions.py`` module with comprehensive exception hierarchy
3320
+ - Created ``exceptions.py`` module with an exception hierarchy
2603
3321
  - Added ``Nwp500Error`` as base exception for all library errors
2604
3322
  - Added MQTT-specific exceptions: ``MqttError``, ``MqttConnectionError``, ``MqttNotConnectedError``,
2605
3323
  ``MqttPublishError``, ``MqttSubscriptionError``, ``MqttCredentialsError``
@@ -2608,9 +3326,9 @@ Added
2608
3326
  ``DeviceOperationError``
2609
3327
  - All exceptions now include ``error_code``, ``details``, and ``retriable`` attributes
2610
3328
  - Added ``to_dict()`` method to all exceptions for structured logging
2611
- - Added comprehensive test suite in ``tests/test_exceptions.py``
2612
- - **Added comprehensive exception handling example** (``examples/exception_handling_example.py``)
2613
- - **Updated key examples** to demonstrate new exception handling patterns
3329
+ - Added a test suite in ``tests/test_exceptions.py``
3330
+ - **Added an exception handling example** (``examples/exception_handling_example.py``)
3331
+ - **Updated the main examples** to demonstrate new exception handling patterns
2614
3332
 
2615
3333
  Changed
2616
3334
  -------
@@ -2857,7 +3575,7 @@ Fixed
2857
3575
  - Preserves AWS credential expiration timestamps during token refresh
2858
3576
  - Prevents reconnection failures when connection interrupts after AWS credentials expire but before JWT tokens expire
2859
3577
  - Resolves AWS_ERROR_HTTP_WEBSOCKET_UPGRADE_FAILURE errors during reconnection attempts
2860
- - Improved test coverage for auth module from 31% to 60% with comprehensive test suite
3578
+ - Improved test coverage for auth module from 31% to 60%
2861
3579
 
2862
3580
  Version 3.1.3 (2025-10-24)
2863
3581
  ==========================
@@ -2905,7 +3623,7 @@ Fixed
2905
3623
 
2906
3624
  - Updated callback to match latest AWS IoT SDK signature: ``(connection, error, **kwargs)``
2907
3625
  - Fixed type annotations in ``MqttConnection`` for proper type checking
2908
- - Resolves mypy type checking errors and ensures AWS SDK compatibility
3626
+ - Resolves mypy type checking errors and keeps AWS SDK compatibility
2909
3627
  - Fixed E501 line length linting issue in connection interruption handler
2910
3628
 
2911
3629
  Version 3.0.0 (Unreleased)
@@ -2949,7 +3667,7 @@ Added
2949
3667
  - Prevents accidental comparison of user preferences with real-time states
2950
3668
  - Better IDE support with more specific enum types
2951
3669
 
2952
- - **Migration Support**: Comprehensive tools for smooth migration
3670
+ - **Migration Support**: Tools for migration
2953
3671
 
2954
3672
  - ``migrate_operation_mode_usage()`` helper function with programmatic guidance
2955
3673
  - ``MIGRATION.md`` with step-by-step migration instructions
@@ -3001,12 +3719,12 @@ Version 0.2 (Unreleased)
3001
3719
  Added
3002
3720
  -----
3003
3721
 
3004
- - **Local/CI Linting Synchronization**: Complete tooling to ensure consistent linting results
3722
+ - **Local/CI Linting Synchronization**: Tooling for consistent linting results
3005
3723
 
3006
3724
  - Multiple sync methods: tox (recommended), direct scripts, pre-commit hooks, Makefile commands
3007
3725
  - CI-identical scripts: ``scripts/lint.py`` and ``scripts/format.py`` mirror ``tox -e lint`` and ``tox -e format``
3008
3726
  - Pre-commit hooks configuration for automatic checking
3009
- - Comprehensive documentation: ``LINTING_SETUP.md``, ``DEVELOPMENT.md``, ``FIX_LINTING.md``
3727
+ - Documentation: ``LINTING_SETUP.md``, ``DEVELOPMENT.md``, ``FIX_LINTING.md``
3010
3728
  - Makefile commands: ``make ci-lint``, ``make ci-format``, ``make ci-check``
3011
3729
  - Standardized ruff configuration across all environments
3012
3730
  - Eliminates "passes locally but fails in CI" issues
@@ -3057,7 +3775,7 @@ Added
3057
3775
  - Enabled by default for reliability
3058
3776
  - ``queued_commands_count`` property for monitoring
3059
3777
  - ``clear_command_queue()`` method for manual management
3060
- - Integrates seamlessly with automatic reconnection
3778
+ - Works with automatic reconnection
3061
3779
  - Example: ``command_queue_demo.py``
3062
3780
  - Documentation: ``COMMAND_QUEUE.rst``
3063
3781
 
@@ -3096,7 +3814,7 @@ Added
3096
3814
  - ``subscribe_device_feature()`` - Automatic parsing of feature messages into ``DeviceFeature`` objects
3097
3815
  - ``subscribe_energy_usage()`` - Automatic parsing of energy usage responses into ``EnergyUsageResponse`` objects
3098
3816
  - Type-safe callbacks with IDE autocomplete support
3099
- - Comprehensive error handling and logging
3817
+ - Error handling and logging
3100
3818
  - Example scripts demonstrating usage patterns
3101
3819
 
3102
3820
  - **Energy Usage API (EMS)**: Historical energy consumption data
@@ -3111,7 +3829,7 @@ Added
3111
3829
  - Energy consumption data (Watt-hours)
3112
3830
  - Efficiency percentage calculations
3113
3831
 
3114
- - **Data Models**: Comprehensive type-safe models
3832
+ - **Data Models**: Type-safe models
3115
3833
 
3116
3834
  - ``DeviceStatus`` dataclass with 125 sensor and operational fields
3117
3835
  - ``DeviceFeature`` dataclass with 46 capability and configuration fields
@@ -3150,7 +3868,7 @@ Added
3150
3868
  - Typed callbacks implementation guide
3151
3869
  - Control command reference with verified command codes
3152
3870
  - Example scripts for common use cases
3153
- - Comprehensive troubleshooting guides
3871
+ - Troubleshooting guides
3154
3872
  - Complete energy data reference (ENERGY_DATA_SUMMARY.md)
3155
3873
 
3156
3874
  - **Examples**: Production-ready example scripts
@@ -3192,14 +3910,14 @@ Changed
3192
3910
  - Removed "Optional Feature" and "not required for basic operation" phrases
3193
3911
  - Fixed table rendering in DEVICE_STATUS_FIELDS.rst
3194
3912
  - Fixed JSON syntax in code examples
3195
- - Added comprehensive reconnection documentation
3196
- - Added comprehensive command queue documentation
3913
+ - Added reconnection documentation
3914
+ - Added command queue documentation
3197
3915
  - Cleaned up backward compatibility references (new library)
3198
3916
 
3199
3917
  Fixed
3200
3918
  -----
3201
3919
 
3202
- - **Critical Bug**: Thread-safe reconnection task creation from MQTT callbacks
3920
+ - **Bug**: Thread-safe reconnection task creation from MQTT callbacks
3203
3921
 
3204
3922
  - Fixed ``RuntimeError: no running event loop`` when connection is interrupted
3205
3923
  - Fixed ``RuntimeWarning: coroutine '_reconnect_with_backoff' was never awaited``
@@ -3207,9 +3925,9 @@ Fixed
3207
3925
  - Implemented ``_start_reconnect_task()`` helper method to properly create reconnection tasks
3208
3926
  - Uses existing ``_schedule_coroutine()`` method for thread-safe task scheduling
3209
3927
  - Prevents crashes during automatic reconnection after connection interruptions
3210
- - Ensures reconnection tasks are properly awaited and executed
3928
+ - Reconnection tasks are awaited and executed
3211
3929
 
3212
- - **Critical Bug**: Thread-safe event emission from MQTT callbacks
3930
+ - **Bug**: Thread-safe event emission from MQTT callbacks
3213
3931
 
3214
3932
  - Fixed ``RuntimeError: no running event loop in thread 'Dummy-1'``
3215
3933
  - MQTT callbacks run in separate threads created by AWS IoT SDK
@@ -3234,7 +3952,7 @@ Fixed
3234
3952
  - Updated built HTML documentation files
3235
3953
  - Protects privacy in public documentation
3236
3954
 
3237
- - **Critical Bug**: Device control command codes
3955
+ - **Bug**: Device control command codes
3238
3956
 
3239
3957
  - Fixed incorrect command code usage causing unintended power-off
3240
3958
  - Power-off now uses command code ``33554433``
@@ -3242,7 +3960,7 @@ Fixed
3242
3960
  - DHW mode control now uses command code ``33554437``
3243
3961
  - Discovered through network traffic analysis of official app
3244
3962
 
3245
- - **Critical Bug**: MQTT topic pattern matching with wildcards
3963
+ - **Bug**: MQTT topic pattern matching with wildcards
3246
3964
 
3247
3965
  - Fixed ``_topic_matches_pattern()`` to correctly handle ``#`` wildcard
3248
3966
  - Topics now match when message arrives on base topic (e.g., ``cmd/52/device/res``)
@@ -3262,9 +3980,9 @@ Fixed
3262
3980
  - Examples now subscribe to broader topic patterns
3263
3981
  - Subscribe to ``cmd/{device_type}/{device_topic}/#`` to catch all command messages
3264
3982
  - Subscribe to ``evt/{device_type}/{device_topic}/#`` to catch all event messages
3265
- - Ensures all device responses are received
3983
+ - All device responses are received
3266
3984
 
3267
- - **Enhancement**: Robust enum conversion with fallbacks
3985
+ - **Enhancement**: Enum conversion with fallbacks
3268
3986
 
3269
3987
  - Added try/except blocks for all enum conversions in ``DeviceStatus.from_dict()``
3270
3988
  - Added try/except blocks for all enum conversions in ``DeviceFeature.from_dict()``