nwp500-python 9.4.0__tar.gz → 9.4.2__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 (228) hide show
  1. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/CHANGELOG.rst +30 -0
  2. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/Makefile +13 -5
  3. {nwp500_python-9.4.0/src/nwp500_python.egg-info → nwp500_python-9.4.2}/PKG-INFO +1 -1
  4. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/explanation/tank-energy.rst +7 -7
  5. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/data_conversions.rst +1 -1
  6. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/models.rst +1 -1
  7. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/converters.py +1 -1
  8. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/reconnection.py +7 -0
  9. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/subscriptions.py +38 -1
  10. {nwp500_python-9.4.0 → nwp500_python-9.4.2/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  11. nwp500_python-9.4.2/src/nwp500_python.egg-info/scm_version.json +8 -0
  12. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_mqtt_reliability.py +39 -0
  13. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_multi_device.py +149 -0
  14. nwp500_python-9.4.0/src/nwp500_python.egg-info/scm_version.json +0 -8
  15. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.coveragerc +0 -0
  16. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  17. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.github/copilot-instructions.md +0 -0
  18. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.github/workflows/ci.yml +0 -0
  19. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.github/workflows/release.yml +0 -0
  20. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.gitignore +0 -0
  21. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.pre-commit-config.yaml +0 -0
  22. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/.readthedocs.yml +0 -0
  23. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/AUTHORS.rst +0 -0
  24. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/CONTRIBUTING.rst +0 -0
  25. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/LICENSE.txt +0 -0
  26. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/README.rst +0 -0
  27. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/RELEASE.md +0 -0
  28. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/Makefile +0 -0
  29. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/_static/.gitignore +0 -0
  30. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/conf.py +0 -0
  31. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/explanation/advanced-features.rst +0 -0
  32. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/explanation/architecture.rst +0 -0
  33. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/explanation/index.rst +0 -0
  34. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/explanation/tou-recovery-cap.rst +0 -0
  35. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/explanation/unknown-values.rst +0 -0
  36. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/authenticate.rst +0 -0
  37. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/auto-recovery.rst +0 -0
  38. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/diagnose-mqtt.rst +0 -0
  39. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/home-assistant.rst +0 -0
  40. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/index.rst +0 -0
  41. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/maintenance.rst +0 -0
  42. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/manage-units.rst +0 -0
  43. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/monitor-status.rst +0 -0
  44. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/optimize-tou.rst +0 -0
  45. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/queue-commands.rst +0 -0
  46. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/schedule-operation.rst +0 -0
  47. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/how-to/track-energy.rst +0 -0
  48. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/index.rst +0 -0
  49. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/openapi.yaml +0 -0
  50. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/project/authors.rst +0 -0
  51. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/project/changelog.rst +0 -0
  52. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/project/contributing.rst +0 -0
  53. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/project/history.rst +0 -0
  54. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/project/license.rst +0 -0
  55. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/configuration.rst +0 -0
  56. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/enumerations.rst +0 -0
  57. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/index.rst +0 -0
  58. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/installation.rst +0 -0
  59. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/device_features.rst +0 -0
  60. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/device_status.rst +0 -0
  61. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/error_codes.rst +0 -0
  62. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  63. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/quick_reference.rst +0 -0
  64. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/protocol/rest_api.rst +0 -0
  65. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/api_client.rst +0 -0
  66. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/auth_client.rst +0 -0
  67. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/cli.rst +0 -0
  68. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/events.rst +0 -0
  69. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/exceptions.rst +0 -0
  70. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/reference/python_api/mqtt_client.rst +0 -0
  71. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/requirements.txt +0 -0
  72. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/docs/tutorials/getting-started.rst +0 -0
  73. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/.ruff.toml +0 -0
  74. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/README.md +0 -0
  75. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/air_filter_reset.py +0 -0
  76. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/anti_legionella.py +0 -0
  77. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/auto_recovery.py +0 -0
  78. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/combined_callbacks.py +0 -0
  79. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/demand_response.py +0 -0
  80. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/device_capabilities.py +0 -0
  81. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/device_status_debug.py +0 -0
  82. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/energy_analytics.py +0 -0
  83. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/error_code_demo.py +0 -0
  84. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/firmware_payload_capture.py +0 -0
  85. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/mqtt_diagnostics.py +0 -0
  86. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/power_control.py +0 -0
  87. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/recirculation_control.py +0 -0
  88. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/reconnection_demo.py +0 -0
  89. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/reservation_schedule.py +0 -0
  90. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/simple_auto_recovery.py +0 -0
  91. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/token_restoration.py +0 -0
  92. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/tou_openei.py +0 -0
  93. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/tou_schedule.py +0 -0
  94. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/advanced/water_reservation.py +0 -0
  95. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/beginner/01_authentication.py +0 -0
  96. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/beginner/02_list_devices.py +0 -0
  97. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/beginner/03_get_status.py +0 -0
  98. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/beginner/04_set_temperature.py +0 -0
  99. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/advanced_auth_patterns.py +0 -0
  100. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/command_queue.py +0 -0
  101. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/device_status_callback.py +0 -0
  102. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/error_handling.py +0 -0
  103. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/event_driven_control.py +0 -0
  104. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/improved_auth.py +0 -0
  105. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/legacy_auth_constructor.py +0 -0
  106. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  107. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/periodic_requests.py +0 -0
  108. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/set_mode.py +0 -0
  109. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/intermediate/vacation_mode.py +0 -0
  110. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/mask.py +0 -0
  111. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/testing/periodic_device_info.py +0 -0
  112. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/testing/simple_periodic_info.py +0 -0
  113. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/testing/test_api_client.py +0 -0
  114. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/testing/test_mqtt_connection.py +0 -0
  115. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/testing/test_mqtt_messaging.py +0 -0
  116. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/examples/testing/test_periodic_minimal.py +0 -0
  117. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/pyproject.toml +0 -0
  118. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/README.md +0 -0
  119. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/bump_version.py +0 -0
  120. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/diagnose_mqtt_connection.py +0 -0
  121. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/extract_changelog.py +0 -0
  122. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/format.py +0 -0
  123. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/lint.py +0 -0
  124. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/setup-dev.py +0 -0
  125. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/scripts/validate_version.py +0 -0
  126. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/setup.cfg +0 -0
  127. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/setup.py +0 -0
  128. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/__init__.py +0 -0
  129. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/_base.py +0 -0
  130. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/api_client.py +0 -0
  131. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/auth.py +0 -0
  132. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/__init__.py +0 -0
  133. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/__main__.py +0 -0
  134. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/handlers.py +0 -0
  135. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/monitoring.py +0 -0
  136. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/output_formatters.py +0 -0
  137. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/presentation.py +0 -0
  138. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/rich_output.py +0 -0
  139. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/cli/token_storage.py +0 -0
  140. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/command_decorators.py +0 -0
  141. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/config.py +0 -0
  142. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/device_capabilities.py +0 -0
  143. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/device_info_cache.py +0 -0
  144. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/encoding.py +0 -0
  145. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/enums.py +0 -0
  146. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/events.py +0 -0
  147. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/exceptions.py +0 -0
  148. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/factory.py +0 -0
  149. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/field_factory.py +0 -0
  150. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/__init__.py +0 -0
  151. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/_converters.py +0 -0
  152. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/device.py +0 -0
  153. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/diagnostics.py +0 -0
  154. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/energy.py +0 -0
  155. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/feature.py +0 -0
  156. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/firmware.py +0 -0
  157. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/mqtt_models.py +0 -0
  158. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/schedule.py +0 -0
  159. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/status.py +0 -0
  160. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/models/tou.py +0 -0
  161. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/__init__.py +0 -0
  162. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/_control_commands.py +0 -0
  163. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
  164. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/client.py +0 -0
  165. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/command_queue.py +0 -0
  166. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/connection.py +0 -0
  167. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/control.py +0 -0
  168. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/diagnostics.py +0 -0
  169. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/periodic.py +0 -0
  170. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/state_tracker.py +0 -0
  171. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/types.py +0 -0
  172. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt/utils.py +0 -0
  173. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/mqtt_events.py +0 -0
  174. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/openei.py +0 -0
  175. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/py.typed +0 -0
  176. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/reservations.py +0 -0
  177. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/temperature.py +0 -0
  178. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/topic_builder.py +0 -0
  179. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/tou_schedule.py +0 -0
  180. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/unit_system.py +0 -0
  181. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500/utils.py +0 -0
  182. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/SOURCES.txt +0 -0
  183. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  184. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  185. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  186. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/requires.txt +0 -0
  187. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/scm_file_list.json +0 -0
  188. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/top_level.txt +0 -0
  189. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/conftest.py +0 -0
  190. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_api_helpers.py +0 -0
  191. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_app_queries.py +0 -0
  192. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_auth.py +0 -0
  193. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_auth_session_lifecycle.py +0 -0
  194. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_bug_fixes.py +0 -0
  195. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_canonical_schedule.py +0 -0
  196. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_cli_basic.py +0 -0
  197. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_cli_commands.py +0 -0
  198. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_cli_tristate_presentation.py +0 -0
  199. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_command_decorators.py +0 -0
  200. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_command_queue.py +0 -0
  201. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_device_capabilities.py +0 -0
  202. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_device_info_cache.py +0 -0
  203. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_device_rest_models.py +0 -0
  204. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_events.py +0 -0
  205. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_exceptions.py +0 -0
  206. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_model_converters.py +0 -0
  207. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_models.py +0 -0
  208. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_mqtt_clean_session_resume.py +0 -0
  209. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_mqtt_client_init.py +0 -0
  210. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_mqtt_events.py +0 -0
  211. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_mqtt_hypothesis.py +0 -0
  212. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_mqtt_reconnection.py +0 -0
  213. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_mqtt_reconnection_storm.py +0 -0
  214. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_openei.py +0 -0
  215. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_protocol_correctness.py +0 -0
  216. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_public_api.py +0 -0
  217. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_reservations.py +0 -0
  218. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_temperature_converters.py +0 -0
  219. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_threading_model.py +0 -0
  220. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_token_storage.py +0 -0
  221. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_tou_api.py +0 -0
  222. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_tou_schedule.py +0 -0
  223. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_unit_switching.py +0 -0
  224. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_unit_system_process_wide.py +0 -0
  225. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_utility_modules.py +0 -0
  226. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tests/test_utils.py +0 -0
  227. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/tox.ini +0 -0
  228. {nwp500_python-9.4.0 → nwp500_python-9.4.2}/uv.lock +0 -0
@@ -5,6 +5,36 @@ Changelog
5
5
  Unreleased
6
6
  ==========
7
7
 
8
+ Version 9.4.2 (2026-09-17)
9
+ ==========================
10
+
11
+ Changed
12
+ -------
13
+ - **A dropped response message now says why it was dropped.** The typed
14
+ response handlers declined a message in three places without a word: a
15
+ topic that does not end in the required suffix, a payload with no data
16
+ under the expected key, and a reply whose ``macAddress`` names another
17
+ device. A caller watching for a reply that never reached its callback
18
+ could not tell a dropped message from one the cloud never published, and
19
+ the two want opposite fixes. Each branch now logs its reason at DEBUG,
20
+ with the topic and MAC redacted as elsewhere, and guarded by
21
+ ``isEnabledFor`` so the redaction regexes do not run on the normal
22
+ dispatch path when debug logging is off.
23
+
24
+ Version 9.4.1 (2026-09-17)
25
+ ==========================
26
+
27
+ Fixed
28
+ -----
29
+ - **The reconnect attempt counter never reset after a successful quick or
30
+ deep reconnect.** Both build a new connection, so the SDK never calls
31
+ ``on_connection_resumed``, the only place the counter was reset. Each
32
+ later interruption started where the last one stopped: its first delay
33
+ was longer and it reached the deep-reconnect threshold sooner. A
34
+ long-running client saw the first delay grow from about 1 s to 12 s
35
+ over five daily AWS IoT disconnects. The counter now resets whenever the
36
+ backoff loop ends connected.
37
+
8
38
  Version 9.4.0 (2026-09-12)
9
39
  ==========================
10
40
 
@@ -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.0
3
+ Version: 9.4.2
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
@@ -3,7 +3,7 @@ Tank Energy
3
3
  ===========
4
4
 
5
5
  The NWP500 reports two energy figures, ``totalEnergyCapacity`` and
6
- ``availableEnergyCapacity``. Both names mislead, and before v10.0 this
6
+ ``availableEnergyCapacity``. Both names mislead, and before v9.3.0 this
7
7
  library also scaled them wrongly. This page explains what they
8
8
  actually measure and shows the evidence for the correction.
9
9
 
@@ -33,7 +33,7 @@ The short version
33
33
  whenever the setpoint moves.
34
34
 
35
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.
36
+ 9.3.0 multiplied by 10, overstating tank energy by 2.5x.
37
37
 
38
38
  Both are also measured **from the setpoint**, so both describe potential
39
39
  rather than content: move the setpoint and both change while the water in
@@ -226,7 +226,7 @@ coefficient of performance:
226
226
  * - 4 Wh/count (corrected)
227
227
  - 2.89
228
228
  - Normal for a heat pump water heater
229
- * - 10 Wh/count (pre-10.0)
229
+ * - 10 Wh/count (pre-9.3.0)
230
230
  - 7.02
231
231
  - Physically impossible
232
232
 
@@ -236,7 +236,7 @@ A heat pump water heater in a 72 degF room runs at a COP of roughly 2 to
236
236
  This argument is worth stating separately because it needs no tank
237
237
  volume, specific heat or stratification model - only the device's own
238
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
239
+ quantum *is*, but it rules out the pre-9.3.0 value regardless of anything
240
240
  assumed elsewhere on this page.
241
241
 
242
242
  .. _two branches:
@@ -403,8 +403,8 @@ the heat capacity is 156 Wh per degF:
403
403
  drawable_Wh = 156 * (tank_mean_temperature - your_floor_degF)
404
404
 
405
405
 
406
- Migrating from before v10.0
407
- ===========================
406
+ Migrating from before v9.3.0
407
+ ============================
408
408
 
409
409
  .. list-table::
410
410
  :header-rows: 1
@@ -429,7 +429,7 @@ rename that is missed fails immediately with ``AttributeError`` instead
429
429
  of silently returning a number 2.5x too large.
430
430
 
431
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.
432
+ rescaling by 0.4 to be comparable with values from v9.3.0 onward.
433
433
 
434
434
 
435
435
  See also
@@ -367,7 +367,7 @@ Power and Energy Fields
367
367
  candidate implying a water volume below the nameplate. Two further
368
368
  checks agree -- 183 heating recoveries give 4.11 Wh/count by a
369
369
  noisier route, and the same recoveries imply a heat-pump COP of 2.89.
370
- The library uses 4.0. Versions before 10.0 used 10, which overstated
370
+ The library uses 4.0. Versions before 9.3.0 used 10, which overstated
371
371
  reported tank energy by 2.5x and implied a physically impossible COP
372
372
  of 7.0. See :doc:`../../explanation/tank-energy`.
373
373
 
@@ -263,7 +263,7 @@ Complete real-time device status with 100+ fields.
263
263
 
264
264
  .. warning::
265
265
  These replace ``total_energy_capacity`` and
266
- ``available_energy_capacity``, which were **removed** in v10.0. The
266
+ ``available_energy_capacity``, which were **removed** in v9.3.0. The
267
267
  old names were misleading -- ``available_energy_capacity`` is the
268
268
  energy *needed*, not the energy available -- and both were scaled
269
269
  2.5x too large. For energy you can actually draw, see
@@ -45,7 +45,7 @@ __all__ = [
45
45
  #: p90 4.45), agreeing to within 2% by a noisier route
46
46
  #: * the same recoveries imply a heat pump COP of 2.89 at 4 Wh/count
47
47
  #:
48
- #: Library versions before 10.0 used 10 Wh/count, which overstated reported
48
+ #: Library versions before 9.3.0 used 10 Wh/count, which overstated reported
49
49
  #: tank energy by 2.5x and implied a physically impossible COP of 7.0.
50
50
  #:
51
51
  #: See ``docs/explanation/tank-energy.rst`` for the full derivation.
@@ -347,6 +347,13 @@ class MqttReconnectionHandler:
347
347
  f"Error during reconnection attempt: {e}", exc_info=True
348
348
  )
349
349
 
350
+ # A successful quick or deep reconnect builds a new connection, so
351
+ # the SDK never calls on_connection_resumed for it. Reset the
352
+ # counter here, or every later interruption starts with a longer
353
+ # backoff and an earlier deep reconnect than the first one did.
354
+ if self._is_connected_func():
355
+ self._reconnect_attempts = 0
356
+
350
357
  # Check final state: report failure when retries are exhausted
351
358
  # (limited mode) or a fatal error stopped the loop.
352
359
  attempts_exhausted = (
@@ -40,7 +40,12 @@ from ..mqtt_events import (
40
40
  from ..topic_builder import MqttTopicBuilder
41
41
  from .state_tracker import DeviceStateTracker
42
42
  from .types import QoS, to_awscrt_qos
43
- from .utils import get_response_data, redact_topic, topic_matches_pattern
43
+ from .utils import (
44
+ get_response_data,
45
+ redact_mac,
46
+ redact_topic,
47
+ topic_matches_pattern,
48
+ )
44
49
 
45
50
  if TYPE_CHECKING:
46
51
  from ..device_info_cache import MqttDeviceInfoCache
@@ -614,13 +619,45 @@ class MqttSubscriptionManager:
614
619
  """
615
620
 
616
621
  def handler(topic: str, message: dict[str, Any]) -> None:
622
+ # Every branch that declines a message says so at debug level.
623
+ # Silence here is expensive to diagnose: a caller watching for a
624
+ # reply that never reaches its callback cannot tell a dropped
625
+ # message from one the cloud never sent, and the two want
626
+ # opposite fixes. The logs are the only evidence either way.
627
+ #
628
+ # Each log is guarded: redact_topic runs a dozen regexes, and
629
+ # these branches are the normal case for a handler registered
630
+ # under the device wildcard, which sees every message for that
631
+ # device and keeps only its own.
617
632
  if topic_suffix and not topic.endswith(topic_suffix):
633
+ if _logger.isEnabledFor(logging.DEBUG):
634
+ _logger.debug(
635
+ "Ignoring a message on %s: the topic does not end "
636
+ "with %r, which this handler requires",
637
+ redact_topic(topic),
638
+ topic_suffix,
639
+ )
618
640
  return
619
641
  try:
620
642
  data = get_response_data(message, key)
621
643
  if not data:
644
+ if _logger.isEnabledFor(logging.DEBUG):
645
+ _logger.debug(
646
+ "Ignoring a message on %s: no %s data to parse "
647
+ "as %s",
648
+ redact_topic(topic),
649
+ f"{key!r}" if key else "response",
650
+ model.__name__,
651
+ )
622
652
  return
623
653
  if only_mac and not _same_device(data, message, only_mac):
654
+ if _logger.isEnabledFor(logging.DEBUG):
655
+ _logger.debug(
656
+ "Ignoring a message on %s: it names another "
657
+ "device, not %s",
658
+ redact_topic(topic),
659
+ redact_mac(only_mac),
660
+ )
624
661
  return
625
662
 
626
663
  parsed = parse(data) if parse else model.model_validate(data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.4.0
3
+ Version: 9.4.2
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,8 @@
1
+ {
2
+ "tag": "9.4.2",
3
+ "distance": 0,
4
+ "node": "gfe7fe156d7d292a934e3bfa48eb7f274b00ffe33",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-18"
8
+ }
@@ -231,6 +231,45 @@ class TestReconnectLoopExceptionHandling:
231
231
  assert all(d <= config.max_reconnect_delay for d in sleeps)
232
232
  assert all(d > 0 for d in sleeps)
233
233
 
234
+ @pytest.mark.asyncio
235
+ @pytest.mark.parametrize("deep", [False, True])
236
+ async def test_successful_reconnect_resets_attempt_count(self, deep):
237
+ """Regression: a quick or deep reconnect makes a new connection, so
238
+ on_connection_resumed never fires and the counter kept growing
239
+ across interruptions, lengthening each later backoff."""
240
+ connected = False
241
+ attempts = []
242
+
243
+ async def reconnect():
244
+ nonlocal connected
245
+ attempts.append(1)
246
+ # Fail the first try of each interruption, then succeed.
247
+ if len(attempts) % 2 == 0:
248
+ connected = True
249
+ return
250
+ raise MqttNotConnectedError("down")
251
+
252
+ kwargs = {"deep_reconnect_func": reconnect} if deep else {}
253
+ config = _fast_config(
254
+ max_reconnect_attempts=-1,
255
+ deep_reconnect_threshold=1,
256
+ )
257
+ handler = _make_handler(
258
+ config,
259
+ lambda: connected,
260
+ reconnect,
261
+ **kwargs,
262
+ )
263
+ handler.enable()
264
+
265
+ for _ in range(3):
266
+ connected = False
267
+ await handler._reconnect_with_backoff()
268
+ assert connected
269
+ assert handler.attempt_count == 0
270
+
271
+ assert len(attempts) == 6
272
+
234
273
 
235
274
  class TestDisconnectDuringInterruption:
236
275
  """disconnect() must fully shut down even when not connected."""
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  from unittest.mock import AsyncMock, MagicMock
2
3
 
3
4
  import pytest
@@ -5,6 +6,7 @@ import pytest
5
6
  from nwp500.enums import CurrentOperationMode
6
7
  from nwp500.events import EventEmitter
7
8
  from nwp500.models import DeviceFeature, DeviceStatus
9
+ from nwp500.mqtt import subscriptions as subscriptions_module
8
10
  from nwp500.mqtt.state_tracker import DeviceStateTracker
9
11
  from nwp500.mqtt.subscriptions import MqttSubscriptionManager
10
12
  from nwp500.mqtt_events import (
@@ -241,3 +243,150 @@ def test_make_handler_injects_mac():
241
243
  parsed = callback_called[0]
242
244
  assert isinstance(parsed, DeviceStatus)
243
245
  assert parsed.mac_address == mac
246
+
247
+
248
+ def _manager() -> MqttSubscriptionManager:
249
+ return MqttSubscriptionManager(
250
+ connection=MagicMock(),
251
+ client_id="test_client",
252
+ event_emitter=MagicMock(),
253
+ schedule_coroutine=MagicMock(),
254
+ )
255
+
256
+
257
+ @pytest.mark.parametrize(
258
+ ("kwargs", "topic", "message", "expected"),
259
+ [
260
+ (
261
+ {"topic_suffix": "st/rsv"},
262
+ "cmd/52/client/res/td/rd",
263
+ {"response": {"reservation": []}},
264
+ "does not end with",
265
+ ),
266
+ (
267
+ {"key": "status"},
268
+ "cmd/52/client/res/st",
269
+ {"response": {}},
270
+ "no 'status' data to parse",
271
+ ),
272
+ (
273
+ {"key": "status", "only_mac": "00:11:22:33:44:55"},
274
+ "cmd/52/client/res/st",
275
+ {"status": {"macAddress": "aa:bb:cc:dd:ee:ff"}},
276
+ "names another device",
277
+ ),
278
+ ],
279
+ ids=["topic-suffix", "no-data", "other-device"],
280
+ )
281
+ def test_a_declined_message_says_why(kwargs, topic, message, expected, caplog):
282
+ """Every branch that drops a message logs its reason.
283
+
284
+ Silence here cost real diagnosis time: a reply that never reaches its
285
+ callback looks identical to one the cloud never sent, and the two want
286
+ opposite fixes.
287
+ """
288
+ delivered = []
289
+ handler = _manager()._make_handler(
290
+ model=DeviceStatus, callback=delivered.append, **kwargs
291
+ )
292
+
293
+ with caplog.at_level(logging.DEBUG, logger="nwp500.mqtt.subscriptions"):
294
+ handler(topic, message)
295
+
296
+ assert delivered == []
297
+ assert any(expected in r.getMessage() for r in caplog.records), caplog.text
298
+
299
+
300
+ def test_a_declined_message_never_logs_a_raw_identifier(caplog):
301
+ """The reason is logged; the MAC in it is not.
302
+
303
+ These lines carry a topic and a MAC straight from the wire, so the
304
+ redaction is the whole reason they are safe to emit at all.
305
+ """
306
+ mac = "04786332fca0"
307
+ other = "aabbccddeeff"
308
+ topic = f"cmd/52/navilink-{mac}/st/td/rd"
309
+ delivered = []
310
+ handler = _manager()._make_handler(
311
+ model=DeviceStatus,
312
+ callback=delivered.append,
313
+ key="status",
314
+ only_mac=mac,
315
+ parse=lambda data: data,
316
+ )
317
+
318
+ with caplog.at_level(logging.DEBUG, logger="nwp500.mqtt.subscriptions"):
319
+ handler(topic, {"status": {"macAddress": other, "command": 0}})
320
+
321
+ assert delivered == []
322
+ assert "names another device" in caplog.text
323
+ assert mac not in caplog.text
324
+ assert other not in caplog.text
325
+ assert "REDACTED" in caplog.text
326
+
327
+
328
+ @pytest.mark.parametrize(
329
+ ("level", "redactions"),
330
+ [(logging.INFO, 0), (logging.DEBUG, 1)],
331
+ ids=["debug-off", "debug-on"],
332
+ )
333
+ def test_declining_a_message_redacts_only_when_debug_is_on(
334
+ level, redactions, monkeypatch
335
+ ):
336
+ """The guard, not just the log level.
337
+
338
+ redact_topic is a dozen regexes, and a handler registered under the
339
+ device wildcard declines most of what it sees, so an unguarded call
340
+ would run them on the normal dispatch path for nothing.
341
+ """
342
+ calls = []
343
+ real = subscriptions_module.redact_topic
344
+
345
+ def counting_redact_topic(topic):
346
+ calls.append(topic)
347
+ return real(topic)
348
+
349
+ monkeypatch.setattr(
350
+ subscriptions_module, "redact_topic", counting_redact_topic
351
+ )
352
+ # setLevel, not the attribute: isEnabledFor caches its answer, and
353
+ # only setLevel invalidates that cache.
354
+ logger = subscriptions_module._logger
355
+ original_level = logger.level
356
+ logger.setLevel(level)
357
+ monkeypatch.setattr(logger, "propagate", False)
358
+
359
+ handler = _manager()._make_handler(
360
+ model=DeviceStatus,
361
+ callback=lambda parsed: None,
362
+ key="status",
363
+ topic_suffix="st/rsv",
364
+ )
365
+ try:
366
+ handler("cmd/52/client/res/td/rd", {"status": {"command": 0}})
367
+ finally:
368
+ logger.setLevel(original_level)
369
+
370
+ assert len(calls) == redactions
371
+
372
+
373
+ def test_a_message_for_this_device_is_still_delivered(caplog):
374
+ """The mac filter accepts its own device, and says nothing about it."""
375
+ mac = "00:11:22:33:44:55"
376
+ delivered = []
377
+ handler = _manager()._make_handler(
378
+ model=DeviceStatus,
379
+ callback=delivered.append,
380
+ key="status",
381
+ only_mac=mac,
382
+ parse=lambda data: data,
383
+ )
384
+
385
+ with caplog.at_level(logging.DEBUG, logger="nwp500.mqtt.subscriptions"):
386
+ handler(
387
+ "cmd/52/client/res/st",
388
+ {"response": {"status": {"macAddress": mac, "command": 0}}},
389
+ )
390
+
391
+ assert len(delivered) == 1
392
+ assert "Ignoring" not in caplog.text
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "9.4.0",
3
- "distance": 0,
4
- "node": "g14ff7fc4c8672df4b22b6ec222f49a67fcf78a50",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-09-13"
8
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes