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

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