nwp500-python 9.3.2__tar.gz → 9.4.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 (230) hide show
  1. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/CHANGELOG.rst +159 -0
  2. {nwp500_python-9.3.2/src/nwp500_python.egg-info → nwp500_python-9.4.0}/PKG-INFO +1 -1
  3. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/explanation/index.rst +1 -0
  4. nwp500_python-9.4.0/docs/explanation/tou-recovery-cap.rst +188 -0
  5. nwp500_python-9.4.0/docs/how-to/maintenance.rst +139 -0
  6. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/how-to/schedule-operation.rst +36 -69
  7. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/how-to/track-energy.rst +43 -15
  8. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/protocol/data_conversions.rst +2 -2
  9. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/protocol/device_status.rst +2 -2
  10. nwp500_python-9.4.0/docs/reference/protocol/mqtt_protocol.rst +1230 -0
  11. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/protocol/quick_reference.rst +61 -29
  12. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/python_api/cli.rst +133 -24
  13. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/python_api/events.rst +3 -1
  14. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/python_api/models.rst +93 -57
  15. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/python_api/mqtt_client.rst +174 -66
  16. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/error_code_demo.py +3 -1
  17. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/__init__.py +12 -4
  18. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/cli/__main__.py +205 -16
  19. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/cli/handlers.py +283 -1
  20. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/cli/output_formatters.py +62 -0
  21. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/cli/presentation.py +251 -8
  22. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/cli/rich_output.py +55 -10
  23. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/converters.py +1 -1
  24. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/enums.py +40 -16
  25. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/models/__init__.py +13 -4
  26. nwp500_python-9.4.0/src/nwp500/models/diagnostics.py +147 -0
  27. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/models/energy.py +35 -3
  28. nwp500_python-9.4.0/src/nwp500/models/firmware.py +47 -0
  29. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/models/schedule.py +105 -157
  30. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/models/status.py +13 -1
  31. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/_control_commands.py +42 -33
  32. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/_device_subscriptions.py +105 -17
  33. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/client.py +76 -3
  34. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/control.py +435 -157
  35. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/subscriptions.py +327 -157
  36. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/utils.py +9 -0
  37. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/topic_builder.py +49 -6
  38. {nwp500_python-9.3.2 → nwp500_python-9.4.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  39. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/SOURCES.txt +4 -0
  40. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/scm_file_list.json +4 -0
  41. nwp500_python-9.4.0/src/nwp500_python.egg-info/scm_version.json +8 -0
  42. nwp500_python-9.4.0/tests/test_app_queries.py +901 -0
  43. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_cli_commands.py +434 -1
  44. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_protocol_correctness.py +353 -132
  45. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_public_api.py +59 -0
  46. nwp500_python-9.3.2/docs/how-to/maintenance.rst +0 -119
  47. nwp500_python-9.3.2/docs/reference/protocol/mqtt_protocol.rst +0 -929
  48. nwp500_python-9.3.2/src/nwp500_python.egg-info/scm_version.json +0 -8
  49. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/.coveragerc +0 -0
  50. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  51. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/.github/copilot-instructions.md +0 -0
  52. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/.github/workflows/ci.yml +0 -0
  53. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/.github/workflows/release.yml +0 -0
  54. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/.gitignore +0 -0
  55. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/.pre-commit-config.yaml +0 -0
  56. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/.readthedocs.yml +0 -0
  57. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/AUTHORS.rst +0 -0
  58. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/CONTRIBUTING.rst +0 -0
  59. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/LICENSE.txt +0 -0
  60. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/Makefile +0 -0
  61. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/README.rst +0 -0
  62. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/RELEASE.md +0 -0
  63. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/Makefile +0 -0
  64. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/_static/.gitignore +0 -0
  65. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/conf.py +0 -0
  66. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/explanation/advanced-features.rst +0 -0
  67. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/explanation/architecture.rst +0 -0
  68. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/explanation/tank-energy.rst +0 -0
  69. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/explanation/unknown-values.rst +0 -0
  70. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/how-to/authenticate.rst +0 -0
  71. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/how-to/auto-recovery.rst +0 -0
  72. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/how-to/diagnose-mqtt.rst +0 -0
  73. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/how-to/home-assistant.rst +0 -0
  74. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/how-to/index.rst +0 -0
  75. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/how-to/manage-units.rst +0 -0
  76. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/how-to/monitor-status.rst +0 -0
  77. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/how-to/optimize-tou.rst +0 -0
  78. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/how-to/queue-commands.rst +0 -0
  79. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/index.rst +0 -0
  80. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/openapi.yaml +0 -0
  81. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/project/authors.rst +0 -0
  82. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/project/changelog.rst +0 -0
  83. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/project/contributing.rst +0 -0
  84. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/project/history.rst +0 -0
  85. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/project/license.rst +0 -0
  86. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/configuration.rst +0 -0
  87. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/enumerations.rst +0 -0
  88. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/index.rst +0 -0
  89. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/installation.rst +0 -0
  90. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/protocol/device_features.rst +0 -0
  91. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/protocol/error_codes.rst +0 -0
  92. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/protocol/rest_api.rst +0 -0
  93. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/python_api/api_client.rst +0 -0
  94. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/python_api/auth_client.rst +0 -0
  95. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/reference/python_api/exceptions.rst +0 -0
  96. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/requirements.txt +0 -0
  97. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/docs/tutorials/getting-started.rst +0 -0
  98. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/.ruff.toml +0 -0
  99. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/README.md +0 -0
  100. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/air_filter_reset.py +0 -0
  101. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/anti_legionella.py +0 -0
  102. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/auto_recovery.py +0 -0
  103. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/combined_callbacks.py +0 -0
  104. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/demand_response.py +0 -0
  105. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/device_capabilities.py +0 -0
  106. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/device_status_debug.py +0 -0
  107. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/energy_analytics.py +0 -0
  108. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/firmware_payload_capture.py +0 -0
  109. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/mqtt_diagnostics.py +0 -0
  110. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/power_control.py +0 -0
  111. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/recirculation_control.py +0 -0
  112. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/reconnection_demo.py +0 -0
  113. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/reservation_schedule.py +0 -0
  114. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/simple_auto_recovery.py +0 -0
  115. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/token_restoration.py +0 -0
  116. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/tou_openei.py +0 -0
  117. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/tou_schedule.py +0 -0
  118. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/advanced/water_reservation.py +0 -0
  119. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/beginner/01_authentication.py +0 -0
  120. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/beginner/02_list_devices.py +0 -0
  121. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/beginner/03_get_status.py +0 -0
  122. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/beginner/04_set_temperature.py +0 -0
  123. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
  124. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/intermediate/command_queue.py +0 -0
  125. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/intermediate/device_status_callback.py +0 -0
  126. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/intermediate/error_handling.py +0 -0
  127. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/intermediate/event_driven_control.py +0 -0
  128. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/intermediate/improved_auth.py +0 -0
  129. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
  130. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  131. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/intermediate/periodic_requests.py +0 -0
  132. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/intermediate/set_mode.py +0 -0
  133. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/intermediate/vacation_mode.py +0 -0
  134. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/mask.py +0 -0
  135. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/testing/periodic_device_info.py +0 -0
  136. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/testing/simple_periodic_info.py +0 -0
  137. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/testing/test_api_client.py +0 -0
  138. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/testing/test_mqtt_connection.py +0 -0
  139. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/testing/test_mqtt_messaging.py +0 -0
  140. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/examples/testing/test_periodic_minimal.py +0 -0
  141. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/pyproject.toml +0 -0
  142. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/scripts/README.md +0 -0
  143. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/scripts/bump_version.py +0 -0
  144. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/scripts/diagnose_mqtt_connection.py +0 -0
  145. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/scripts/extract_changelog.py +0 -0
  146. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/scripts/format.py +0 -0
  147. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/scripts/lint.py +0 -0
  148. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/scripts/setup-dev.py +0 -0
  149. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/scripts/validate_version.py +0 -0
  150. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/setup.cfg +0 -0
  151. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/setup.py +0 -0
  152. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/_base.py +0 -0
  153. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/api_client.py +0 -0
  154. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/auth.py +0 -0
  155. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/cli/__init__.py +0 -0
  156. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/cli/monitoring.py +0 -0
  157. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/cli/token_storage.py +0 -0
  158. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/command_decorators.py +0 -0
  159. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/config.py +0 -0
  160. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/device_capabilities.py +0 -0
  161. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/device_info_cache.py +0 -0
  162. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/encoding.py +0 -0
  163. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/events.py +0 -0
  164. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/exceptions.py +0 -0
  165. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/factory.py +0 -0
  166. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/field_factory.py +0 -0
  167. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/models/_converters.py +0 -0
  168. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/models/device.py +0 -0
  169. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/models/feature.py +0 -0
  170. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/models/mqtt_models.py +0 -0
  171. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/models/tou.py +0 -0
  172. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/__init__.py +0 -0
  173. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/command_queue.py +0 -0
  174. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/connection.py +0 -0
  175. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/diagnostics.py +0 -0
  176. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/periodic.py +0 -0
  177. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/reconnection.py +0 -0
  178. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/state_tracker.py +0 -0
  179. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt/types.py +0 -0
  180. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/mqtt_events.py +0 -0
  181. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/openei.py +0 -0
  182. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/py.typed +0 -0
  183. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/reservations.py +0 -0
  184. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/temperature.py +0 -0
  185. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/tou_schedule.py +0 -0
  186. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/unit_system.py +0 -0
  187. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500/utils.py +0 -0
  188. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  189. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  190. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  191. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/requires.txt +0 -0
  192. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
  193. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/conftest.py +0 -0
  194. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_api_helpers.py +0 -0
  195. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_auth.py +0 -0
  196. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_auth_session_lifecycle.py +0 -0
  197. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_bug_fixes.py +0 -0
  198. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_canonical_schedule.py +0 -0
  199. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_cli_basic.py +0 -0
  200. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_cli_tristate_presentation.py +0 -0
  201. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_command_decorators.py +0 -0
  202. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_command_queue.py +0 -0
  203. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_device_capabilities.py +0 -0
  204. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_device_info_cache.py +0 -0
  205. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_device_rest_models.py +0 -0
  206. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_events.py +0 -0
  207. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_exceptions.py +0 -0
  208. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_model_converters.py +0 -0
  209. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_models.py +0 -0
  210. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_mqtt_clean_session_resume.py +0 -0
  211. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_mqtt_client_init.py +0 -0
  212. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_mqtt_events.py +0 -0
  213. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_mqtt_hypothesis.py +0 -0
  214. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_mqtt_reconnection.py +0 -0
  215. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_mqtt_reconnection_storm.py +0 -0
  216. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_mqtt_reliability.py +0 -0
  217. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_multi_device.py +0 -0
  218. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_openei.py +0 -0
  219. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_reservations.py +0 -0
  220. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_temperature_converters.py +0 -0
  221. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_threading_model.py +0 -0
  222. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_token_storage.py +0 -0
  223. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_tou_api.py +0 -0
  224. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_tou_schedule.py +0 -0
  225. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_unit_switching.py +0 -0
  226. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_unit_system_process_wide.py +0 -0
  227. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_utility_modules.py +0 -0
  228. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tests/test_utils.py +0 -0
  229. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/tox.ini +0 -0
  230. {nwp500_python-9.3.2 → nwp500_python-9.4.0}/uv.lock +0 -0
@@ -5,6 +5,164 @@ Changelog
5
5
  Unreleased
6
6
  ==========
7
7
 
8
+ Version 9.4.0 (2026-09-12)
9
+ ==========================
10
+
11
+ This release realigns the MQTT command set with what the NaviLink app
12
+ actually sends. The app (version 2.03.00) was decompiled and its request
13
+ builder compared with the library; every query it sends now has a
14
+ method, six commands it never sends were removed, and the recirculation
15
+ schedule write was corrected. Breaking changes are listed under
16
+ *Removed* and *Changed*.
17
+
18
+ Added
19
+ -----
20
+ - **Installer diagnostics query.** ``request_diagnostics()`` /
21
+ ``subscribe_diagnostics()`` and the ``nwp-cli diagnostics`` command
22
+ return the lifetime counters the app's installer screen shows: heat
23
+ pump and element energy, days since installation, fault event counts,
24
+ demand-response operation times, hot-water draw statistics and
25
+ component run times and start counts, as
26
+ :class:`~nwp500.models.DeviceDiagnostics`. The device answers the
27
+ ``st/td/rd`` query with packed hex; the NaviLink cloud decodes it into
28
+ JSON only when the reply topic has the app's five-segment form, so the
29
+ library requests that form. Verified live: the energies and compressor
30
+ and element run times match the energy query's lifetime totals.
31
+ - **Monthly energy query.** ``request_energy_usage_monthly()`` /
32
+ ``subscribe_energy_usage_monthly()`` and ``nwp-cli energy --years``
33
+ return one entry per year with twelve per-month items. Verified live.
34
+ - **Hourly energy query.** ``request_energy_usage_hourly()`` /
35
+ ``subscribe_energy_usage_hourly()`` send the app's per-hour query. The
36
+ NWP500 tested never answered it in four attempts, so it has no CLI
37
+ command.
38
+ - **Recirculation schedule read.** ``request_recirculation_schedule()``
39
+ and ``nwp-cli recirc-schedule get``; the write is exposed as
40
+ ``nwp-cli recirc-schedule set``, which checks the JSON before
41
+ connecting (known keys only, integers in range, at most 20 entries).
42
+ The read was verified live on a unit that reports recirculation
43
+ scheduling but has no pump (empty, disabled schedule); the write and
44
+ its echo are unverified. The CLI reports a write as updated only when
45
+ the echo matches what was written. ``RecirculationSchedule`` and
46
+ ``RecirculationScheduleEntry`` fields are strict integers, so ``True``
47
+ or ``6.0`` is rejected instead of coerced.
48
+ - **Firmware download info.** ``request_firmware_download_info()`` /
49
+ ``subscribe_firmware_download_info()`` and ``nwp-cli firmware info``
50
+ return :class:`~nwp500.models.FirmwareDownloadInfo`. The device replies
51
+ on its own topic rather than a client-keyed one. Verified live.
52
+ - **Session end.** ``end_session()`` sends the ``st/end`` query the app
53
+ sends whenever it leaves a device. No reply to it has been observed.
54
+ Unlike other commands it is never queued while disconnected (it raises
55
+ ``MqttNotConnectedError``), since a session end replayed after a
56
+ reconnect would end the new session.
57
+ - ``MqttConnectionConfig.send_session_end_on_disconnect`` (default
58
+ ``True``) controls whether ``disconnect()`` sends it.
59
+ - ``subscribe_firmware_commit_response()`` receives the reply to
60
+ ``commit_firmware_update()``, which is requested on the app-form
61
+ ``res/commit-ota`` topic that no other subscription covers.
62
+ - **Controls with app builder cases that had no method:**
63
+ ``set_vacation_duration(days)`` (``goout-day``; the app has a builder
64
+ case but no screen that calls it; verified live to change the day count
65
+ without changing the operation mode),
66
+ ``set_air_filter_life(hours)`` (``air-filter-life``; the wire value is
67
+ hours divided by 500, matching the app's 0 or 1000-10000 picker;
68
+ verified live) and ``reset_condenser_fault()`` (``cond-fault-reset``,
69
+ installer-level in the app; acknowledged live, clearing effect not
70
+ observable without a fault). CLI: ``vacation-duration``,
71
+ ``filter-life``, ``reset-condenser-fault``.
72
+ - **Command codes** for every member of the app's ``DeviceControlMGPP``
73
+ enum, with the declared-but-never-sent ones annotated.
74
+ - ``EnergyUsageResponse.type_of_usage``, ``get_year_data()``,
75
+ ``MonthlyEnergyData.day`` and ``EnergyUsageDay.ep_usage`` /
76
+ ``water_usage`` (declared by the app; not reported by the NWP500).
77
+
78
+ Changed
79
+ -------
80
+ - **Recirculation schedule models take the app's entry shape** (breaking).
81
+ :class:`~nwp500.models.RecirculationScheduleEntry` is now
82
+ ``enable``/``week``/``hour``/``min``/``mode``/``param`` like a
83
+ reservation entry (``mode`` is the entry's pump on/off toggle in the
84
+ app's editor) and :class:`~nwp500.models.RecirculationSchedule` carries
85
+ ``reservation_use`` plus ``reservation`` instead of ``schedule``. The
86
+ previous start/end window fields were not what the device accepts.
87
+ - ``MonthlyEnergyData.month`` is optional: monthly-query entries have no
88
+ month.
89
+ - **``NavienMqttClient.disconnect()`` now sends ``st/end``** to every
90
+ device that was passed to any subscription, as the NaviLink app does
91
+ when it leaves a device. The publishes go straight to the connection,
92
+ never into the offline command queue, stop if the connection drops,
93
+ are bounded by a short timeout and never fail the disconnect. What it
94
+ does to other clients connected to the same device (for example a Home
95
+ Assistant integration) is not known; turn it off with
96
+ ``MqttConnectionConfig(send_session_end_on_disconnect=False)``.
97
+ - **CLI energy summaries total the requested periods.** ``energy
98
+ --month``, ``--months`` and ``--years`` printed the device's lifetime
99
+ total under a per-month or per-year heading. Each summary now covers
100
+ only the rows shown, and the lifetime total is printed once under its
101
+ own heading. (The response's ``total`` is lifetime whatever is
102
+ requested; checked live.)
103
+ - **Parameter validation.** ``set_air_filter_life()``,
104
+ ``set_vacation_duration()`` and the daily, monthly and hourly energy
105
+ queries reject non-integers (including ``True`` and floats) and
106
+ out-of-range values; years must be 2000-2099 and ``--years`` drops
107
+ duplicates. ``filter-life``, ``--year`` and ``--years`` reject bad
108
+ input before connecting. ``request_energy_usage()`` now rejects an
109
+ empty month list.
110
+
111
+ Fixed
112
+ -----
113
+ - **``configure_recirculation_schedule()`` published to the wrong topic
114
+ with the wrong keys.** It sent ``{"schedule": [...]}`` on the bare
115
+ ``ctrl`` topic; the app publishes RECIR_RESERVATION on
116
+ ``ctrl/recirc-rsv/rd`` with ``reservationUse`` and ``reservation`` and
117
+ asks for the echo on ``recirc-rsv/rd``.
118
+ - **``commit_firmware_update()`` published to the bare ``ctrl`` topic.**
119
+ The app publishes OTA_COMMIT on ``ctrl/commit-ota`` and asks for the
120
+ reply on ``res/commit-ota``; the library now does the same. Not
121
+ exercised against a device.
122
+ - **Unsubscribing could remove the wrong handler.** When one callback was
123
+ registered for more than one device-wildcard subscription (status,
124
+ feature, firmware info), unsubscribing one removed whichever handler
125
+ came first. Handlers are now matched on subscription kind as well.
126
+ - **Query replies could reach another device's callback.** Client-keyed
127
+ reply topics are shared by every device a client queries, so with two
128
+ devices subscribed a reservation, energy, TOU, diagnostics or
129
+ recirculation reply reached both callbacks. Typed subscriptions now
130
+ ignore replies whose ``macAddress`` names a different device.
131
+ - Recirculation read-backs in hex carry the unused ``param`` as ``0xFF``
132
+ and in JSON as ``-1``; the model normalizes to ``-1`` so the two compare
133
+ equal.
134
+ - **Protocol documentation** rewritten from the decompiled request
135
+ builder and live captures: real topic structure, the anti-legionella
136
+ modes (``anti-leg-on [days]`` / ``anti-leg-off``, not
137
+ ``anti-legionella-setting``), the message example that paired the
138
+ weekly-reservation code with ``dhw-temperature``, the undocumented
139
+ 16777226 reservation write, response shapes for every query, and the
140
+ quick reference's invented ``header``/``body`` format with 0x11/0x21/0x31
141
+ codes that never existed. Stale ``docs/protocol/`` paths in code
142
+ comments and docstrings now point at ``docs/reference/protocol/``.
143
+ - ``models/schedule.py`` docstrings cited 33554444 (the hot button) for
144
+ the recirculation reservation; it is 33554440.
145
+ - CLI reference: token cache path is ``~/.nwp500_tokens.json``, ``-vv``
146
+ is a repeated ``-v`` (there was no ``--very-verbose``), and the
147
+ ``--unit-system`` option and ``tou rates``/``plan``/``apply`` commands
148
+ are documented.
149
+
150
+ Removed
151
+ -------
152
+ - **Six MQTT commands the NaviLink app never sends** (breaking):
153
+ ``check_firmware_update()``, ``reconnect_wifi()``, ``reset_wifi()``,
154
+ ``set_freeze_protection_temperature()``, ``run_smart_diagnostic()`` and
155
+ ``update_weekly_reservation()`` with
156
+ ``subscribe_weekly_reservation_response()`` and the
157
+ ``WeeklyReservationEntry`` / ``WeeklyReservationSchedule`` models. The
158
+ app's enum declares the first five codes but its request builder has
159
+ no case for them, so the library's ``ota-check``, ``wifi-reconnect``,
160
+ ``wifi-reset``, ``frez-temp`` and ``smart-diagnostic`` mode strings were
161
+ invented. The app's weekly schedule is the ordinary reservation
162
+ program: it writes with 16777226 on ``ctrl/rsv/rd``
163
+ (``update_reservations()``), never with 33554438 or a ``rsv-weekly/rd``
164
+ topic. The ``CommandCode`` members stay, annotated as declared only.
165
+
8
166
  Version 9.3.2 (2026-09-01)
9
167
  ==========================
10
168
 
@@ -129,6 +287,7 @@ Fixed
129
287
  other clients on the same device. Query results the device routes back to a
130
288
  client-keyed topic, such as reservations and energy usage, were already
131
289
  covered by the existing response wildcard.
290
+
132
291
  Version 9.3.0 (2026-08-03)
133
292
  ==========================
134
293
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.3.2
3
+ Version: 9.4.0
4
4
  Summary: A library for controlling Navien NWP500 Water Heaters via NaviLink
5
5
  Home-page: https://github.com/eman/nwp500-python
6
6
  Author: Emmanuel Levijarvi
@@ -10,4 +10,5 @@ Understanding-oriented deep dives into the library's design and advanced feature
10
10
  advanced-features
11
11
  architecture
12
12
  tank-energy
13
+ tou-recovery-cap
13
14
  unknown-values
@@ -0,0 +1,188 @@
1
+ ==========================
2
+ The TOU Recovery Cap
3
+ ==========================
4
+
5
+ When a Time-of-Use schedule is active, the NWP500 stops a heat-pump
6
+ recovery at about **90 % charge** instead of running to the setpoint. The
7
+ tank finishes roughly 1.9 degC below ``hp_lower_off_temp_setting`` and
8
+ stays there. Nothing in the protocol announces this, and no error is
9
+ raised - the compressor simply stops early.
10
+
11
+ This matters for anything that waits for the tank to reach its setpoint:
12
+ under an active TOU window that instant never arrives.
13
+
14
+ .. contents::
15
+ :local:
16
+ :depth: 2
17
+
18
+
19
+ The short version
20
+ =================
21
+
22
+ .. list-table::
23
+ :header-rows: 1
24
+ :widths: 30 70
25
+
26
+ * - Observation
27
+ - Value
28
+ * - Charge at termination
29
+ - ``dhw_charge_per`` = 90 % (p10 89, p90 91). Never reaches 100.
30
+ * - Shortfall below setpoint
31
+ - ``tank_lower_temperature`` finishes 1.9 degC (3.42 degF) below
32
+ ``hp_lower_off_temp_setting``
33
+ * - Precondition
34
+ - An expensive TOU period in force, un-overridden. ``tou_status``
35
+ alone is **not** the test - see below.
36
+ * - Frequency
37
+ - 59 of 333 recoveries (18 %) over 260 days
38
+
39
+ A normal recovery, for contrast, ends with ``dhw_charge_per`` at 100 %
40
+ (p10 99) and the lower probe at or above the setpoint.
41
+
42
+
43
+ Evidence
44
+ ========
45
+
46
+ Measured across 333 heat-pump recovery cycles of 15-480 minutes, from
47
+ 2025-12-28 to 2026-09-06, on one unit. 196 reached the lower-off
48
+ setpoint; 137 did not.
49
+
50
+ The shortfall of the 137 is **bimodal**, not a spread:
51
+
52
+ .. code:: text
53
+
54
+ shortfall below hp_lower_off_temp_setting, cycles that never reached it
55
+
56
+ min 0.18 p10 1.66 p25 3.42 median 3.42 p75 4.32
57
+ p90 31.03 max 58.86 (degF)
58
+
59
+ most common values: 3.42 degF x 59 cycles 3.24 x 6 3.60 x 6
60
+
61
+ The spike at 3.42 degF is 1.9 degC exactly, and 19 steps of the 0.1 degC
62
+ tank-probe quantum. It holds across eight different setpoints from 140.0
63
+ to 147.2 degF and appears in all nine months observed, so it is an
64
+ offset in the control logic rather than a proportional effect or a
65
+ seasonal one. The long tail beyond 8 degF is ordinary interruption -
66
+ a draw starting mid-recovery, a mode change - and is unrelated.
67
+
68
+ ``tou_status`` looks like a clean separator, and is not one:
69
+
70
+ .. list-table::
71
+ :header-rows: 1
72
+ :widths: 34 14 26 26
73
+
74
+ * - Population
75
+ - n
76
+ - ``tou_status`` True
77
+ - ``tou_override_status`` True
78
+ * - Stopped 3.42 degF short
79
+ - 59
80
+ - **59 / 59 (100 %)**
81
+ - 0 / 59
82
+ * - Other unreached
83
+ - 78
84
+ - 49 / 78 (63 %)
85
+ - 6 / 78
86
+ * - Reached setpoint
87
+ - 196
88
+ - 98 / 196 (50 %)
89
+ - 27 / 196
90
+
91
+ All 59 ran in ``HEAT_PUMP`` mode throughout, so this is not
92
+ ``ENERGY_SAVER`` or ``VACATION`` behaviour.
93
+
94
+ But read that table carefully: 98 cycles reached the setpoint with
95
+ ``tou_status`` True. **The flag only reports that TOU scheduling is
96
+ enabled.** It says nothing about whether the recovery ran inside an
97
+ expensive period, and the cap bites only when it did - outside one, an
98
+ enabled schedule does nothing at all.
99
+
100
+ The device marks a period itself. While inside one it applies non-zero
101
+ ``*_diff_temp_setting`` offsets and reverts them at the end; on the unit
102
+ measured this toggles at 21:00 and 04:00 UTC daily. Using that as the
103
+ in-period test, among ``tou_status`` True cycles:
104
+
105
+ .. list-table::
106
+ :header-rows: 1
107
+ :widths: 34 22 22 22
108
+
109
+ * - At cycle end
110
+ - Capped
111
+ - Reached
112
+ - Other
113
+ * - Inside a period
114
+ - **42 (62 %)**
115
+ - 12
116
+ - 14
117
+ * - Outside a period
118
+ - 16 (12 %)
119
+ - 86
120
+ - 34
121
+
122
+ Test it at the moment the cycle *ended*, not when it began: a recovery
123
+ that starts off-peak and runs into a period is still capped, and testing
124
+ the start catches 17 of 59 against 43 testing the end.
125
+
126
+ Two plausible explanations were tested and **refuted**:
127
+
128
+ - *The upper zone satisfied its own cut-out first.* No -
129
+ ``tank_upper_temperature`` reached ``hp_upper_off_temp_setting`` on
130
+ only 2 of 137 unreached cycles, against 179 of 196 reached ones. The
131
+ upper probe finishes short too, by a median of 4.5 degF.
132
+ - *The device terminates on outlet temperature.* No -
133
+ ``dhw_outlet_temperature`` reached the setpoint on only 2 of 136.
134
+
135
+
136
+ What is not yet established
137
+ ===========================
138
+
139
+ Being inside a period is a strong predictor but not a deterministic one:
140
+ 12 cycles reached the setpoint from inside one, and 16 were capped from
141
+ outside. Some of that is the marker's own resolution - it is sampled at
142
+ the cycle end, and a period boundary crossed mid-recovery is not captured
143
+ - but it has not been reconciled against the schedule the device was
144
+ actually holding, which ``configure_tou_schedule_confirmed`` can read
145
+ back.
146
+
147
+ The 90 % figure has been observed on one unit under one TOU schedule. It
148
+ is not known whether the ceiling is fixed in firmware, derived from the
149
+ schedule, or configurable.
150
+
151
+ A note on reading ``tou_status`` over time: the field emits brief
152
+ ``unknown`` values on integration reconnect - sub-second blips that
153
+ return immediately to the value they interrupted - so a raw state history
154
+ contains entries that are not transitions. Stores that keep only boolean
155
+ values drop these, which is the right behaviour and leaves a faithful
156
+ record of the genuine changes.
157
+
158
+ Do not read a low record count as lost data. It means the state was
159
+ *held*: on the unit measured, ``tou_status`` logged 165 changes in one
160
+ month and 5 in another, and the sparse month was the one in which TOU was
161
+ active nearly continuously. Forward-filling the genuine transitions is
162
+ correct. It can be corroborated against the ``*_diff_temp_setting``
163
+ fields, which toggle away from zero at the start of each TOU window and
164
+ back at the end.
165
+
166
+
167
+ Working with the cap
168
+ ====================
169
+
170
+ **Do not wait for the setpoint.** Code that treats "tank reached
171
+ ``hp_lower_off_temp_setting``" as the completion signal will block
172
+ indefinitely on a capped recovery. Watch ``dhw_charge_per`` plateauing,
173
+ or the compressor stopping, instead.
174
+
175
+ **Do not read a capped cycle as a fault or as degraded capacity.** The
176
+ appliance is doing what the schedule told it to. A recovery that ends at
177
+ 90 % charge with ``tou_status`` True is a normal outcome.
178
+
179
+ **When measuring recovery duration, treat capped cycles separately.**
180
+ They are not censored observations of a full recovery - they are
181
+ complete observations of a different, shorter target. Pooling them with
182
+ uncapped recoveries biases any duration estimate downward; discarding
183
+ them biases it upward, because capped cycles are systematically shorter
184
+ (median 161 minutes against 235 for cycles that ran to setpoint).
185
+
186
+ **To force a full recovery,** set ``tou_override_status`` - none of the
187
+ 59 capped cycles had it set, and 27 of the cycles that ran to setpoint
188
+ under an active TOU schedule did.
@@ -0,0 +1,139 @@
1
+ ==================
2
+ Device Maintenance
3
+ ==================
4
+
5
+ Maintenance commands cover firmware update information, the installer
6
+ diagnostics counters, air filter service intervals and fault resets. All
7
+ of them mirror what the NaviLink app sends; the CLI equivalents are noted
8
+ with each one.
9
+
10
+ .. contents:: On This Page
11
+ :local:
12
+ :depth: 2
13
+
14
+ Before You Start
15
+ ================
16
+
17
+ Some maintenance operations are gated on device capabilities. Request
18
+ device features first so the client can inspect capability flags.
19
+
20
+ .. code-block:: python
21
+
22
+ await mqtt.subscribe_device_feature(device, lambda feature: print(feature))
23
+ await mqtt.request_device_info(device)
24
+
25
+ Installer Diagnostics
26
+ =====================
27
+
28
+ The NaviLink app's installer screen shows lifetime counters the device
29
+ keeps: heat pump and element energy, days since installation, fault
30
+ event counts, demand-response operation times, hot-water draw statistics
31
+ and component run times and start counts. The gate is in the app, so a
32
+ consumer account's unit answers the query.
33
+
34
+ .. code-block:: python
35
+
36
+ from nwp500 import DeviceDiagnostics
37
+
38
+ def on_diagnostics(diag: DeviceDiagnostics) -> None:
39
+ ts, ta = diag.ts_data, diag.ta_data
40
+ print(f"Installed {ts.days_since_installation} days ago")
41
+ print(f"Heat pump: {ts.cumulated_pwr_hp} Wh over "
42
+ f"{ta.cumulated_op_time_comp} h, {ta.cumulated_op_num_comp} starts")
43
+ print(f"Condensate overflows: {ts.cumulated_occ_num_con_ovr_flow}")
44
+
45
+ await mqtt.subscribe_diagnostics(device, on_diagnostics)
46
+ await mqtt.request_diagnostics(device)
47
+
48
+ The two energies are watt-hours and ``cumulated_op_time_*`` values are
49
+ hours (both cross-checked against the energy query); the remaining
50
+ counters have no documented unit. See
51
+ :class:`~nwp500.models.DeviceDiagnostics` for every field.
52
+
53
+ CLI: ``nwp-cli diagnostics`` (add ``--json`` for the raw model).
54
+
55
+ Firmware Download Information
56
+ =============================
57
+
58
+ The device reports which firmware components it has downloaded for an
59
+ over-the-air update. A unit with nothing pending reports one all-zero
60
+ entry.
61
+
62
+ .. code-block:: python
63
+
64
+ from nwp500 import FirmwareDownloadInfo
65
+
66
+ def on_firmware(info: FirmwareDownloadInfo) -> None:
67
+ for entry in info.download_sw_info:
68
+ print(entry.component_name, entry.sw_version, entry.status)
69
+
70
+ await mqtt.subscribe_firmware_download_info(device, on_firmware)
71
+ await mqtt.request_firmware_download_info(device)
72
+
73
+ To apply a downloaded update, call
74
+ :meth:`nwp500.mqtt.client.NavienMqttClient.commit_firmware_update` with an
75
+ :class:`~nwp500.models.OtaCommitPayload` naming the component code and
76
+ version from the entry above.
77
+
78
+ .. warning::
79
+
80
+ Committing firmware reboots the device. Heating and MQTT connectivity
81
+ will be interrupted until the upgrade completes.
82
+
83
+ .. code-block:: python
84
+
85
+ from nwp500 import OtaCommitPayload
86
+
87
+ payload = OtaCommitPayload(swCode=1, swVersion=1234)
88
+ await mqtt.commit_firmware_update(device, payload)
89
+
90
+ CLI: ``nwp-cli firmware info``.
91
+
92
+ Air Filter
93
+ ==========
94
+
95
+ The air filter timer is reset after cleaning or replacing the filter. The
96
+ service interval is set in evaporator-fan hours: ``0`` disables the alarm,
97
+ otherwise 1000 to 10000 hours in 500-hour steps, which are the values the
98
+ NaviLink app offers. The device reports the interval back as
99
+ ``DeviceStatus.air_filter_alarm_period``.
100
+
101
+ .. code-block:: python
102
+
103
+ await mqtt.set_air_filter_life(device, 3000)
104
+ await mqtt.reset_air_filter(device)
105
+
106
+ CLI: ``nwp-cli filter-life 3000`` and ``nwp-cli reset-filter``.
107
+
108
+ Condenser Fault Reset
109
+ =====================
110
+
111
+ Clears a condenser fault. The NaviLink app exposes this on its status
112
+ screen to installer accounts only; the gate is in the app. A consumer
113
+ account's unit acknowledges the command; the clearing effect could not be
114
+ observed because no fault was present.
115
+
116
+ .. code-block:: python
117
+
118
+ await mqtt.reset_condenser_fault(device)
119
+
120
+ CLI: ``nwp-cli reset-condenser-fault``.
121
+
122
+ Commands the App Never Sends
123
+ ============================
124
+
125
+ The NaviLink app's command enum also declares an OTA check, WiFi reconnect
126
+ and reset, a freeze-protection temperature and a smart diagnostic trigger,
127
+ but its request builder has no case for any of them, so no NaviLink client
128
+ publishes those codes and their payloads are unknown. Earlier versions of
129
+ this library sent made-up payloads for them; those methods were removed.
130
+ The freeze-protection range and the smart diagnostic result remain
131
+ readable from :class:`~nwp500.models.DeviceFeature` and
132
+ :class:`~nwp500.models.DeviceStatus`.
133
+
134
+ Related Documentation
135
+ =====================
136
+
137
+ * :doc:`../reference/python_api/mqtt_client` - Full MQTT client API reference
138
+ * :doc:`schedule-operation` - Reservations, recirculation schedules, and intelligent scheduling
139
+ * :doc:`diagnose-mqtt` - Connection troubleshooting and diagnostics
@@ -580,95 +580,62 @@ Important Notes
580
580
  Weekly Reservations
581
581
  ===================
582
582
 
583
- ``update_weekly_reservation()`` configures a separate weekly reservation payload
584
- using :class:`~nwp500.models.WeeklyReservationSchedule`. This is useful when you
585
- want to send the whole weekly program as one typed object.
586
-
587
- .. code-block:: python
588
-
589
- from nwp500 import (
590
- WeeklyReservationEntry,
591
- WeeklyReservationSchedule,
592
- )
593
- from nwp500.encoding import build_reservation_entry
594
-
595
- morning = WeeklyReservationEntry.model_validate(
596
- build_reservation_entry(
597
- enabled=True,
598
- days=["MO", "TU", "WE", "TH", "FR"],
599
- hour=6,
600
- minute=0,
601
- mode_id=4,
602
- temperature=60.0,
603
- )
604
- )
605
-
606
- day = WeeklyReservationEntry.model_validate(
607
- build_reservation_entry(
608
- enabled=True,
609
- days=["MO", "TU", "WE", "TH", "FR"],
610
- hour=9,
611
- minute=0,
612
- mode_id=3,
613
- temperature=50.0,
614
- )
615
- )
616
-
617
- schedule = WeeklyReservationSchedule(
618
- reservationUse=2,
619
- reservation=[morning, day],
620
- )
621
-
622
- await mqtt.update_weekly_reservation(device, schedule)
623
-
624
- You can also subscribe to weekly reservation responses with
625
- :meth:`nwp500.mqtt.client.NavienMqttClient.subscribe_weekly_reservation_response`.
583
+ The NaviLink app's "weekly" schedule screen is the reservation program
584
+ above: it writes with ``update_reservations()`` on ``ctrl/rsv/rd`` and
585
+ reads with ``request_reservations()``. There is no separate weekly
586
+ command. (The app's enum declares a ``RESERVATION_WEEKLY`` code, but
587
+ nothing sends it; the ``update_weekly_reservation()`` method that earlier
588
+ library versions built on it was removed.)
626
589
 
627
590
  Recirculation Scheduling
628
591
  ========================
629
592
 
630
593
  Recirculation schedules are represented by
631
594
  :class:`~nwp500.models.RecirculationSchedule` and
632
- :class:`~nwp500.models.RecirculationScheduleEntry`.
595
+ :class:`~nwp500.models.RecirculationScheduleEntry`. The app builds these
596
+ entries with the same fields as a reservation entry: a day bitfield, a
597
+ time, and ``mode`` as the entry's on/off toggle (``2`` pump on, ``1``
598
+ pump off). The write goes to ``ctrl/recirc-rsv/rd`` and asks for the
599
+ schedule back on ``recirc-rsv/rd``; the read is
600
+ ``request_recirculation_schedule()``. Both are gated on the
601
+ ``recirc_reservation_use`` capability, which a unit can report without
602
+ having a recirculation pump fitted. Writes are limited to 20 entries with
603
+ in-range fields.
633
604
 
634
605
  .. code-block:: python
635
606
 
636
607
  from nwp500 import RecirculationSchedule, RecirculationScheduleEntry
637
608
 
638
609
  schedule = RecirculationSchedule(
639
- schedule=[
640
- RecirculationScheduleEntry(
641
- enable=2,
642
- week=124, # Mon-Fri
643
- start_hour=6,
644
- start_min=0,
645
- end_hour=8,
646
- end_min=30,
647
- mode=2,
648
- ),
649
- RecirculationScheduleEntry(
650
- enable=2,
651
- week=130, # Sat-Sun
652
- start_hour=7,
653
- start_min=0,
654
- end_hour=9,
655
- end_min=0,
656
- mode=2,
657
- ),
658
- ]
610
+ reservationUse=2,
611
+ reservation=[
612
+ # Mon-Fri: pump on at 06:00, off at 08:30
613
+ RecirculationScheduleEntry(enable=2, week=124, hour=6, min=0, mode=2),
614
+ RecirculationScheduleEntry(enable=2, week=124, hour=8, min=30, mode=1),
615
+ # Sat-Sun: pump on at 07:00, off at 09:00
616
+ RecirculationScheduleEntry(enable=2, week=130, hour=7, min=0, mode=2),
617
+ RecirculationScheduleEntry(enable=2, week=130, hour=9, min=0, mode=1),
618
+ ],
659
619
  )
660
620
 
661
- await mqtt.configure_recirculation_schedule(device, schedule)
662
-
663
- def on_recirculation(schedule):
664
- for entry in schedule.schedule:
665
- print(entry.start_time, entry.end_time, entry.mode_name)
621
+ def on_recirculation(schedule: RecirculationSchedule) -> None:
622
+ print("enabled" if schedule.enabled else "disabled")
623
+ for entry in schedule.reservation:
624
+ print(entry.days, entry.time, "on" if entry.pump_on else "off")
666
625
 
667
626
  await mqtt.subscribe_recirculation_schedule_response(device, on_recirculation)
627
+ await mqtt.configure_recirculation_schedule(device, schedule)
628
+ await mqtt.request_recirculation_schedule(device) # read back
629
+
630
+ The ``mode``/``param`` semantics and the write echo are inferred from the
631
+ app and have not been confirmed on a unit with recirculation fitted.
668
632
 
669
633
  Use :meth:`nwp500.mqtt.client.NavienMqttClient.set_recirculation_mode` to switch
670
634
  between always-on, button, schedule, and temperature modes.
671
635
 
636
+ CLI: ``nwp-cli recirc-schedule get`` and
637
+ ``nwp-cli recirc-schedule set '<json array of entries>'``.
638
+
672
639
  Intelligent Scheduling
673
640
  ======================
674
641