nwp500-python 9.2.1__tar.gz → 9.3.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 (227) hide show
  1. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/.gitignore +2 -0
  2. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/CHANGELOG.rst +164 -0
  3. {nwp500_python-9.2.1/src/nwp500_python.egg-info → nwp500_python-9.3.0}/PKG-INFO +1 -1
  4. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/explanation/index.rst +2 -0
  5. nwp500_python-9.3.0/docs/explanation/tank-energy.rst +439 -0
  6. nwp500_python-9.3.0/docs/explanation/unknown-values.rst +259 -0
  7. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/how-to/manage-units.rst +4 -4
  8. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/how-to/track-energy.rst +44 -30
  9. nwp500_python-9.3.0/docs/project/authors.rst +2 -0
  10. nwp500_python-9.3.0/docs/project/changelog.rst +2 -0
  11. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/project/history.rst +1 -1
  12. nwp500_python-9.3.0/docs/project/license.rst +8 -0
  13. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/index.rst +1 -0
  14. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/installation.rst +1 -1
  15. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/protocol/data_conversions.rst +18 -5
  16. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/protocol/device_status.rst +5 -5
  17. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/python_api/models.rst +11 -3
  18. nwp500_python-9.3.0/examples/.ruff.toml +31 -0
  19. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/air_filter_reset.py +1 -1
  20. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/anti_legionella.py +4 -4
  21. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/combined_callbacks.py +7 -1
  22. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/demand_response.py +2 -2
  23. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/mqtt_diagnostics.py +8 -5
  24. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/power_control.py +2 -2
  25. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/recirculation_control.py +3 -3
  26. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/token_restoration.py +2 -1
  27. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/tou_schedule.py +1 -1
  28. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/water_reservation.py +1 -1
  29. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/beginner/02_list_devices.py +6 -4
  30. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/beginner/03_get_status.py +2 -3
  31. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/beginner/04_set_temperature.py +1 -1
  32. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/intermediate/event_driven_control.py +2 -1
  33. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/intermediate/periodic_requests.py +1 -1
  34. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/intermediate/set_mode.py +1 -1
  35. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/intermediate/vacation_mode.py +1 -1
  36. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/mask.py +1 -1
  37. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/testing/simple_periodic_info.py +2 -3
  38. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/cli/presentation.py +25 -6
  39. nwp500_python-9.3.0/src/nwp500/converters.py +242 -0
  40. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/enums.py +6 -0
  41. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/models/__init__.py +2 -2
  42. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/models/status.py +79 -16
  43. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt_events.py +1 -1
  44. {nwp500_python-9.2.1 → nwp500_python-9.3.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  45. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/SOURCES.txt +3 -0
  46. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/scm_file_list.json +3 -0
  47. nwp500_python-9.3.0/src/nwp500_python.egg-info/scm_version.json +8 -0
  48. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/conftest.py +4 -2
  49. nwp500_python-9.3.0/tests/test_cli_tristate_presentation.py +126 -0
  50. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_model_converters.py +120 -57
  51. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_models.py +161 -0
  52. nwp500_python-9.2.1/docs/project/authors.rst +0 -2
  53. nwp500_python-9.2.1/docs/project/changelog.rst +0 -2
  54. nwp500_python-9.2.1/docs/project/license.rst +0 -7
  55. nwp500_python-9.2.1/examples/.ruff.toml +0 -5
  56. nwp500_python-9.2.1/src/nwp500/converters.py +0 -154
  57. nwp500_python-9.2.1/src/nwp500_python.egg-info/scm_version.json +0 -8
  58. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/.coveragerc +0 -0
  59. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  60. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/.github/copilot-instructions.md +0 -0
  61. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/.github/workflows/ci.yml +0 -0
  62. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/.github/workflows/release.yml +0 -0
  63. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/.pre-commit-config.yaml +0 -0
  64. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/.readthedocs.yml +0 -0
  65. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/AUTHORS.rst +0 -0
  66. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/CONTRIBUTING.rst +0 -0
  67. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/LICENSE.txt +0 -0
  68. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/Makefile +0 -0
  69. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/README.rst +0 -0
  70. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/RELEASE.md +0 -0
  71. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/Makefile +0 -0
  72. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/_static/.gitignore +0 -0
  73. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/conf.py +0 -0
  74. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/explanation/advanced-features.rst +0 -0
  75. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/explanation/architecture.rst +0 -0
  76. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/how-to/authenticate.rst +0 -0
  77. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/how-to/auto-recovery.rst +0 -0
  78. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/how-to/diagnose-mqtt.rst +0 -0
  79. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/how-to/home-assistant.rst +0 -0
  80. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/how-to/index.rst +0 -0
  81. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/how-to/maintenance.rst +0 -0
  82. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/how-to/monitor-status.rst +0 -0
  83. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/how-to/optimize-tou.rst +0 -0
  84. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/how-to/queue-commands.rst +0 -0
  85. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/how-to/schedule-operation.rst +0 -0
  86. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/index.rst +0 -0
  87. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/openapi.yaml +0 -0
  88. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/project/contributing.rst +0 -0
  89. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/configuration.rst +0 -0
  90. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/enumerations.rst +0 -0
  91. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/protocol/device_features.rst +0 -0
  92. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/protocol/error_codes.rst +0 -0
  93. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  94. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/protocol/quick_reference.rst +0 -0
  95. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/protocol/rest_api.rst +0 -0
  96. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/python_api/api_client.rst +0 -0
  97. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/python_api/auth_client.rst +0 -0
  98. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/python_api/cli.rst +0 -0
  99. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/python_api/events.rst +0 -0
  100. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/python_api/exceptions.rst +0 -0
  101. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/reference/python_api/mqtt_client.rst +0 -0
  102. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/requirements.txt +0 -0
  103. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/docs/tutorials/getting-started.rst +0 -0
  104. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/README.md +0 -0
  105. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/auto_recovery.py +0 -0
  106. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/device_capabilities.py +0 -0
  107. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/device_status_debug.py +0 -0
  108. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/energy_analytics.py +0 -0
  109. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/error_code_demo.py +0 -0
  110. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/firmware_payload_capture.py +0 -0
  111. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/reconnection_demo.py +0 -0
  112. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/reservation_schedule.py +0 -0
  113. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/simple_auto_recovery.py +0 -0
  114. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/advanced/tou_openei.py +0 -0
  115. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/beginner/01_authentication.py +0 -0
  116. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
  117. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/intermediate/command_queue.py +0 -0
  118. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/intermediate/device_status_callback.py +0 -0
  119. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/intermediate/error_handling.py +0 -0
  120. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/intermediate/improved_auth.py +0 -0
  121. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
  122. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  123. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/testing/periodic_device_info.py +0 -0
  124. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/testing/test_api_client.py +0 -0
  125. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/testing/test_mqtt_connection.py +0 -0
  126. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/testing/test_mqtt_messaging.py +0 -0
  127. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/examples/testing/test_periodic_minimal.py +0 -0
  128. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/pyproject.toml +0 -0
  129. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/scripts/README.md +0 -0
  130. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/scripts/bump_version.py +0 -0
  131. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/scripts/diagnose_mqtt_connection.py +0 -0
  132. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/scripts/extract_changelog.py +0 -0
  133. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/scripts/format.py +0 -0
  134. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/scripts/lint.py +0 -0
  135. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/scripts/setup-dev.py +0 -0
  136. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/scripts/validate_version.py +0 -0
  137. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/setup.cfg +0 -0
  138. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/setup.py +0 -0
  139. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/__init__.py +0 -0
  140. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/_base.py +0 -0
  141. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/api_client.py +0 -0
  142. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/auth.py +0 -0
  143. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/cli/__init__.py +0 -0
  144. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/cli/__main__.py +0 -0
  145. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/cli/handlers.py +0 -0
  146. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/cli/monitoring.py +0 -0
  147. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/cli/output_formatters.py +0 -0
  148. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/cli/rich_output.py +0 -0
  149. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/cli/token_storage.py +0 -0
  150. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/command_decorators.py +0 -0
  151. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/config.py +0 -0
  152. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/device_capabilities.py +0 -0
  153. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/device_info_cache.py +0 -0
  154. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/encoding.py +0 -0
  155. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/events.py +0 -0
  156. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/exceptions.py +0 -0
  157. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/factory.py +0 -0
  158. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/field_factory.py +0 -0
  159. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/models/_converters.py +0 -0
  160. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/models/device.py +0 -0
  161. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/models/energy.py +0 -0
  162. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/models/feature.py +0 -0
  163. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/models/mqtt_models.py +0 -0
  164. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/models/schedule.py +0 -0
  165. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/models/tou.py +0 -0
  166. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/__init__.py +0 -0
  167. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/_control_commands.py +0 -0
  168. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
  169. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/client.py +0 -0
  170. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/command_queue.py +0 -0
  171. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/connection.py +0 -0
  172. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/control.py +0 -0
  173. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/diagnostics.py +0 -0
  174. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/periodic.py +0 -0
  175. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/reconnection.py +0 -0
  176. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/state_tracker.py +0 -0
  177. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/subscriptions.py +0 -0
  178. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/types.py +0 -0
  179. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/mqtt/utils.py +0 -0
  180. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/openei.py +0 -0
  181. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/py.typed +0 -0
  182. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/reservations.py +0 -0
  183. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/temperature.py +0 -0
  184. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/topic_builder.py +0 -0
  185. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/tou_schedule.py +0 -0
  186. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/unit_system.py +0 -0
  187. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500/utils.py +0 -0
  188. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  189. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  190. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  191. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/requires.txt +0 -0
  192. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
  193. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_api_helpers.py +0 -0
  194. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_auth.py +0 -0
  195. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_auth_session_lifecycle.py +0 -0
  196. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_bug_fixes.py +0 -0
  197. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_canonical_schedule.py +0 -0
  198. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_cli_basic.py +0 -0
  199. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_cli_commands.py +0 -0
  200. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_command_decorators.py +0 -0
  201. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_command_queue.py +0 -0
  202. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_device_capabilities.py +0 -0
  203. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_device_info_cache.py +0 -0
  204. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_events.py +0 -0
  205. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_exceptions.py +0 -0
  206. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_mqtt_clean_session_resume.py +0 -0
  207. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_mqtt_client_init.py +0 -0
  208. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_mqtt_events.py +0 -0
  209. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_mqtt_hypothesis.py +0 -0
  210. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_mqtt_reconnection.py +0 -0
  211. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_mqtt_reconnection_storm.py +0 -0
  212. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_mqtt_reliability.py +0 -0
  213. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_multi_device.py +0 -0
  214. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_openei.py +0 -0
  215. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_protocol_correctness.py +0 -0
  216. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_public_api.py +0 -0
  217. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_reservations.py +0 -0
  218. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_temperature_converters.py +0 -0
  219. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_threading_model.py +0 -0
  220. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_token_storage.py +0 -0
  221. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_tou_api.py +0 -0
  222. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_tou_schedule.py +0 -0
  223. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_unit_switching.py +0 -0
  224. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_unit_system_process_wide.py +0 -0
  225. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_utility_modules.py +0 -0
  226. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tests/test_utils.py +0 -0
  227. {nwp500_python-9.2.1 → nwp500_python-9.3.0}/tox.ini +0 -0
@@ -49,6 +49,8 @@ build/*
49
49
  dist/*
50
50
  sdist/*
51
51
  docs/api/*
52
+ docs/reference/api/*
53
+ .obsidian/
52
54
  docs/_rst/*
53
55
  docs/_build/*
54
56
  cover/*
@@ -5,6 +5,170 @@ Changelog
5
5
  Unreleased
6
6
  ==========
7
7
 
8
+ Version 9.3.0 (2026-08-03)
9
+ ==========================
10
+
11
+ **BREAKING CHANGES**: two independent corrections land together.
12
+
13
+ Tank energy values were wrong in two ways - a 2.5x unit-scale error and
14
+ two actively misleading field names - and both are corrected. Reported
15
+ tank energy is now 2.5x smaller and two public field names are removed.
16
+
17
+ Separately, eight status flags change type from ``bool`` to
18
+ ``bool | None`` so the device's "unknown" state is no longer reported as
19
+ a definite OFF.
20
+
21
+ Changed
22
+ -------
23
+ - **Energy unit scale corrected.** ``totalEnergyCapacity`` and ``availableEnergyCapacity`` were
24
+ scaled by 10 on the assumption the device reported 10 Wh units. It does
25
+ not. Because ``totalEnergyCapacity`` is a whole-tank quantity, its slope
26
+ against the setpoint measures the quantum with no stratification
27
+ assumption: on a 65-gallon NWP500 that is 70.25 raw counts per Kelvin.
28
+ The field turns out to be bimodal - at a fixed setpoint it takes one of
29
+ two values exactly 2 degC apart - but both branches give the same slope
30
+ to within 0.2%, so the quantum is unaffected. Converting to Watt-hours needs a
31
+ water mass, and a "65 gallon" tank does not hold 65 gallons - so taking
32
+ the quantum to be round, as every other conversion in this protocol is,
33
+ 4 Wh/count is the only candidate implying a water volume below the
34
+ nameplate (241.7 L). Two further checks agree: 183 individual heating
35
+ recoveries give 4.11 Wh/count by a noisier route, and integrating
36
+ ``currentInstPower`` over them implies a heat-pump COP of 2.89 at the new
37
+ scale against 7.02 at the old, the latter being physically impossible.
38
+ **Reported tank energy is now 2.5x smaller.**
39
+ Historical series logged from earlier versions need rescaling by 0.4 to
40
+ be comparable.
41
+
42
+ - **Energy fields renamed.**
43
+ ``availableEnergyCapacity`` is not available energy - it is the energy
44
+ still *needed* to reach the setpoint. It falls as the tank heats and
45
+ reaches zero when the tank is fully charged, so code treating it as
46
+ stored energy had the signal backwards (regression against mean tank
47
+ temperature: negative slope, R-squared 0.93, zero crossing at the
48
+ setpoint). Likewise ``totalEnergyCapacity`` is not a fixed tank size but
49
+ the cost of a full recovery to the *current setpoint*, measured from the
50
+ device's own minimum setpoint of 104.9 degF; it moves by about 140 Wh
51
+ per 0.5 degC of setpoint change.
52
+
53
+ .. code-block:: python
54
+
55
+ # OLD (removed)
56
+ status.total_energy_capacity # 15800.0
57
+ status.available_energy_capacity # 11660.0
58
+
59
+ # NEW
60
+ status.full_recovery_energy # 6320.0
61
+ status.energy_to_setpoint # 4664.0
62
+
63
+ The protocol field names on the wire are unchanged. CLI rows are
64
+ relabelled from "Total Capacity"/"Available Capacity" to
65
+ "Full Recovery"/"Energy to Setpoint".
66
+
67
+ - **Status flags now preserve the device's unknown state.** The protocol
68
+ encodes these flags as ``0 = unknown, 1 = OFF, 2 = ON``, and the library
69
+ was collapsing 0 to ``False`` - inventing an OFF the device never claimed.
70
+ Confirmed against Navien's own NaviLink app (2.03.00, versionCode 141),
71
+ which decodes exactly this set of fields through an enum declared
72
+ ``UNKNOWN(0), OFF(1), ON(2)``; two sibling enums render their zero as
73
+ ``"-"`` and ``"Not Applied"`` rather than as an off state.
74
+
75
+ Affected: ``operation_busy``, ``comp_use``, ``anti_legionella_use``,
76
+ ``anti_legionella_operation_busy``, ``heat_upper_use``, ``heat_lower_use``,
77
+ ``air_filter_alarm_use``, ``recirc_reservation_use``.
78
+
79
+ ``None`` is falsy, so ``if status.comp_use:`` is unaffected. Code that
80
+ distinguishes ``is False`` from "not reported", or does arithmetic or
81
+ formatting on these fields, needs a ``None`` check. For Home Assistant
82
+ this is the wanted shape: ``None`` renders as "Unknown" instead of writing
83
+ a fabricated OFF into the recorder database.
84
+
85
+ - ``OnOffFlag`` gains the vendor's ``UNKNOWN = 0`` member. It previously
86
+ started at ``OFF = 1``, leaving the device's reserved value unrepresented.
87
+
88
+ - The CLI renders these flags as ``Unknown`` rather than ``No``. Both
89
+ affected rows are updated: "Busy" under OPERATION STATUS and
90
+ "Operation Busy" under ANTI-LEGIONELLA.
91
+
92
+ Added
93
+ -----
94
+ - **``DeviceStatus.usable_energy``**: drawable energy in Watt-hours,
95
+ computed as ``full_recovery_energy - energy_to_setpoint``. Both raw
96
+ fields are measured from the setpoint, so neither is a state of charge;
97
+ subtracting them cancels the setpoint and leaves the tank's heat above
98
+ the device's minimum operating temperature (104.9 degF), which is about
99
+ the lowest temperature usable for a shower. Robust despite
100
+ ``full_recovery_energy`` being bimodal, since both fields shift
101
+ together: the implied tank temperature tracks the thermistor mean to a
102
+ standard deviation of 0.57 degF over 12275 samples. Rendered by the CLI
103
+ as "Usable Energy".
104
+
105
+ - ``converters.device_tristate_to_python`` and
106
+ ``models.status.DeviceTriState`` for flags the device may decline to
107
+ report. ``converters.device_bool_to_python`` is unchanged and remains
108
+ correct for capability flags.
109
+ - New ``docs/explanation/unknown-values.rst`` recording which field families
110
+ use 0 as a sentinel and which do not, with the app evidence for each.
111
+
112
+ Removed
113
+ -------
114
+ - **Misnamed energy fields**: removed ``DeviceStatus.total_energy_capacity``
115
+ and ``DeviceStatus.available_energy_capacity`` outright rather than
116
+ aliasing them, so a missed rename fails with ``AttributeError`` instead
117
+ of silently returning a number 2.5x too large. Use
118
+ ``full_recovery_energy`` and ``energy_to_setpoint``.
119
+
120
+ - **Incorrect converter**: removed ``converters.mul_10``, which existed
121
+ only to apply the wrong 10 Wh scale. Use
122
+ ``converters.energy_count_to_wh`` and ``converters.WH_PER_ENERGY_COUNT``.
123
+
124
+ Fixed
125
+ -----
126
+ - **Documentation contradicted itself and the code on energy capacity.**
127
+ Three incompatible descriptions were published: Watt-hours with no
128
+ conversion (protocol reference), Watt-hours with a x10 scale (the code),
129
+ and a 0-100 percentage (``how-to/track-energy.rst``,
130
+ ``reference/python_api/models.rst``, ``project/history.rst``). The
131
+ percentage claim was never true of any library version. All are now
132
+ consistent.
133
+ - ``how-to/track-energy.rst`` documented four fields that do not exist on
134
+ ``DeviceStatus`` (``dhw_tank_upper_temp``, ``dhw_tank_lower_temp``,
135
+ ``comp_temp``, ``dhw_heatex_out_temp``); replaced with the real names.
136
+ - ``dhwTemperature`` is documented as an outlet temperature but is
137
+ measured inside the tank: it tracks ``tankUpperTemperature`` to within
138
+ one 0.5 degC step, and the device has no sensor downstream of itself.
139
+ Navien's own app agrees, labelling it "DHW Temp." beside the tank
140
+ thermistors and showing ``dischargeTemperature`` separately.
141
+ - ``docs/explanation/tank-energy.rst`` tabulated ``totalEnergyCapacity``
142
+ and ``availableEnergyCapacity`` at ten times their raw wire values,
143
+ under column headings naming the raw protocol fields. The series had
144
+ been logged through the pre-fix library, which multiplied by 10. The
145
+ prose beside the tables ("70.25 raw counts per Kelvin", "35 counts")
146
+ was already in true raw counts, so the page contradicted itself by
147
+ exactly the factor it exists to correct. Tables are now in raw counts;
148
+ 1580 counts at a 145.4 degF setpoint matches the test fixtures and the
149
+ 6320 Wh the CLI reports. No numeric conclusion changes.
150
+ - ``docs/explanation/tank-energy.rst`` referred to a field
151
+ ``hpUpperOnTemperatureSetting``; the protocol name is
152
+ ``hpUpperOnTempSetting``.
153
+ - The setpoint-per-0.5-degC figure is 140 Wh, not 143 Wh, which is what
154
+ the documented slope of 70.25 counts/K gives at 4 Wh/count. Corrected
155
+ in the changelog, the model field description and the protocol
156
+ reference.
157
+ - New ``docs/explanation/tank-energy.rst`` records what the two fields
158
+ actually measure and the calibration evidence behind the scale change,
159
+ including that Navien's own NaviLink app (2.03.00, versionCode 141)
160
+ reads neither field, so no vendor-side corroboration exists.
161
+
162
+ - Documented that **temperature fields carry no sentinel at all**. The app
163
+ has no out-of-band constant (no ``0xFFFF``/``-999``/``-1``), no zero-guard
164
+ in any display path, and formats whatever arrives - so a temperature of
165
+ zero means zero. This closes a recurring source of bugs where zero-as-none
166
+ was applied to temperature converters and reported working sensors as
167
+ missing during cold-weather operation.
168
+ - Documented that capability flags are a distinct case: the app hides a
169
+ feature's entire UI when its DID ``Use`` flag reads 0, so 0 there means
170
+ "not fitted" and the existing ``bool`` mapping is correct.
171
+
8
172
  Version 9.2.1 (2026-07-30)
9
173
  ==========================
10
174
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.2.1
3
+ Version: 9.3.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
@@ -9,3 +9,5 @@ Understanding-oriented deep dives into the library's design and advanced feature
9
9
 
10
10
  advanced-features
11
11
  architecture
12
+ tank-energy
13
+ unknown-values
@@ -0,0 +1,439 @@
1
+ ===========
2
+ Tank Energy
3
+ ===========
4
+
5
+ The NWP500 reports two energy figures, ``totalEnergyCapacity`` and
6
+ ``availableEnergyCapacity``. Both names mislead, and before v10.0 this
7
+ library also scaled them wrongly. This page explains what they
8
+ actually measure and shows the evidence for the correction.
9
+
10
+ .. contents::
11
+ :local:
12
+ :depth: 2
13
+
14
+
15
+ The short version
16
+ =================
17
+
18
+ .. list-table::
19
+ :header-rows: 1
20
+ :widths: 22 30 48
21
+
22
+ * - Protocol field
23
+ - What the name suggests
24
+ - What it actually is
25
+ * - ``availableEnergyCapacity``
26
+ - Energy available in the tank
27
+ - Energy still **needed** to reach the setpoint. It *falls* as the
28
+ tank heats and hits zero when the tank is fully charged - the
29
+ exact inverse of the name.
30
+ * - ``totalEnergyCapacity``
31
+ - Fixed tank capacity
32
+ - Cost of a full recovery to the **current setpoint**. It moves
33
+ whenever the setpoint moves.
34
+
35
+ Both are raw counts of 4 Wh each, not Watt-hours. Library versions before
36
+ 10.0 multiplied by 10, overstating tank energy by 2.5x.
37
+
38
+ Both are also measured **from the setpoint**, so both describe potential
39
+ rather than content: move the setpoint and both change while the water in
40
+ the tank does not. Neither is a state of charge.
41
+
42
+ Their **difference** is a state of charge, and is exposed as
43
+ ``DeviceStatus.usable_energy``:
44
+
45
+ .. code:: text
46
+
47
+ usable_energy = full_recovery_energy - energy_to_setpoint
48
+ = k * (tank_temperature - 104.9 degF)
49
+
50
+ The setpoint cancels. What remains is the tank's heat above the device's
51
+ minimum operating temperature - close enough to the lowest useful shower
52
+ temperature that it is a good estimate of what you can actually draw.
53
+
54
+
55
+ How the fields behave
56
+ =====================
57
+
58
+ Both fields fit a single two-parameter model:
59
+
60
+ .. code:: text
61
+
62
+ energy_to_setpoint = k * (setpoint - tank_mean_temperature)
63
+ full_recovery_energy = k * (setpoint - reference_temperature)
64
+
65
+ where ``k`` is the tank's heat capacity and ``reference_temperature`` is
66
+ the device's own minimum setpoint, ``dhwTemperatureMin`` (40.5 degC /
67
+ 104.9 degF) - though only about two thirds of the time, see
68
+ `Two branches`_.
69
+
70
+ Two consequences follow, and both matter:
71
+
72
+ * ``energy_to_setpoint`` is a **deficit**. Code that treats it as stored
73
+ energy has the signal backwards: it is largest when the tank is
74
+ coldest.
75
+ * ``full_recovery_energy`` is **not a constant**. Raising the setpoint by
76
+ 0.5 degC raises it by 35 counts, about 140 Wh, on a 65-gallon tank. Seven months of
77
+ history on one device shows sixteen distinct values as the setpoint was
78
+ adjusted.
79
+
80
+
81
+ The evidence
82
+ ============
83
+
84
+ Deficit, not stored energy
85
+ --------------------------
86
+
87
+ During a heating recovery on a 65-gallon unit at a 140.9 degF setpoint,
88
+ with the tank warming and no draws. Each row is a mean over the samples
89
+ in that temperature bin, so the counts are not whole numbers:
90
+
91
+ .. list-table::
92
+ :header-rows: 1
93
+
94
+ * - Mean tank temp
95
+ - Setpoint minus tank
96
+ - mean ``availableEnergyCapacity``
97
+ - ``dhwChargePer``
98
+ * - 119.5 degF
99
+ - 21.4 degF
100
+ - 881.6
101
+ - 54.5 %
102
+ * - 127.4 degF
103
+ - 13.5 degF
104
+ - 530.0
105
+ - 69.5 %
106
+ * - 135.1 degF
107
+ - 5.8 degF
108
+ - 227.5
109
+ - 85.7 %
110
+
111
+ The field falls as the tank fills with heat. Regressed against mean tank
112
+ temperature over two weeks of five-minute samples, the slope is negative
113
+ with an R-squared of 0.93 and a zero crossing at the setpoint.
114
+
115
+ The last two rows also check out against the tank's heat capacity of
116
+ 156 Wh/degF: 227.5 counts x 4 Wh = 910 Wh against 5.8 degF x 156 =
117
+ 905 Wh, and 530.0 counts = 2120 Wh against 13.5 degF x 156 = 2106 Wh.
118
+ The coldest row runs about 6 % high, which is the stratification error
119
+ in using ``(upper + lower) / 2`` as the mean tank temperature - it is
120
+ worst when the tank is least mixed.
121
+
122
+ The 4 Wh quantum
123
+ ----------------
124
+
125
+ ``totalEnergyCapacity`` is a whole-tank quantity, so its slope against
126
+ the setpoint measures the quantum without needing any assumption about
127
+ how the tank stratifies.
128
+
129
+ The device does not report a single ``totalEnergyCapacity`` per setpoint -
130
+ see `Two branches`_ below - so the table lists the most common value at
131
+ each setpoint, which covers 68 % of samples. Values are raw counts as
132
+ they arrive on the wire:
133
+
134
+ .. list-table::
135
+ :header-rows: 1
136
+
137
+ * - Setpoint
138
+ - ``totalEnergyCapacity``
139
+ - Setpoint
140
+ - ``totalEnergyCapacity``
141
+ * - 140.0 degF
142
+ - 1369
143
+ - 144.5 degF
144
+ - 1545
145
+ * - 140.9 degF
146
+ - 1404
147
+ - 145.4 degF
148
+ - 1580
149
+ * - 141.8 degF
150
+ - 1439
151
+ - 146.3 degF
152
+ - 1615
153
+ * - 142.7 degF
154
+ - 1475
155
+ - 147.2 degF
156
+ - 1650
157
+ * - 143.6 degF
158
+ - 1510
159
+ - 148.1 degF
160
+ - 1685
161
+
162
+ An arithmetic sequence: least squares gives **R-squared 0.99999** and a
163
+ slope of **70.25 raw counts per Kelvin** of whole-tank temperature rise.
164
+ The endpoints alone give the same figure: (1685 - 1369) / 4.5 K = 70.2.
165
+
166
+ The slope is the robust part of this. The second branch, fitted
167
+ separately, gives 38.98 counts/degF against the primary's 39.06 - the
168
+ same figure to within 0.2 %. Two independent populations agreeing on the
169
+ slope is stronger evidence for the quantum than either alone.
170
+
171
+ Converting that to Watt-hours needs a water mass, and this is where care
172
+ is required: a "65 gallon" tank does not hold 65 gallons of water. The
173
+ nameplate is an upper bound - the vendor's own app hard-codes it, mapping
174
+ ``volumeCode`` 1/2/3 to 189.2 L, **246.0 L** and 302.8 L (see `What the
175
+ vendor app does with these fields`_) - and the water actually in the tank
176
+ must come in under it. Rather than assume nominal volume and derive an
177
+ odd-looking quantum, assume the quantum is a round number - every other
178
+ conversion in this protocol is (half-degrees, tenths) - and see which one
179
+ implies a sensible volume:
180
+
181
+ .. list-table::
182
+ :header-rows: 1
183
+
184
+ * - Candidate quantum
185
+ - Implied water volume
186
+ - Plausible?
187
+ * - **4 Wh** (1/250 kWh)
188
+ - **241.7 L / 63.9 gal**
189
+ - Yes - slightly under nominal, as expected
190
+ * - 1/240 kWh (4.167 Wh)
191
+ - 251.8 L / 66.5 gal
192
+ - No - more than the nameplate
193
+ * - 10 kJ
194
+ - 167.8 L / 44.3 gal
195
+ - No
196
+ * - 15 kJ
197
+ - 251.7 L / 66.5 gal
198
+ - No
199
+
200
+ **4 Wh per count** is the only round candidate implying a volume below
201
+ the nameplate, which is the only physically sensible direction. The
202
+ library uses 4.0.
203
+
204
+ A second, noisier method agrees. Across 183 individual heating
205
+ recoveries, dividing each tank sensible-heat gain (from the two
206
+ thermistors and the nominal mass) by the device's reported change gives a
207
+ median of 4.11 Wh/count, p10 3.47 and p90 4.45. That route depends on
208
+ ``(upper + lower) / 2`` approximating the true mean tank temperature, so
209
+ it is far less precise, but it is an independent confirmation and it does
210
+ not use electrical input at all.
211
+
212
+ The efficiency cross-check
213
+ --------------------------
214
+
215
+ A third check rules out the old scale on its own. Integrating
216
+ ``currentInstPower`` over each recovery gives the electrical energy in,
217
+ and dividing the device's *reported* energy gain by it gives an implied
218
+ coefficient of performance:
219
+
220
+ .. list-table::
221
+ :header-rows: 1
222
+
223
+ * - Scale used
224
+ - Implied COP (median)
225
+ - Verdict
226
+ * - 4 Wh/count (corrected)
227
+ - 2.89
228
+ - Normal for a heat pump water heater
229
+ * - 10 Wh/count (pre-10.0)
230
+ - 7.02
231
+ - Physically impossible
232
+
233
+ A heat pump water heater in a 72 degF room runs at a COP of roughly 2 to
234
+ 4. A COP of 7 would mean the device generated energy it never consumed.
235
+
236
+ This argument is worth stating separately because it needs no tank
237
+ volume, specific heat or stratification model - only the device's own
238
+ reported energy and its own reported power. It cannot tell you what the
239
+ quantum *is*, but it rules out the pre-10.0 value regardless of anything
240
+ assumed elsewhere on this page.
241
+
242
+ .. _two branches:
243
+
244
+ Two branches
245
+ ------------
246
+
247
+ ``totalEnergyCapacity`` is **not** a function of the setpoint alone. At a
248
+ fixed setpoint it takes one of two values, flipping between them several
249
+ times a day. Over four months at nine setpoints:
250
+
251
+ .. list-table::
252
+ :header-rows: 1
253
+
254
+ * - Branch
255
+ - Share
256
+ - Slope
257
+ - Zero crossing
258
+ * - Primary
259
+ - 68 %
260
+ - 39.06 counts/degF
261
+ - **104.95 degF**
262
+ * - Secondary
263
+ - 32 %
264
+ - 38.98 counts/degF
265
+ - **108.48 degF**
266
+
267
+ The two are parallel, separated by a constant 140-141 counts - exactly
268
+ **2 degC** of setpoint - at every setpoint measured.
269
+
270
+ The primary branch's zero crossing matches the device's
271
+ ``dhwTemperatureMin`` of 104.9 degF to within a twentieth of a degree -
272
+ 140.0 - 1369 / 39.06 = 104.95 degF - using only the device's own two
273
+ numbers: no tank mass, no specific heat, no thermistors, no assumption
274
+ about the quantum. On that branch,
275
+
276
+ .. code:: text
277
+
278
+ full_recovery_energy = k * (setpoint - dhwTemperatureMin)
279
+
280
+ The secondary branch behaves identically with a reference 2 degC higher,
281
+ and **what selects between them is unknown**. The device's
282
+ ``hpUpperOnTempSetting`` correlates with the choice - 104.9 degF
283
+ when the primary is active, 143.4 degF when the secondary is - which
284
+ would fit the device computing recovery cost from its own turn-on
285
+ threshold, but only 22 paired samples were available and that is a lead
286
+ rather than a finding.
287
+
288
+ .. warning::
289
+ Because of this, do not derive the tank's heat capacity from a live
290
+ ``full_recovery_energy`` reading: landing on the wrong branch gives an
291
+ error of about 9 %. Use the slope, which is stable across both
292
+ branches, or compute stored energy from the thermistors directly.
293
+
294
+
295
+ What ``dhwChargePer`` does
296
+ ==========================
297
+
298
+ ``dhwChargePer`` is a fourth signal and does not reconcile with the other
299
+ two. On one device, ``energy_to_setpoint / full_recovery_energy``
300
+ implies 30 % charged while ``dhwChargePer`` reads 59 %; over two weeks
301
+ the two differ by a mean of 48 points with 32 points of scatter.
302
+
303
+ It is nonetheless the number Navien shows its own users: the NaviLink app
304
+ prints it unmodified as a percentage labelled "DHW Charge", with no
305
+ client-side arithmetic of any kind. Whatever it means, it is computed on
306
+ the device, and a user comparing the app against this library will see
307
+ the app's figure and not the ratio above.
308
+
309
+ Treat it as an opaque vendor heuristic rather than a defined fraction of
310
+ anything. For a charge figure with defined meaning, use
311
+ ``usable_energy``.
312
+
313
+
314
+ .. _what the vendor app does with these fields:
315
+
316
+ What the vendor app does with these fields
317
+ ==========================================
318
+
319
+ Nothing. Navien's own NaviLink app never reads either field.
320
+
321
+ Decompiling the current release - version 2.03.00, versionCode 141,
322
+ published March 2026 - gives 8,101 Java sources, and neither
323
+ ``totalEnergyCapacity`` nor ``availableEnergyCapacity`` appears in any of
324
+ them. Neither string appears in the raw dex string pool either, which
325
+ rules out the names having been lost to obfuscation. The app's status
326
+ model, ``KDResponseMgppStatus.Status``, declares about 140 fields -
327
+ including ``dhwChargePer``, ``tankUpperTemperature``,
328
+ ``tankLowerTemperature``, ``currentInstPower`` and ``mixingRate`` - and
329
+ neither energy field is among them. The app requests no field subset, so
330
+ the device sends both and the app discards them on deserialization.
331
+
332
+ As a control, ``dhwChargePer`` and ``tankUpperTemperature`` *are* present
333
+ in the dex strings, so the absence of the other two is a real result and
334
+ not a broken search.
335
+
336
+ This matters for reading the rest of this page. There is no vendor label,
337
+ no vendor scale factor and no vendor formula to check the conclusions
338
+ above against - the evidence here is the only account of these two fields
339
+ that exists. It also explains why the protocol names are so misleading:
340
+ nothing Navien ships ever has to act on them.
341
+
342
+ The app does corroborate the surrounding facts this page leans on:
343
+
344
+ .. list-table::
345
+ :header-rows: 1
346
+ :widths: 40 60
347
+
348
+ * - What the app does
349
+ - What it confirms
350
+ * - ``MgppStatusFragment.getVolume()`` maps ``volumeCode`` 1/2/3 to
351
+ "50"/"65"/"80" gallons and 189.2/246.0/302.8 L, displayed under
352
+ the label "Volume"
353
+ - The nominal volume the quantum candidates are judged against is
354
+ the vendor's own figure, not an assumption of ours
355
+ * - ``MgppControlFragment.makeTempMap()`` reads
356
+ ``dhwTemperatureMin / 2.0f`` as degC before converting
357
+ - ``dhwTemperatureMin`` is in half-degrees C, so 40.5 degC /
358
+ 104.9 degF is the exact value the regression lands on. The app
359
+ rounds for display and shows 105 degF
360
+ * - The status screen labels ``dhwTemperature`` "DHW Temp." next to
361
+ "Upper Temp." and "Lower Temp.", and gives ``dischargeTemperature``
362
+ a separate "Discharge Temp." row
363
+ - The vendor UI does not treat ``dhwTemperature`` as an outlet
364
+ reading, and reserves a different field for what leaves the unit
365
+
366
+
367
+ Drawable energy
368
+ ===============
369
+
370
+ ``DeviceStatus.usable_energy`` is the difference of the two fields, and
371
+ is the one number here that describes the tank's state rather than its
372
+ distance from a target:
373
+
374
+ .. code:: text
375
+
376
+ usable_energy = full_recovery_energy - energy_to_setpoint
377
+
378
+ Raising the setpoint inflates both inputs equally, so the result does not
379
+ move - which is what makes it a state of charge and the two raw fields
380
+ not.
381
+
382
+ The implied reference is ``dhw_temperature_min``, 104.9 degF. A shower
383
+ runs around 105 degF, so heat below that reference is real but not
384
+ useful, and excluding it is the behaviour you want. Note that a mixing
385
+ valve does not change this floor: it caps how *hot* water can be
386
+ delivered, and once the tank falls below its setting it simply passes
387
+ through, so water stays usable down to the temperature you actually want
388
+ at the tap.
389
+
390
+ Despite ``full_recovery_energy`` being bimodal (see `Two branches`_),
391
+ the difference is robust, because both fields shift together. Checked
392
+ against the tank thermistors over 12275 samples, the tank temperature
393
+ implied by ``usable_energy`` agrees with the thermistor mean to a
394
+ standard deviation of **0.57 degF**, with 97.5 % of samples inside
395
+ 2 degF.
396
+
397
+ If you need a different floor - a bath at 100 degF, or energy above the
398
+ cold inlet - compute it from the thermistors instead. On a 65-gallon tank
399
+ the heat capacity is 156 Wh per degF:
400
+
401
+ .. code:: text
402
+
403
+ drawable_Wh = 156 * (tank_mean_temperature - your_floor_degF)
404
+
405
+
406
+ Migrating from before v10.0
407
+ ===========================
408
+
409
+ .. list-table::
410
+ :header-rows: 1
411
+ :widths: 40 60
412
+
413
+ * - Old
414
+ - New
415
+ * - ``status.total_energy_capacity`` (removed)
416
+ - ``status.full_recovery_energy`` - and the value is 2.5x smaller
417
+ * - ``status.available_energy_capacity`` (removed)
418
+ - ``status.energy_to_setpoint`` - 2.5x smaller, and note it was
419
+ never "available"
420
+ * - Treating the value as available energy
421
+ - It is a deficit; invert the logic. ``full_recovery_energy -
422
+ energy_to_setpoint`` is the energy added above the device's
423
+ reference temperature
424
+ * - Treating the value as a percentage
425
+ - It never was one
426
+
427
+ The old attribute names are removed rather than aliased, so a
428
+ rename that is missed fails immediately with ``AttributeError`` instead
429
+ of silently returning a number 2.5x too large.
430
+
431
+ If you logged these values historically, the stored series needs
432
+ rescaling by 0.4 to be comparable with values from v10.0 onward.
433
+
434
+
435
+ See also
436
+ ========
437
+
438
+ - :doc:`../how-to/track-energy` - Monitoring energy and power
439
+ - :doc:`../reference/protocol/data_conversions` - Protocol field conversions