nwp500-python 9.4.1__tar.gz → 9.4.3__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 (231) hide show
  1. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/CHANGELOG.rst +152 -0
  2. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/Makefile +13 -5
  3. {nwp500_python-9.4.1/src/nwp500_python.egg-info → nwp500_python-9.4.3}/PKG-INFO +1 -1
  4. nwp500_python-9.4.3/docs/explanation/heating-elements.rst +452 -0
  5. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/explanation/index.rst +2 -0
  6. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/explanation/tank-energy.rst +56 -12
  7. nwp500_python-9.4.3/docs/explanation/what-starts-a-recovery.rst +303 -0
  8. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/how-to/schedule-operation.rst +92 -2
  9. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/how-to/track-energy.rst +8 -3
  10. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/protocol/data_conversions.rst +44 -5
  11. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/protocol/device_status.rst +17 -4
  12. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/python_api/models.rst +2 -2
  13. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/python_api/mqtt_client.rst +18 -0
  14. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/models/status.py +20 -10
  15. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/control.py +155 -1
  16. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/subscriptions.py +38 -1
  17. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/reservations.py +16 -0
  18. {nwp500_python-9.4.1 → nwp500_python-9.4.3/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  19. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500_python.egg-info/SOURCES.txt +3 -0
  20. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500_python.egg-info/scm_file_list.json +3 -0
  21. nwp500_python-9.4.3/src/nwp500_python.egg-info/scm_version.json +8 -0
  22. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_multi_device.py +149 -0
  23. nwp500_python-9.4.3/tests/test_reservation_validation.py +306 -0
  24. nwp500_python-9.4.1/src/nwp500_python.egg-info/scm_version.json +0 -8
  25. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/.coveragerc +0 -0
  26. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  27. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/.github/copilot-instructions.md +0 -0
  28. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/.github/workflows/ci.yml +0 -0
  29. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/.github/workflows/release.yml +0 -0
  30. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/.gitignore +0 -0
  31. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/.pre-commit-config.yaml +0 -0
  32. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/.readthedocs.yml +0 -0
  33. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/AUTHORS.rst +0 -0
  34. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/CONTRIBUTING.rst +0 -0
  35. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/LICENSE.txt +0 -0
  36. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/README.rst +0 -0
  37. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/RELEASE.md +0 -0
  38. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/Makefile +0 -0
  39. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/_static/.gitignore +0 -0
  40. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/conf.py +0 -0
  41. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/explanation/advanced-features.rst +0 -0
  42. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/explanation/architecture.rst +0 -0
  43. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/explanation/tou-recovery-cap.rst +0 -0
  44. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/explanation/unknown-values.rst +0 -0
  45. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/how-to/authenticate.rst +0 -0
  46. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/how-to/auto-recovery.rst +0 -0
  47. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/how-to/diagnose-mqtt.rst +0 -0
  48. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/how-to/home-assistant.rst +0 -0
  49. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/how-to/index.rst +0 -0
  50. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/how-to/maintenance.rst +0 -0
  51. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/how-to/manage-units.rst +0 -0
  52. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/how-to/monitor-status.rst +0 -0
  53. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/how-to/optimize-tou.rst +0 -0
  54. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/how-to/queue-commands.rst +0 -0
  55. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/index.rst +0 -0
  56. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/openapi.yaml +0 -0
  57. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/project/authors.rst +0 -0
  58. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/project/changelog.rst +0 -0
  59. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/project/contributing.rst +0 -0
  60. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/project/history.rst +0 -0
  61. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/project/license.rst +0 -0
  62. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/configuration.rst +0 -0
  63. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/enumerations.rst +0 -0
  64. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/index.rst +0 -0
  65. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/installation.rst +0 -0
  66. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/protocol/device_features.rst +0 -0
  67. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/protocol/error_codes.rst +0 -0
  68. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  69. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/protocol/quick_reference.rst +0 -0
  70. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/protocol/rest_api.rst +0 -0
  71. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/python_api/api_client.rst +0 -0
  72. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/python_api/auth_client.rst +0 -0
  73. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/python_api/cli.rst +0 -0
  74. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/python_api/events.rst +0 -0
  75. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/reference/python_api/exceptions.rst +0 -0
  76. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/requirements.txt +0 -0
  77. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/docs/tutorials/getting-started.rst +0 -0
  78. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/.ruff.toml +0 -0
  79. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/README.md +0 -0
  80. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/air_filter_reset.py +0 -0
  81. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/anti_legionella.py +0 -0
  82. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/auto_recovery.py +0 -0
  83. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/combined_callbacks.py +0 -0
  84. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/demand_response.py +0 -0
  85. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/device_capabilities.py +0 -0
  86. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/device_status_debug.py +0 -0
  87. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/energy_analytics.py +0 -0
  88. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/error_code_demo.py +0 -0
  89. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/firmware_payload_capture.py +0 -0
  90. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/mqtt_diagnostics.py +0 -0
  91. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/power_control.py +0 -0
  92. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/recirculation_control.py +0 -0
  93. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/reconnection_demo.py +0 -0
  94. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/reservation_schedule.py +0 -0
  95. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/simple_auto_recovery.py +0 -0
  96. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/token_restoration.py +0 -0
  97. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/tou_openei.py +0 -0
  98. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/tou_schedule.py +0 -0
  99. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/advanced/water_reservation.py +0 -0
  100. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/beginner/01_authentication.py +0 -0
  101. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/beginner/02_list_devices.py +0 -0
  102. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/beginner/03_get_status.py +0 -0
  103. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/beginner/04_set_temperature.py +0 -0
  104. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/intermediate/advanced_auth_patterns.py +0 -0
  105. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/intermediate/command_queue.py +0 -0
  106. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/intermediate/device_status_callback.py +0 -0
  107. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/intermediate/error_handling.py +0 -0
  108. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/intermediate/event_driven_control.py +0 -0
  109. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/intermediate/improved_auth.py +0 -0
  110. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/intermediate/legacy_auth_constructor.py +0 -0
  111. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  112. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/intermediate/periodic_requests.py +0 -0
  113. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/intermediate/set_mode.py +0 -0
  114. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/intermediate/vacation_mode.py +0 -0
  115. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/mask.py +0 -0
  116. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/testing/periodic_device_info.py +0 -0
  117. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/testing/simple_periodic_info.py +0 -0
  118. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/testing/test_api_client.py +0 -0
  119. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/testing/test_mqtt_connection.py +0 -0
  120. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/testing/test_mqtt_messaging.py +0 -0
  121. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/examples/testing/test_periodic_minimal.py +0 -0
  122. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/pyproject.toml +0 -0
  123. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/scripts/README.md +0 -0
  124. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/scripts/bump_version.py +0 -0
  125. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/scripts/diagnose_mqtt_connection.py +0 -0
  126. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/scripts/extract_changelog.py +0 -0
  127. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/scripts/format.py +0 -0
  128. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/scripts/lint.py +0 -0
  129. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/scripts/setup-dev.py +0 -0
  130. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/scripts/validate_version.py +0 -0
  131. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/setup.cfg +0 -0
  132. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/setup.py +0 -0
  133. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/__init__.py +0 -0
  134. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/_base.py +0 -0
  135. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/api_client.py +0 -0
  136. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/auth.py +0 -0
  137. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/cli/__init__.py +0 -0
  138. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/cli/__main__.py +0 -0
  139. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/cli/handlers.py +0 -0
  140. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/cli/monitoring.py +0 -0
  141. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/cli/output_formatters.py +0 -0
  142. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/cli/presentation.py +0 -0
  143. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/cli/rich_output.py +0 -0
  144. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/cli/token_storage.py +0 -0
  145. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/command_decorators.py +0 -0
  146. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/config.py +0 -0
  147. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/converters.py +0 -0
  148. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/device_capabilities.py +0 -0
  149. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/device_info_cache.py +0 -0
  150. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/encoding.py +0 -0
  151. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/enums.py +0 -0
  152. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/events.py +0 -0
  153. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/exceptions.py +0 -0
  154. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/factory.py +0 -0
  155. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/field_factory.py +0 -0
  156. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/models/__init__.py +0 -0
  157. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/models/_converters.py +0 -0
  158. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/models/device.py +0 -0
  159. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/models/diagnostics.py +0 -0
  160. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/models/energy.py +0 -0
  161. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/models/feature.py +0 -0
  162. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/models/firmware.py +0 -0
  163. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/models/mqtt_models.py +0 -0
  164. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/models/schedule.py +0 -0
  165. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/models/tou.py +0 -0
  166. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/__init__.py +0 -0
  167. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/_control_commands.py +0 -0
  168. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
  169. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/client.py +0 -0
  170. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/command_queue.py +0 -0
  171. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/connection.py +0 -0
  172. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/diagnostics.py +0 -0
  173. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/periodic.py +0 -0
  174. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/reconnection.py +0 -0
  175. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/state_tracker.py +0 -0
  176. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/types.py +0 -0
  177. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt/utils.py +0 -0
  178. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/mqtt_events.py +0 -0
  179. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/openei.py +0 -0
  180. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/py.typed +0 -0
  181. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/temperature.py +0 -0
  182. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/topic_builder.py +0 -0
  183. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/tou_schedule.py +0 -0
  184. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/unit_system.py +0 -0
  185. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500/utils.py +0 -0
  186. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  187. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  188. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  189. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500_python.egg-info/requires.txt +0 -0
  190. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/src/nwp500_python.egg-info/top_level.txt +0 -0
  191. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/conftest.py +0 -0
  192. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_api_helpers.py +0 -0
  193. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_app_queries.py +0 -0
  194. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_auth.py +0 -0
  195. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_auth_session_lifecycle.py +0 -0
  196. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_bug_fixes.py +0 -0
  197. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_canonical_schedule.py +0 -0
  198. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_cli_basic.py +0 -0
  199. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_cli_commands.py +0 -0
  200. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_cli_tristate_presentation.py +0 -0
  201. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_command_decorators.py +0 -0
  202. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_command_queue.py +0 -0
  203. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_device_capabilities.py +0 -0
  204. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_device_info_cache.py +0 -0
  205. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_device_rest_models.py +0 -0
  206. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_events.py +0 -0
  207. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_exceptions.py +0 -0
  208. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_model_converters.py +0 -0
  209. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_models.py +0 -0
  210. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_mqtt_clean_session_resume.py +0 -0
  211. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_mqtt_client_init.py +0 -0
  212. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_mqtt_events.py +0 -0
  213. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_mqtt_hypothesis.py +0 -0
  214. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_mqtt_reconnection.py +0 -0
  215. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_mqtt_reconnection_storm.py +0 -0
  216. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_mqtt_reliability.py +0 -0
  217. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_openei.py +0 -0
  218. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_protocol_correctness.py +0 -0
  219. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_public_api.py +0 -0
  220. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_reservations.py +0 -0
  221. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_temperature_converters.py +0 -0
  222. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_threading_model.py +0 -0
  223. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_token_storage.py +0 -0
  224. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_tou_api.py +0 -0
  225. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_tou_schedule.py +0 -0
  226. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_unit_switching.py +0 -0
  227. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_unit_system_process_wide.py +0 -0
  228. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_utility_modules.py +0 -0
  229. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tests/test_utils.py +0 -0
  230. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/tox.ini +0 -0
  231. {nwp500_python-9.4.1 → nwp500_python-9.4.3}/uv.lock +0 -0
@@ -5,6 +5,158 @@ Changelog
5
5
  Unreleased
6
6
  ==========
7
7
 
8
+ Version 9.4.3 (2026-10-01)
9
+ ==========================
10
+
11
+ Changed
12
+ -------
13
+ - **Raw reservation writes are now validated** (#148).
14
+ ``update_reservations()`` and ``update_reservations_confirmed()`` sent
15
+ their entry dicts unchecked, so an unknown mode such as 7, an hour of 99
16
+ or a setpoint of 200 (100 degC) went to the device as-is. A Python
17
+ ``True`` for ``enable`` was worse: the confirmed helper read it as 1,
18
+ which means disabled. Every entry is now checked before anything is
19
+ sent, with the same rules as ``build_reservation_entry()``: the six
20
+ protocol fields present and plain integers, ``enable`` 1 or 2, ``week``
21
+ a day bitfield, ``hour`` 0-23, ``min`` 0-59 and ``mode`` a
22
+ ``DhwOperationSetting`` id. ``param`` is held to the setpoint range the
23
+ heater reports in its feature data (``dhw_temperature_min_raw`` to
24
+ ``dhw_temperature_max_raw``), which is requested if it is not cached;
25
+ clearing the schedule with an empty list needs none. A bad entry raises
26
+ ``ParameterValidationError`` or ``RangeValidationError``, and missing
27
+ feature data raises ``DeviceCapabilityError``. Callers that wrote
28
+ entries outside these ranges and relied on the heater to clamp them now
29
+ get an error instead. The checks are also available on their own as
30
+ ``nwp500.mqtt.control.validate_reservation_entries()``.
31
+
32
+ Documentation
33
+ -------------
34
+ - **New: what starts a recovery**
35
+ (``docs/explanation/what-starts-a-recovery.rst``). The lower-tank
36
+ turn-on setting is one trigger among several. Measured on one unit:
37
+ outside a TOU window the lower setting reads 104.9 degF at every
38
+ setpoint and never follows it; at a window's opening the upper tank
39
+ starts a cycle when it is well below the setpoint, and the lower trigger
40
+ waits for the close. **A control write re-evaluates**: outside a window,
41
+ in ``HEAT_PUMP`` with the compressor off, a setpoint write leaving the
42
+ upper tank below the new setpoint started the compressor within
43
+ 2 minutes in 112 of 117 writes (a median 30 s later), lowerings
44
+ included. 46 of 81 mode writes made with the compressor off did the
45
+ same. A setpoint dropped well below the upper tank stopped a running
46
+ compressor within 5 s (one run). A lowered setpoint did not hold off a
47
+ draw-driven start (one run, in ``ENERGY_SAVER``). The page also covers
48
+ traps for anyone re-deriving this from history.
49
+ - **TOU window: the two open cases measured, and three more results.** In
50
+ ``docs/how-to/schedule-operation.rst`` (one or two runs each): an entry
51
+ scheduled after a window fires on its minute; a mode held in a window
52
+ takes effect when the window ends naturally; writing ``HEAT_PUMP`` over
53
+ a held mode withdraws it; turning TOU back on inside a window drops a
54
+ running element mode; and the operation setting reports the configured
55
+ mode, not activity. The section's first measurement date is corrected to
56
+ local time (2026-09-19).
57
+ - **New: what the heating elements actually do**
58
+ (``docs/explanation/heating-elements.rst``). The four
59
+ ``he*TempSetting`` fields describe the thermostat, and **on entry to
60
+ a mode the thermostat is not what runs the element**: in
61
+ Electric and High Demand the on and off settings are the same value,
62
+ and every ``he*DiffTempSetting`` reads 0. Measured on one unit by
63
+ commanding the modes and watching, the start differential is
64
+ **mode-dependent**, one bracket per mode: ``ELECTRIC`` at most
65
+ **0.3 degC** below the setpoint, ``HIGH_DEMAND`` within
66
+ **(0.2, 0.7] degC**, ``ENERGY_SAVER`` within **(0.8, 1.0] degC**.
67
+ Whether the first two share one threshold is an open question, not a
68
+ finding. At 0.7 degC short, High Demand runs the element and Energy
69
+ Saver does not. These
70
+ are **entry** thresholds, not thermostats: in High Demand's steady
71
+ state this unit's history has the element on in 1 of 47 minutes
72
+ 0.2-0.3 degC short and 168 of 169 more than 2.2 degC short.
73
+ Neither differential is published anywhere in the status message.
74
+
75
+ **Later in a stint it depends on the mode.** From one
76
+ unit's recorded history, 17 of 23 mid-stint upper-element starts in
77
+ ``ENERGY_SAVER`` have the probe in the 39.4-41.1 degC band where
78
+ ``heUpperOnTempSetting`` rests, a median 0.2 degC below the field -
79
+ the element comes on as the probe reaches it. ``HIGH_DEMAND``, whose
80
+ ON setting tracks the setpoint, re-engages a median 2.1 degC *below*
81
+ it, which the field does not describe. So in ``ENERGY_SAVER`` entry is
82
+ the exception and the reference description is the rule; in
83
+ ``HIGH_DEMAND`` it is not; ``ELECTRIC`` is uncharacterised.
84
+ ``data_conversions.rst``'s warning now says which applies when.
85
+ A sampling note with it: the element runs in two very different
86
+ lengths. Of 115 upper-element runs over eight months, 20 lasted under
87
+ 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
89
+ is ~0.3 degC. Because ``currentPower`` samples at a median 30 s, a
90
+ one-minute-resampled power series can miss one entirely, making a
91
+ 5.5 kW element look like 0.5 kW. Resample power with a maximum.
92
+
93
+ Confirmed on the device, one unit, 2026-09-21: in ``ENERGY_SAVER``
94
+ the upper element engages near ``heUpperOnTempSetting`` rather than a
95
+ fixed gap below the setpoint (setpoint lowered to 42.0 degC; a gap
96
+ predicted 22 degC; it engaged at 39.8, three times). It then runs to
97
+ ~0.9 degC under the setpoint-tracking ``heUpperOffTempSetting``, not
98
+ to a fixed point. Raising the setpoint in that mode starts the element
99
+ through the entry rule. And ``heUpperOnTempSetting`` rests at 40.5
100
+ degC reliably only in ``ENERGY_SAVER`` - in ``HEAT_PUMP`` about half
101
+ the time.
102
+
103
+ Also documents Energy Saver's entry behaviour (the element engages
104
+ although ``heUpperOnTempSetting`` rests 33 degC below the tank), Electric's
105
+ upper-to-lower handover and the upper probe **falling** 1.9 degC while
106
+ the lower element runs, measured element power of 5,219 W against the
107
+ 5,000 W rating, and that a mode write may not take effect while a TOU
108
+ window is in force. ``device_status.rst``'s Heating Elements notes now
109
+ point at it.
110
+ - **currentInstPower includes the heating elements.** The field
111
+ table and *Power and Energy Fields* in the protocol reference, and the
112
+ ``current_inst_power`` field description, said it excludes element
113
+ power; ``track-energy.rst`` said it includes it. On a 240 V NWP500-65
114
+ it includes it: element-only minutes read about 5.1 kW, compressor
115
+ and element together about 5.5 kW, and the tank's heat gain agrees.
116
+ All four now say so, with the measurement and its limits (one model,
117
+ 240 V only). (#139)
118
+ - **TOU does not suspend reservations.** ``schedule-operation.rst``
119
+ said twice that it does, and its own priority table said otherwise. On
120
+ one NWP500, inside an active peak window, a reservation entry moved the
121
+ setpoint at its scheduled minute, twice. What the window holds back is
122
+ the entry's **mode**, as it does a direct ``set_operation_mode()``.
123
+ The mode is held, not discarded: a mode written in the window took
124
+ effect when TOU was switched off, and the upper element came on. The
125
+ priority table and Important Notes are corrected, and a new section
126
+ gives the measurements and their limits. ``heating-elements.rst`` now
127
+ links to it instead of to the issue. (#141)
128
+ - **tank-energy: what selects the second branch, and where usable_energy
129
+ under-reads.** ``tank-energy.rst`` left open what selects
130
+ ``full_recovery_energy``'s second branch. On one NWP500-65 over 50,819
131
+ ``HEAT_PUMP`` minutes, ``hpUpperOnTempSetting`` separates the branches
132
+ with no exceptions: 104.9 degF on the primary, raised to about the
133
+ setpoint during the afternoon TOU window on the secondary.
134
+ ``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
136
+ caps at ``full_recovery_energy`` once ``energy_to_setpoint`` hits 0,
137
+ up to about 3.6 degF short of the setpoint, and under-reads by up to
138
+ 561 Wh. The page and the ``usable_energy`` docstring now say so and
139
+ recommend the thermistors in that case. The ``full_recovery_energy``
140
+ and ``energy_to_setpoint`` descriptions in the model, the protocol
141
+ reference, the API reference and ``track-energy.rst`` now note the
142
+ window too. (#140)
143
+
144
+ Version 9.4.2 (2026-09-17)
145
+ ==========================
146
+
147
+ Changed
148
+ -------
149
+ - **A dropped response message now says why it was dropped.** The typed
150
+ response handlers declined a message in three places without a word: a
151
+ topic that does not end in the required suffix, a payload with no data
152
+ under the expected key, and a reply whose ``macAddress`` names another
153
+ device. A caller watching for a reply that never reached its callback
154
+ could not tell a dropped message from one the cloud never published, and
155
+ the two want opposite fixes. Each branch now logs its reason at DEBUG,
156
+ with the topic and MAC redacted as elsewhere, and guarded by
157
+ ``isEnabledFor`` so the redaction regexes do not run on the normal
158
+ dispatch path when debug logging is off.
159
+
8
160
  Version 9.4.1 (2026-09-17)
9
161
  ==========================
10
162
 
@@ -1,4 +1,4 @@
1
- .PHONY: help install install-dev lint format test clean build release check-release ci-lint ci-format ci-check version-bump validate-version
1
+ .PHONY: help install install-dev lint format test test-cov require-python clean build release check-release ci-lint ci-format ci-check version-bump validate-version
2
2
 
3
3
  help: ## Show this help message
4
4
  @echo 'Usage: make [target]'
@@ -35,11 +35,19 @@ ci-format: ## Run formatting exactly as CI does (via tox format)
35
35
  ci-check: ci-lint ## Run the same checks as CI (ensures local/CI consistency)
36
36
  @echo "🎉 All CI checks passed locally!"
37
37
 
38
- test: ## Run tests with pytest
39
- pytest
38
+ # The package declares python_requires = >=3.14 and uses 3.14 syntax, but
39
+ # that floor is only enforced when installing - running pytest against the
40
+ # source tree happens on whatever interpreter is active. Fail with a clear
41
+ # message instead of a SyntaxError in every test module.
42
+ require-python:
43
+ @python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 14) else 1)' \
44
+ || { echo "Python 3.14+ required, found $$(python3 -V 2>&1). Activate the project environment."; exit 1; }
40
45
 
41
- test-cov: ## Run tests with coverage report
42
- pytest --cov=nwp500 --cov-report=html --cov-report=term-missing
46
+ test: require-python ## Run tests with pytest
47
+ python3 -m pytest
48
+
49
+ test-cov: require-python ## Run tests with coverage report
50
+ python3 -m pytest --cov=nwp500 --cov-report=html --cov-report=term-missing
43
51
 
44
52
  clean: ## Remove build artifacts and cache files
45
53
  rm -rf build dist *.egg-info .eggs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.4.1
3
+ Version: 9.4.3
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
@@ -0,0 +1,452 @@
1
+ ====================
2
+ The Heating Elements
3
+ ====================
4
+
5
+ The NWP500 publishes four element thermostat settings -
6
+ ``heUpperOnTempSetting``, ``heUpperOffTempSetting`` and their lower-zone
7
+ pair - plus a differential for each. Read literally they describe a
8
+ thermostat, and how far that holds depends on the mode and the moment:
9
+
10
+ * **On entry to any element mode they do not** describe when the element
11
+ runs. It engages on a mode-dependent differential of its own, published
12
+ nowhere - the start thresholds below.
13
+ * **Later in a stint, in** ``ENERGY_SAVER`` **they do**: the element
14
+ engages as the probe reaches ``heUpperOnTempSetting``. That is from this
15
+ unit's history and confirmed by a commanded run.
16
+ * **Later in a stint, in** ``HIGH_DEMAND`` **they do not**: it re-engages
17
+ about 2.1 degC below its ON setting. That is from history only.
18
+ ``ELECTRIC`` is uncharacterised.
19
+
20
+ The fields also look wrong as a thermostat taken literally: in Electric
21
+ and High Demand the on and off settings are **the same value**, and every
22
+ ``he*DiffTempSetting`` reads 0.
23
+
24
+ **The start thresholds** here were measured on one unit by commanding
25
+ the modes and watching - not inferred from historical logs. That
26
+ distinction matters: two earlier values came from "the smallest deficit
27
+ ever seen to start an element" in a passive record, and both were wrong,
28
+ because such a figure measures where the tank happened to sit rather
29
+ than where the device decides.
30
+
31
+ Two things on this page are *not* from commanded runs, and are labelled
32
+ where they appear: High Demand's **steady-state** re-engagement, which
33
+ is this unit's history, and the mid-stint behaviour it is contrasted
34
+ with. Everything else is a bounded run.
35
+
36
+ .. contents::
37
+ :local:
38
+ :depth: 2
39
+
40
+
41
+ The short version
42
+ =================
43
+
44
+ .. list-table::
45
+ :header-rows: 1
46
+ :widths: 34 66
47
+
48
+ * - Observation
49
+ - Value
50
+ * - Element start differential
51
+ - **Mode-dependent, one bracket per mode.** ``ELECTRIC`` at most
52
+ **0.3 degC (0.54 degF)** below the setpoint; ``HIGH_DEMAND``
53
+ somewhere in **(0.2, 0.7] degC** ((0.36, 1.26] degF);
54
+ ``ENERGY_SAVER`` in **(0.8, 1.0] degC** ((1.44, 1.80] degF).
55
+ Each bracket has a run on either side of it - except Electric's
56
+ lower edge, whose only negative sits *above* the setpoint.
57
+ * - Where that differential is published
58
+ - **Nowhere.** ``heUpperOnDiffTempSetting`` and its three siblings
59
+ read 0 on this unit while the behaviour above holds.
60
+ * - ``heUpperOnTempSetting`` by mode
61
+ - ``ELECTRIC`` and ``HIGH_DEMAND``: the setpoint, same as
62
+ ``heUpperOffTempSetting``. ``ENERGY_SAVER``: 40.5 degC
63
+ (104.9 degF), the device minimum, in 96 % of recorded minutes on
64
+ this unit. ``HEAT_PUMP``: 40.5 degC only about **half** the time;
65
+ the rest are other values that match the current setpoint only
66
+ 6 % of the time. Why is not known.
67
+ * - Energy Saver on entry
68
+ - Switching into ``ENERGY_SAVER`` engages the upper element even
69
+ though ``heUpperOnTempSetting`` rests 33 degC below the tank, so
70
+ **on entry** that field is not what drives it. Its bracket also
71
+ sits higher than the other two modes'. Why the two differ is
72
+ **not** established: the Energy Saver runs had the compressor
73
+ running and the others did not.
74
+ * - Energy Saver later in a stint
75
+ - Different rule. The element re-engages **as the upper probe
76
+ reaches** ``heUpperOnTempSetting`` - the field working exactly
77
+ as this reference describes it. Entry is the exception, not the
78
+ rule.
79
+ * - Electric ordering
80
+ - Upper element to the setpoint, then the lower element, never
81
+ both, as the protocol reference already states. The handover
82
+ takes one poll.
83
+ * - Element power
84
+ - 5,200-5,242 W over three measurements (two upper, one lower),
85
+ metered with the compressor off. The reference gives 5,000 W at
86
+ 240 V.
87
+ * - Zone coupling
88
+ - While the lower element runs, the **upper** probe falls about
89
+ 1.9 degC (3.4 degF) before recovering. The zones are not
90
+ independent.
91
+
92
+
93
+ The start differential
94
+ ======================
95
+
96
+ Three modes, three brackets, and no two of them the same width. Each
97
+ run holds a fixed deficit and watches for one to two minutes; the
98
+ setpoint quantises to 0.5 degC, so the deficit is set by choosing a
99
+ setpoint step relative to the tank.
100
+
101
+ ``ENERGY_SAVER``
102
+ ----------------
103
+
104
+ .. list-table::
105
+ :header-rows: 1
106
+ :widths: 30 30 40
107
+
108
+ * - Deficit below setpoint
109
+ - Upper element
110
+ - Poll at which it engaged
111
+ * - 0.3 degC (0.54 degF)
112
+ - **off** for the whole watch
113
+ - n/a
114
+ * - 0.8 degC (1.44 degF)
115
+ - **off** for the whole watch
116
+ - n/a
117
+ * - 1.0 degC (1.80 degF)
118
+ - **on**
119
+ - first poll after the mode landed
120
+ * - 1.2 degC (2.16 degF)
121
+ - **on**
122
+ - first poll after the mode landed
123
+
124
+ So the boundary lies in (0.8, 1.0] degC, and 1.0 degC is the device's
125
+ own storage granularity - settings are held in half-degree Celsius
126
+ steps - which makes a one-degree differential the natural parameter.
127
+
128
+ A further run put the element on at 2.0 degC (3.6 degF) short, so
129
+ nothing above the boundary has been seen to decline.
130
+
131
+ ``ELECTRIC``
132
+ ------------
133
+
134
+ .. list-table::
135
+ :header-rows: 1
136
+ :widths: 30 30 40
137
+
138
+ * - Deficit below setpoint
139
+ - Upper element
140
+ - Poll at which it engaged
141
+ * - 0.6 degC **above** the setpoint
142
+ - **off** for the whole watch
143
+ - n/a
144
+ * - 0.3 degC (0.54 degF)
145
+ - **on**
146
+ - first poll after the mode landed
147
+ * - 0.4 degC (0.72 degF)
148
+ - **on**
149
+ - first poll after the mode landed
150
+
151
+ ``HIGH_DEMAND``
152
+ ---------------
153
+
154
+ .. list-table::
155
+ :header-rows: 1
156
+ :widths: 30 30 40
157
+
158
+ * - Deficit below setpoint
159
+ - Upper element
160
+ - Poll at which it engaged
161
+ * - 0.2 degC (0.36 degF)
162
+ - **off** for the whole watch
163
+ - n/a
164
+ * - 0.7 degC (1.26 degF)
165
+ - **on**
166
+ - first poll after the mode landed
167
+
168
+ ``heUpperOnTempSetting`` moved to the new setpoint in the same status
169
+ message the mode landed in, as it does in Electric.
170
+
171
+ Putting the three together
172
+ --------------------------
173
+
174
+ .. list-table::
175
+ :header-rows: 1
176
+ :widths: 24 26 24 26
177
+
178
+ * - Mode
179
+ - Element off at
180
+ - Element on at
181
+ - Bracket
182
+ * - ``ELECTRIC``
183
+ - 0.6 degC above setpoint
184
+ - 0.3, 0.4 degC
185
+ - at most 0.3 degC [#electric]_
186
+ * - ``HIGH_DEMAND``
187
+ - 0.2 degC
188
+ - 0.7 degC
189
+ - (0.2, 0.7] degC
190
+ * - ``ENERGY_SAVER``
191
+ - 0.3, 0.8 degC
192
+ - 1.0, 1.2 degC
193
+ - (0.8, 1.0] degC
194
+
195
+ **These are entry thresholds, not thermostats.** Every run measured what
196
+ happens within a poll of switching *into* a mode. In ``HIGH_DEMAND``'s
197
+ steady state this unit's history has the upper element on in 1 of 47
198
+ minutes 0.2-0.3 degC short and 168 of 169 minutes more than 2.2 degC
199
+ short - so a stint already running re-engages on something much larger
200
+ than the entry figure. Do not use one for the other.
201
+
202
+ .. [#electric] Electric's own negative is 0.6 degC *above* the setpoint,
203
+ so nothing at all was observed between there and 0.3 below it. Its
204
+ bracket is open at the bottom: no run has shown Electric declining
205
+ to heat a tank that is short of the setpoint by any amount.
206
+
207
+ **Electric and High Demand may share one entry threshold. That is an
208
+ inference, not a measurement.** Their brackets overlap in
209
+ (0.2, 0.3] degC and the two modes handle ``heUpperOnTempSetting``
210
+ identically, which is a real argument for one number covering both -
211
+ but High Demand has never been watched between 0.2 and 0.7 degC, so
212
+ nothing here measures where its boundary falls inside that span.
213
+ Energy Saver's band does not intersect either of them: at 0.7 degC
214
+ short, High Demand runs the element and Energy Saver does not. One
215
+ caveat on that last comparison: the Energy Saver runs had the
216
+ compressor running and the others did not, so mode is not the only
217
+ difference between them.
218
+
219
+ A consumer wanting one number per mode should take the **top of that
220
+ mode's own bracket** - 0.3 degC for ``ELECTRIC``, **0.7 degC** for
221
+ ``HIGH_DEMAND``, 1.0 degC for ``ENERGY_SAVER``. Each is the **smallest
222
+ deficit at which that mode was seen to start an element**, so assuming
223
+ it contradicts no observation of that mode - where anything lower
224
+ contradicts that mode's own negative. Borrowing Electric's 0.3 degC for High Demand predicts an
225
+ element at, say, 0.4 degC short that High Demand has never been seen to
226
+ run; if your consumer is deciding whether hot water will be ready,
227
+ that is the expensive direction to be wrong in.
228
+
229
+ **The differential is not in the protocol.** A ``status --raw`` taken
230
+ between these runs shows::
231
+
232
+ "heUpperOnTempSetting": 81, # 40.5 degC
233
+ "heUpperOffTempSetting": 122, # 61.0 degC
234
+ "heUpperOnDiffTempSetting": 0,
235
+ "heUpperOffDiffTempSetting": 0,
236
+ "heLowerOnTDiffempSetting": 0,
237
+ "heLowerOffDiffTempSetting": 0,
238
+
239
+ A consumer that wants to predict when an element will run therefore
240
+ cannot read it from the status message; it has to assume the per-mode
241
+ figures above - 0.3 degC for ``ELECTRIC``, 0.7 for ``HIGH_DEMAND``,
242
+ 1.0 for ``ENERGY_SAVER`` - and treat a tank inside the corresponding
243
+ bracket as a case where the device may go either way. High Demand's
244
+ bracket is the widest, so that "either way" span is widest there too.
245
+
246
+
247
+ What the on-setting does and does not tell you
248
+ ==============================================
249
+
250
+ ``heUpperOnTempSetting`` tracks the **mode**, not the moment:
251
+
252
+ * In ``ELECTRIC`` and ``HIGH_DEMAND`` it equals ``heUpperOffTempSetting``
253
+ and both equal the DHW setpoint. Taken literally that is a thermostat
254
+ with no hysteresis at all, which cannot be how the device behaves; on
255
+ entry the measured differential is at most 0.3 degC in Electric and
256
+ within (0.2, 0.7] degC in High Demand.
257
+ * In ``ENERGY_SAVER`` it rests at 40.5 degC, the device minimum, which
258
+ is 33 degC below a normally charged tank. An earlier version of this
259
+ page said the same of ``HEAT_PUMP``; on this unit that holds in only
260
+ 48.5 % of Heat Pump minutes. The rest are other values - 63.0 degC
261
+ most often - which equal the *current* setpoint in only 6 % of Heat
262
+ Pump minutes, so this is not the field tracking the setpoint; it may
263
+ be holding a value over from an earlier mode. Do not rely on it in
264
+ Heat Pump mode.
265
+
266
+ The second case misleads **on entry only**. Switching into
267
+ ``ENERGY_SAVER`` with the tank 1.0 degC or more short brought the upper
268
+ element on within one poll in both runs tested, while
269
+ ``heUpperOnTempSetting`` dropped from the setpoint to 40.5 degC in the
270
+ *same* status message - 33 degC below the probe. For that moment the
271
+ field explains nothing.
272
+
273
+ That is as far as the entry observation goes. It does not explain *why*
274
+ Energy Saver's bracket sits higher than Electric's; the runs differ in
275
+ compressor state as well as mode, and nothing here separates the two.
276
+
277
+ **Later in the same stint the field is exactly what drives the
278
+ element.** In 23 mid-stint upper-element starts in ``ENERGY_SAVER``
279
+ drawn from one unit's recorded history, **17 have the upper probe
280
+ between 39.4 and 41.1 degC** - the band ``heUpperOnTempSetting`` rests
281
+ in for this mode - and where the field itself is recorded the median
282
+ start is **0.2 degC below it**. The element comes on as the probe
283
+ reaches the on-setting, which is what this reference has said all
284
+ along.
285
+
286
+ **Confirmed on the device.** The history above cannot prove it on its
287
+ own: with the setpoint fixed for almost the whole record, "at the
288
+ on-setting" and "a fixed gap below the setpoint" are the same
289
+ prediction. So the setpoint was lowered to 42.0 degC and the unit left
290
+ in ``ENERGY_SAVER``. With the on-setting still at 40.5 degC, a fixed gap
291
+ would have put the element near 22 degC; it came on three times with
292
+ the upper probe reading **39.8 degC**. Two cautions: all three came
293
+ while water was moving through the tank, and readings of 40.4, 40.1 and
294
+ 40.0 degC did not trigger, so on this probe the working threshold may
295
+ sit a little under the published 40.5.
296
+
297
+ ``HIGH_DEMAND`` behaves differently, and this is the clearest statement
298
+ of the difference on this page. There ``heUpperOnTempSetting`` tracks
299
+ the setpoint, and 12 mid-stint starts came a median **2.1 degC below
300
+ it**, none in the 39.4-41.1 degC band. That differential is real and is
301
+ published nowhere.
302
+
303
+ So a consumer needs both halves:
304
+
305
+ .. list-table::
306
+ :header-rows: 1
307
+ :widths: 26 37 37
308
+
309
+ * - Mode
310
+ - On entry
311
+ - Later in the stint
312
+ * - ``ENERGY_SAVER``
313
+ - element on, on-setting irrelevant (1.0 degC below setpoint)
314
+ - element on **near** ``heUpperOnTempSetting``
315
+ * - ``HIGH_DEMAND``
316
+ - element on, (0.2, 0.7] degC below setpoint
317
+ - element on ~2.1 degC **below** ``heUpperOnTempSetting``
318
+
319
+ Three more behaviours, from the same commanded run. Each is one unit
320
+ and one morning; the counts are given so they can be weighed.
321
+
322
+ * **The element runs to the setpoint, not to a fixed point.** It
323
+ stopped about 0.9 degC under ``heUpperOffTempSetting``, which follows
324
+ the setpoint. At a 42.0 degC setpoint that was 41.1 degC; historical
325
+ runs at a 60.5 degC setpoint stop around 59. Only the *engagement*
326
+ point is fixed.
327
+ * **Raising the setpoint in** ``ENERGY_SAVER`` **starts the element.**
328
+ With the tank at 41.7 degC, a setpoint raise to 61.0 degC switched the
329
+ heat source to heat pump plus element in the same second (n = 1). It
330
+ is the entry rule again, triggered by the setpoint rather than a mode
331
+ change. A controller writing setpoints in this mode can start a 5 kW
332
+ element.
333
+ * **Every engagement came with water moving through the tank** — one
334
+ shower and two runs of a recirculation pump on a cooled loop, which
335
+ the tank experiences as draws. Whether movement is *required*, or the
336
+ temperature alone suffices, is not separable from these.
337
+
338
+ .. warning::
339
+
340
+ These mid-stint figures come from **recorded history, not commanded
341
+ runs**, and they carry a sampling hazard worth repeating - one that
342
+ bit us.
343
+
344
+ **The element runs in two very different lengths.** Of 115 raw
345
+ upper-element runs on this unit over eight months, 20 (17 %) lasted
346
+ **under a minute** - median 16 s - and 95 ran a median 8 minutes.
347
+ The short ones are real elements at full power. They carry little
348
+ energy - 0.6 kWh across the record against 93 kWh for the rest -
349
+ but they are not negligible to the probe: a 20-second burst raises
350
+ the upper zone about 0.3 degC, three quanta of its 0.1 degC
351
+ resolution, and measured bursts of 41-58 s moved it 0.7-1.5 degC.
352
+ *(An earlier version said they "heat nothing", from dividing by the
353
+ whole tank's heat capacity rather than the upper zone's - three
354
+ times too large.)*
355
+
356
+ They are also easy to lose or to misread. ``currentPower`` samples
357
+ at a median 30 s, with 24 % of gaps over a minute, so **a
358
+ one-minute-resampled power series can miss a sub-minute burst
359
+ entirely** - taking the last sample at or before each minute makes a
360
+ real 5.5 kW element look like 0.5 kW. Resample power with a
361
+ **maximum** over the interval, or work from the raw edges, and
362
+ corroborate a run's duration rather than a single aligned sample.
363
+
364
+ For predicting recovery, filter on **duration or energy**, not on
365
+ the flag and not on one power reading: it is the sustained runs that
366
+ heat the tank.
367
+
368
+
369
+ Electric: the handover, and what happens to the upper zone
370
+ ===========================================================
371
+
372
+ The protocol reference states that elements do not operate
373
+ simultaneously in Electric mode. Confirmed, and the order is upper
374
+ first: in a six-minute run the upper element stopped and the lower one
375
+ appeared in the next poll, with no overlap.
376
+
377
+ Two details a consumer modelling the tank should know:
378
+
379
+ **The upper probe falls while the lower element runs.** It peaked at
380
+ 63.3 degC (145.9 degF), fell to 61.4 (142.5) over the following three
381
+ minutes, then recovered to 62.3 (144.1) as the tank mixed. Anything
382
+ that treats "upper zone reached X" as monotonic during an Electric
383
+ recovery will be wrong by about 1.9 degC for several minutes.
384
+
385
+ **The off point lags in both directions.** The upper element switched
386
+ off with the probe 0.8 degC (1.4 degF) *below* the setpoint, and the
387
+ probe then coasted 1.3 degC (2.3 degF) *past* it. The device's decision
388
+ and the probe are not the same instant.
389
+
390
+
391
+ Element power
392
+ =============
393
+
394
+ Metered at the whole-unit power reading with the compressor off, so it
395
+ is the element alone:
396
+
397
+ * upper element: 5,219 W on one run (5,205-5,242 over 33 samples) and
398
+ 5,242 W on a second
399
+ * lower element: 5,200 W
400
+
401
+ The reference gives 3,755 W at 208 V or 5,000 W at 240 V. The measured
402
+ figure is 4 % above the 240 V rating on this unit, which is one unit at
403
+ one supply voltage - a nudge for anyone integrating energy, not a
404
+ correction to the specification.
405
+
406
+ For the record, the upper zone rose at 0.87-0.89 degC/min
407
+ (1.56-1.61 degF/min) end to end over 5.8 minutes on a 63.2 gal tank -
408
+ the spread is sampling resolution, the higher figure from 10-second
409
+ polls and the lower from a one-minute grid - and the lower zone at
410
+ 0.66-0.86 degC/min depending on where the fit starts; the probe lags the
411
+ element for the first 40-60 seconds, which is most of the difference.
412
+ Those are tank-specific and are offered as an order of magnitude only.
413
+
414
+
415
+ Writing the mode while a TOU window is in force
416
+ ===============================================
417
+
418
+ Relevant when planning any of this: with TOU scheduling enabled and a
419
+ window in force, a mode write may not take effect. Measured on the same
420
+ unit, four minutes with no change and nothing logged, twice - and the
421
+ command was not discarded either: it took effect one poll after the TOU
422
+ switch was turned off, with nothing else commanded.
423
+
424
+ Whether the device queues the command or accepts it and masks the
425
+ read-back is undetermined. Either way, **confirm a mode write by reading
426
+ the mode back, and do not treat an unconfirmed one as failed.**
427
+
428
+ Reservation entries are a separate case. Their target temperature
429
+ applies normally in a window, and only their mode is held back; see
430
+ :ref:`reservations and mode writes during a tou window`.
431
+
432
+
433
+ Limits
434
+ ======
435
+
436
+ One unit, one supply voltage, 2026-09-19 and 2026-09-20. **Twenty-two**
437
+ bounded runs in all: thirteen on the elements and the thresholds, nine
438
+ on mode writes and reservation entries under TOU. Specifically **not**
439
+ established:
440
+
441
+ * **where inside (0.2, 0.7] degC ``HIGH_DEMAND``'s boundary falls.** It
442
+ has a run on each side, so it is bracketed - but that bracket is more
443
+ than twice Electric's width, and one run at 0.4 degC would halve it.
444
+ Until then, whether the two modes share a threshold is unresolved.
445
+ * **Electric's** behaviour later in a stint. Energy Saver's and High
446
+ Demand's are described above from history; Electric has only two
447
+ mid-stint starts on record, which characterises nothing.
448
+ * anything about runs longer than six minutes.
449
+ * whether either differential is firmware-dependent. The unit reported
450
+ ``heUpperOnDiffTempSetting`` as 0 throughout; if another unit reports
451
+ it as non-zero, that field is the better answer and this page is a
452
+ description of one firmware's default.
@@ -9,6 +9,8 @@ Understanding-oriented deep dives into the library's design and advanced feature
9
9
 
10
10
  advanced-features
11
11
  architecture
12
+ heating-elements
12
13
  tank-energy
13
14
  tou-recovery-cap
14
15
  unknown-values
16
+ what-starts-a-recovery