nwp500-python 8.1.0__tar.gz → 8.1.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 (201) hide show
  1. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/CHANGELOG.rst +3 -0
  2. {nwp500_python-8.1.0/src/nwp500_python.egg-info → nwp500_python-8.1.2}/PKG-INFO +1 -1
  3. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/mqtt/client.py +74 -5
  4. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/mqtt/periodic.py +0 -6
  5. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/mqtt/reconnection.py +17 -2
  6. {nwp500_python-8.1.0 → nwp500_python-8.1.2/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  7. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500_python.egg-info/SOURCES.txt +2 -0
  8. nwp500_python-8.1.2/tests/test_mqtt_clean_session_resume.py +190 -0
  9. nwp500_python-8.1.2/tests/test_mqtt_reconnection_storm.py +447 -0
  10. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/.coveragerc +0 -0
  11. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  12. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/.github/copilot-instructions.md +0 -0
  13. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/.github/workflows/ci.yml +0 -0
  14. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/.github/workflows/release.yml +0 -0
  15. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/.gitignore +0 -0
  16. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/.pre-commit-config.yaml +0 -0
  17. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/.readthedocs.yml +0 -0
  18. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/AUTHORS.rst +0 -0
  19. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/CONTRIBUTING.rst +0 -0
  20. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/LICENSE.txt +0 -0
  21. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/Makefile +0 -0
  22. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/README.rst +0 -0
  23. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/RELEASE.md +0 -0
  24. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/Makefile +0 -0
  25. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/_static/.gitignore +0 -0
  26. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/conf.py +0 -0
  27. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/explanation/advanced-features.rst +0 -0
  28. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/explanation/architecture.rst +0 -0
  29. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/explanation/index.rst +0 -0
  30. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/how-to/authenticate.rst +0 -0
  31. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/how-to/auto-recovery.rst +0 -0
  32. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/how-to/diagnose-mqtt.rst +0 -0
  33. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/how-to/home-assistant.rst +0 -0
  34. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/how-to/index.rst +0 -0
  35. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/how-to/maintenance.rst +0 -0
  36. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/how-to/manage-units.rst +0 -0
  37. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/how-to/monitor-status.rst +0 -0
  38. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/how-to/optimize-tou.rst +0 -0
  39. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/how-to/queue-commands.rst +0 -0
  40. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/how-to/schedule-operation.rst +0 -0
  41. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/how-to/track-energy.rst +0 -0
  42. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/index.rst +0 -0
  43. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/openapi.yaml +0 -0
  44. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/project/authors.rst +0 -0
  45. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/project/changelog.rst +0 -0
  46. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/project/contributing.rst +0 -0
  47. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/project/history.rst +0 -0
  48. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/project/license.rst +0 -0
  49. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/configuration.rst +0 -0
  50. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/enumerations.rst +0 -0
  51. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/index.rst +0 -0
  52. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/installation.rst +0 -0
  53. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/protocol/data_conversions.rst +0 -0
  54. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/protocol/device_features.rst +0 -0
  55. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/protocol/device_status.rst +0 -0
  56. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/protocol/error_codes.rst +0 -0
  57. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  58. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/protocol/quick_reference.rst +0 -0
  59. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/protocol/rest_api.rst +0 -0
  60. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/python_api/api_client.rst +0 -0
  61. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/python_api/auth_client.rst +0 -0
  62. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/python_api/cli.rst +0 -0
  63. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/python_api/events.rst +0 -0
  64. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/python_api/exceptions.rst +0 -0
  65. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/python_api/models.rst +0 -0
  66. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/reference/python_api/mqtt_client.rst +0 -0
  67. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/requirements.txt +0 -0
  68. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/docs/tutorials/getting-started.rst +0 -0
  69. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/.ruff.toml +0 -0
  70. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/README.md +0 -0
  71. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/air_filter_reset.py +0 -0
  72. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/anti_legionella.py +0 -0
  73. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/auto_recovery.py +0 -0
  74. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/combined_callbacks.py +0 -0
  75. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/demand_response.py +0 -0
  76. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/device_capabilities.py +0 -0
  77. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/device_status_debug.py +0 -0
  78. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/energy_analytics.py +0 -0
  79. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/error_code_demo.py +0 -0
  80. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/firmware_payload_capture.py +0 -0
  81. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/mqtt_diagnostics.py +0 -0
  82. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/power_control.py +0 -0
  83. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/recirculation_control.py +0 -0
  84. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/reconnection_demo.py +0 -0
  85. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/reservation_schedule.py +0 -0
  86. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/simple_auto_recovery.py +0 -0
  87. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/token_restoration.py +0 -0
  88. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/tou_openei.py +0 -0
  89. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/tou_schedule.py +0 -0
  90. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/advanced/water_reservation.py +0 -0
  91. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/beginner/01_authentication.py +0 -0
  92. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/beginner/02_list_devices.py +0 -0
  93. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/beginner/03_get_status.py +0 -0
  94. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/beginner/04_set_temperature.py +0 -0
  95. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/intermediate/advanced_auth_patterns.py +0 -0
  96. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/intermediate/command_queue.py +0 -0
  97. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/intermediate/device_status_callback.py +0 -0
  98. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/intermediate/error_handling.py +0 -0
  99. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/intermediate/event_driven_control.py +0 -0
  100. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/intermediate/improved_auth.py +0 -0
  101. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/intermediate/legacy_auth_constructor.py +0 -0
  102. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  103. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/intermediate/periodic_requests.py +0 -0
  104. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/intermediate/set_mode.py +0 -0
  105. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/intermediate/vacation_mode.py +0 -0
  106. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/mask.py +0 -0
  107. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/testing/periodic_device_info.py +0 -0
  108. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/testing/simple_periodic_info.py +0 -0
  109. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/testing/test_api_client.py +0 -0
  110. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/testing/test_mqtt_connection.py +0 -0
  111. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/testing/test_mqtt_messaging.py +0 -0
  112. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/examples/testing/test_periodic_minimal.py +0 -0
  113. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/pyproject.toml +0 -0
  114. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/scripts/README.md +0 -0
  115. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/scripts/bump_version.py +0 -0
  116. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/scripts/diagnose_mqtt_connection.py +0 -0
  117. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/scripts/extract_changelog.py +0 -0
  118. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/scripts/format.py +0 -0
  119. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/scripts/lint.py +0 -0
  120. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/scripts/setup-dev.py +0 -0
  121. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/scripts/validate_version.py +0 -0
  122. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/setup.cfg +0 -0
  123. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/setup.py +0 -0
  124. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/__init__.py +0 -0
  125. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/_base.py +0 -0
  126. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/api_client.py +0 -0
  127. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/auth.py +0 -0
  128. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/cli/__init__.py +0 -0
  129. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/cli/__main__.py +0 -0
  130. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/cli/commands.py +0 -0
  131. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/cli/handlers.py +0 -0
  132. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/cli/monitoring.py +0 -0
  133. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/cli/output_formatters.py +0 -0
  134. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/cli/rich_output.py +0 -0
  135. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/cli/token_storage.py +0 -0
  136. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/command_decorators.py +0 -0
  137. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/config.py +0 -0
  138. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/converters.py +0 -0
  139. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/device_capabilities.py +0 -0
  140. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/device_info_cache.py +0 -0
  141. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/encoding.py +0 -0
  142. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/enums.py +0 -0
  143. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/events.py +0 -0
  144. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/exceptions.py +0 -0
  145. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/factory.py +0 -0
  146. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/field_factory.py +0 -0
  147. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/models/__init__.py +0 -0
  148. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/models/_converters.py +0 -0
  149. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/models/device.py +0 -0
  150. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/models/energy.py +0 -0
  151. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/models/feature.py +0 -0
  152. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/models/mqtt_models.py +0 -0
  153. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/models/schedule.py +0 -0
  154. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/models/status.py +0 -0
  155. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/models/tou.py +0 -0
  156. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/mqtt/__init__.py +0 -0
  157. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/mqtt/command_queue.py +0 -0
  158. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/mqtt/connection.py +0 -0
  159. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/mqtt/control.py +0 -0
  160. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/mqtt/diagnostics.py +0 -0
  161. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/mqtt/state_tracker.py +0 -0
  162. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/mqtt/subscriptions.py +0 -0
  163. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/mqtt/utils.py +0 -0
  164. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/mqtt_events.py +0 -0
  165. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/openei.py +0 -0
  166. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/py.typed +0 -0
  167. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/reservations.py +0 -0
  168. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/temperature.py +0 -0
  169. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/topic_builder.py +0 -0
  170. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/unit_system.py +0 -0
  171. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500/utils.py +0 -0
  172. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  173. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  174. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  175. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500_python.egg-info/requires.txt +0 -0
  176. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/src/nwp500_python.egg-info/top_level.txt +0 -0
  177. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/conftest.py +0 -0
  178. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_api_helpers.py +0 -0
  179. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_auth.py +0 -0
  180. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_bug_fixes.py +0 -0
  181. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_cli_basic.py +0 -0
  182. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_cli_commands.py +0 -0
  183. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_command_decorators.py +0 -0
  184. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_command_queue.py +0 -0
  185. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_device_capabilities.py +0 -0
  186. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_device_info_cache.py +0 -0
  187. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_events.py +0 -0
  188. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_exceptions.py +0 -0
  189. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_model_converters.py +0 -0
  190. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_models.py +0 -0
  191. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_mqtt_client_init.py +0 -0
  192. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_mqtt_hypothesis.py +0 -0
  193. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_mqtt_reconnection.py +0 -0
  194. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_multi_device.py +0 -0
  195. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_openei.py +0 -0
  196. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_reservations.py +0 -0
  197. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_temperature_converters.py +0 -0
  198. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_tou_api.py +0 -0
  199. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_unit_switching.py +0 -0
  200. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tests/test_utils.py +0 -0
  201. {nwp500_python-8.1.0 → nwp500_python-8.1.2}/tox.ini +0 -0
@@ -5,6 +5,9 @@ Changelog
5
5
  Unreleased
6
6
  ==========
7
7
 
8
+ Version 8.1.2 (2026-05-25)
9
+ ==========================
10
+
8
11
  Version 8.1.0 (2026-05-16)
9
12
  ==========================
10
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 8.1.0
3
+ Version: 8.1.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
@@ -226,6 +226,11 @@ class NavienMqttClient(EventEmitter):
226
226
  # Connection state (simpler than checking _connection_manager)
227
227
  self._connection: mqtt.Connection | None = None
228
228
  self._connected = False
229
+ # Guards _active_reconnect / _deep_reconnect against re-entrancy.
230
+ # While True, _on_connection_interrupted_internal will not forward
231
+ # events to the reconnection handler, preventing the intentional
232
+ # teardown of the old connection from spawning a competing backoff loop.
233
+ self._actively_reconnecting = False
229
234
 
230
235
  _logger.info(
231
236
  f"Initialized MQTT client with ID: {self.config.client_id}"
@@ -276,8 +281,17 @@ class NavienMqttClient(EventEmitter):
276
281
  )
277
282
  )
278
283
 
279
- # Delegate to reconnection handler if available
280
- if self._reconnection_handler and self.config.auto_reconnect:
284
+ # Delegate to reconnection handler if available.
285
+ # Skip while _actively_reconnecting: the interruption was caused by
286
+ # _active_reconnect / _deep_reconnect intentionally closing the old
287
+ # connection. Forwarding it would queue a _start_reconnect_task
288
+ # coroutine that could fire after the new connection is up and the
289
+ # existing backoff task has been cancelled, spawning a competing loop.
290
+ if (
291
+ self._reconnection_handler
292
+ and self.config.auto_reconnect
293
+ and not self._actively_reconnecting
294
+ ):
281
295
  self._reconnection_handler.on_connection_interrupted(error)
282
296
 
283
297
  # Record diagnostic event
@@ -350,8 +364,20 @@ class NavienMqttClient(EventEmitter):
350
364
  )
351
365
  )
352
366
 
353
- # Send any queued commands
354
- if self.config.enable_command_queue and self._command_queue:
367
+ # When the broker starts a clean session (session_present=False), all
368
+ # previous subscriptions have been dropped server-side. We must
369
+ # re-establish them before any device data can flow. This covers the
370
+ # common case where the AWS IoT SDK auto-reconnects internally before
371
+ # the MqttReconnectionHandler fires its own reconnect path — in that
372
+ # scenario the reconnect handler sees _connected==True and exits early,
373
+ # so resubscribe_all() would never be called without this block.
374
+ #
375
+ # When session_present=False, we must resubscribe before sending queued
376
+ # commands to ensure subscriptions are restored before device responses
377
+ # are processed. Use a composite coroutine to enforce ordering.
378
+ if not session_present and self._subscription_manager:
379
+ self._schedule_coroutine(self._handle_clean_session_resume())
380
+ elif self.config.enable_command_queue and self._command_queue:
355
381
  self._schedule_coroutine(self._send_queued_commands_internal())
356
382
 
357
383
  async def _send_queued_commands_internal(self) -> None:
@@ -363,6 +389,29 @@ class NavienMqttClient(EventEmitter):
363
389
  self._connection_manager.publish, lambda: self._connected
364
390
  )
365
391
 
392
+ async def _handle_clean_session_resume(self) -> None:
393
+ """
394
+ Handle clean session reconnection with ordered resubscription.
395
+
396
+ When session_present=False (clean session), the broker has dropped all
397
+ subscriptions. This method ensures subscriptions are restored BEFORE
398
+ sending any queued commands, preventing commands from being processed
399
+ before their subscriptions are re-established.
400
+ """
401
+ if not self._subscription_manager or not self._connection_manager:
402
+ return
403
+
404
+ if not self._connection_manager.connection:
405
+ return
406
+
407
+ self._subscription_manager.update_connection(
408
+ self._connection_manager.connection
409
+ )
410
+ await self._subscription_manager.resubscribe_all()
411
+
412
+ if self.config.enable_command_queue and self._command_queue:
413
+ await self._send_queued_commands_internal()
414
+
366
415
  async def _active_reconnect(self) -> None:
367
416
  """
368
417
  Actively trigger a reconnection attempt.
@@ -380,8 +429,13 @@ class NavienMqttClient(EventEmitter):
380
429
  _logger.debug("Already connected, skipping reconnection")
381
430
  return
382
431
 
432
+ if self._actively_reconnecting:
433
+ _logger.debug("Active reconnection already in progress, skipping")
434
+ return
435
+
383
436
  _logger.info("Attempting active reconnection...")
384
437
 
438
+ self._actively_reconnecting = True
385
439
  try:
386
440
  # Ensure tokens are still valid
387
441
  await self._auth_client.ensure_valid_token()
@@ -390,6 +444,9 @@ class NavienMqttClient(EventEmitter):
390
444
  if self._connection_manager:
391
445
  # Close old connection to stop SDK auto-reconnect and
392
446
  # prevent two connections with the same client ID.
447
+ # _actively_reconnecting suppresses the
448
+ # on_connection_interrupted callback that closing triggers,
449
+ # preventing a competing backoff loop from being spawned.
393
450
  _logger.debug("Recreating MQTT connection...")
394
451
  try:
395
452
  await self._connection_manager.close()
@@ -432,6 +489,8 @@ class NavienMqttClient(EventEmitter):
432
489
  f"Error during active reconnection: {e}", exc_info=True
433
490
  )
434
491
  raise
492
+ finally:
493
+ self._actively_reconnecting = False
435
494
 
436
495
  async def _deep_reconnect(self) -> None:
437
496
  """
@@ -451,13 +510,21 @@ class NavienMqttClient(EventEmitter):
451
510
  _logger.debug("Already connected, skipping deep reconnection")
452
511
  return
453
512
 
513
+ if self._actively_reconnecting:
514
+ _logger.debug("Active reconnection already in progress, skipping")
515
+ return
516
+
454
517
  _logger.warning(
455
518
  "Performing deep reconnection (full rebuild)... "
456
519
  "This may take longer."
457
520
  )
458
521
 
522
+ self._actively_reconnecting = True
459
523
  try:
460
- # Step 1: Clean up existing connection if any
524
+ # Step 1: Clean up existing connection if any.
525
+ # _actively_reconnecting suppresses the on_connection_interrupted
526
+ # callback that closing triggers, preventing a competing backoff
527
+ # loop from being spawned.
461
528
  if self._connection_manager:
462
529
  _logger.debug("Cleaning up old connection...")
463
530
  try:
@@ -534,6 +601,8 @@ class NavienMqttClient(EventEmitter):
534
601
  ) as e:
535
602
  _logger.error(f"Error during deep reconnection: {e}", exc_info=True)
536
603
  raise
604
+ finally:
605
+ self._actively_reconnecting = False
537
606
 
538
607
  async def connect(self) -> bool:
539
608
  """
@@ -173,12 +173,6 @@ class MqttPeriodicRequestManager:
173
173
  await self._request_device_info(device)
174
174
  elif request_type == PeriodicRequestType.DEVICE_STATUS:
175
175
  await self._request_device_status(device)
176
- else:
177
- _logger.error(
178
- "Unknown periodic request type: %s",
179
- request_type,
180
- )
181
- break
182
176
 
183
177
  _logger.debug(
184
178
  "Sent periodic %s request for %s",
@@ -94,11 +94,18 @@ class MqttReconnectionHandler:
94
94
  """
95
95
  _logger.warning(f"Connection interrupted: {error}")
96
96
 
97
- # Start automatic reconnection if enabled
97
+ # Start automatic reconnection if enabled.
98
+ # Also guard against stale interruption events that arrive after the
99
+ # connection has already been restored: these can be queued via
100
+ # run_coroutine_threadsafe and fire after on_connection_resumed has
101
+ # cancelled _reconnect_task (setting it to None), which would
102
+ # otherwise bypass the task-existence check and spawn a new backoff
103
+ # loop while the client is perfectly healthy.
98
104
  if (
99
105
  self.config.auto_reconnect
100
106
  and self._enabled
101
107
  and not self._manual_disconnect
108
+ and not self._is_connected_func()
102
109
  and (not self._reconnect_task or self._reconnect_task.done())
103
110
  ):
104
111
  _logger.info("Starting automatic reconnection...")
@@ -132,8 +139,16 @@ class MqttReconnectionHandler:
132
139
 
133
140
  This is a helper method to create the reconnect task from within
134
141
  a coroutine that's scheduled via _schedule_coroutine.
142
+
143
+ The is_connected guard is re-checked here because this coroutine may
144
+ be queued via run_coroutine_threadsafe and run after the connection
145
+ has already been restored (e.g. by on_connection_resumed cancelling
146
+ _reconnect_task), in which case starting a new backoff loop would
147
+ incorrectly tear down a healthy connection.
135
148
  """
136
- if not self._reconnect_task or self._reconnect_task.done():
149
+ if not self._is_connected_func() and (
150
+ not self._reconnect_task or self._reconnect_task.done()
151
+ ):
137
152
  self._reconnect_task = asyncio.create_task(
138
153
  self._reconnect_with_backoff()
139
154
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 8.1.0
3
+ Version: 8.1.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
@@ -186,9 +186,11 @@ tests/test_events.py
186
186
  tests/test_exceptions.py
187
187
  tests/test_model_converters.py
188
188
  tests/test_models.py
189
+ tests/test_mqtt_clean_session_resume.py
189
190
  tests/test_mqtt_client_init.py
190
191
  tests/test_mqtt_hypothesis.py
191
192
  tests/test_mqtt_reconnection.py
193
+ tests/test_mqtt_reconnection_storm.py
192
194
  tests/test_multi_device.py
193
195
  tests/test_openei.py
194
196
  tests/test_reservations.py
@@ -0,0 +1,190 @@
1
+ """Tests for MQTT client clean session reconnection handling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import AsyncMock, MagicMock, patch
6
+
7
+ import pytest
8
+
9
+ from nwp500.auth import AuthenticationResponse, AuthTokens, UserInfo
10
+ from nwp500.mqtt import NavienMqttClient
11
+
12
+
13
+ @pytest.fixture
14
+ def auth_client_with_valid_tokens():
15
+ """Create an auth client with valid tokens."""
16
+ from nwp500.auth import NavienAuthClient
17
+
18
+ auth_client = NavienAuthClient("test@example.com", "password")
19
+ valid_tokens = AuthTokens(
20
+ id_token="test_id",
21
+ access_token="test_access",
22
+ refresh_token="test_refresh",
23
+ authentication_expires_in=3600,
24
+ access_key_id="test_key_id",
25
+ secret_key="test_secret_key",
26
+ session_token="test_session",
27
+ authorization_expires_in=3600,
28
+ )
29
+ auth_client._auth_response = AuthenticationResponse(
30
+ user_info=UserInfo(user_first_name="Test", user_last_name="User"),
31
+ tokens=valid_tokens,
32
+ )
33
+ return auth_client
34
+
35
+
36
+ class TestMqttCleanSessionResume:
37
+ """Tests for clean session (session_present=False) reconnection handling."""
38
+
39
+ @pytest.mark.asyncio(loop_scope="function")
40
+ async def test_on_connection_resumed_with_clean_session_resubscribes(
41
+ self, auth_client_with_valid_tokens
42
+ ):
43
+ """Resubscribe when session_present=False on connection resume."""
44
+ client = NavienMqttClient(auth_client_with_valid_tokens)
45
+
46
+ # Mock the components
47
+ mock_subscription_manager = AsyncMock()
48
+ mock_subscription_manager.resubscribe_all = AsyncMock()
49
+ client._subscription_manager = mock_subscription_manager
50
+
51
+ mock_connection_manager = MagicMock()
52
+ mock_connection = MagicMock()
53
+ mock_connection_manager.connection = mock_connection
54
+ client._connection_manager = mock_connection_manager
55
+
56
+ # Mock the event emitter and diagnostics
57
+ client.emit = AsyncMock()
58
+ client._diagnostics = MagicMock()
59
+ client._diagnostics.record_connection_success = AsyncMock()
60
+
61
+ # Call with session_present=False (clean session)
62
+ client._on_connection_resumed_internal(
63
+ connection=mock_connection, return_code=0, session_present=False
64
+ )
65
+
66
+ # Give the scheduled coroutine time to run
67
+ import asyncio
68
+
69
+ await asyncio.sleep(0.1)
70
+
71
+ # Verify resubscribe_all was called
72
+ mock_subscription_manager.update_connection.assert_called_once_with(
73
+ mock_connection
74
+ )
75
+ # The resubscribe should be scheduled via _schedule_coroutine
76
+ # We need to wait for it or check the internal state
77
+
78
+ @pytest.mark.asyncio(loop_scope="function")
79
+ async def test_resubscribe_before_queued_commands(
80
+ self, auth_client_with_valid_tokens
81
+ ):
82
+ """Resubscribe completes before queued commands are sent."""
83
+ client = NavienMqttClient(auth_client_with_valid_tokens)
84
+
85
+ # Track call order
86
+ call_order = []
87
+
88
+ # Mock the components
89
+ mock_subscription_manager = MagicMock()
90
+ mock_subscription_manager.resubscribe_all = AsyncMock(
91
+ side_effect=lambda: call_order.append("resubscribe")
92
+ )
93
+ client._subscription_manager = mock_subscription_manager
94
+
95
+ mock_connection_manager = MagicMock()
96
+ mock_connection = MagicMock()
97
+ mock_connection_manager.connection = mock_connection
98
+ client._connection_manager = mock_connection_manager
99
+
100
+ # Mock command queue
101
+ client._command_queue = AsyncMock()
102
+ client.config.enable_command_queue = True
103
+
104
+ # Mock send_queued_commands to track it's called after resubscribe
105
+ original_send = client._send_queued_commands_internal
106
+
107
+ async def mock_send():
108
+ call_order.append("send_queued")
109
+ await original_send()
110
+
111
+ client._send_queued_commands_internal = mock_send
112
+
113
+ # Call the method
114
+ await client._handle_clean_session_resume()
115
+
116
+ # Verify subscription manager was updated with connection
117
+ mock_subscription_manager.update_connection.assert_called_once_with(
118
+ mock_connection
119
+ )
120
+
121
+ # Verify resubscribe was called before queued commands
122
+ assert call_order == ["resubscribe", "send_queued"]
123
+
124
+ @pytest.mark.asyncio(loop_scope="function")
125
+ async def test_skip_when_no_subscription_manager(
126
+ self, auth_client_with_valid_tokens
127
+ ):
128
+ """Return early if subscription_manager is None."""
129
+ client = NavienMqttClient(auth_client_with_valid_tokens)
130
+ client._subscription_manager = None
131
+
132
+ # Should not raise
133
+ await client._handle_clean_session_resume()
134
+
135
+ @pytest.mark.asyncio(loop_scope="function")
136
+ async def test_handle_clean_session_resume_skips_when_no_connection(
137
+ self, auth_client_with_valid_tokens
138
+ ):
139
+ """Return early if connection is None."""
140
+ client = NavienMqttClient(auth_client_with_valid_tokens)
141
+
142
+ mock_subscription_manager = MagicMock()
143
+ client._subscription_manager = mock_subscription_manager
144
+
145
+ mock_connection_manager = MagicMock()
146
+ mock_connection_manager.connection = None
147
+ client._connection_manager = mock_connection_manager
148
+
149
+ # Should not raise
150
+ await client._handle_clean_session_resume()
151
+
152
+ # Should not try to update connection
153
+ mock_subscription_manager.update_connection.assert_not_called()
154
+
155
+ @pytest.mark.asyncio(loop_scope="function")
156
+ async def test_on_connection_resumed_with_session_sends_queued_commands(
157
+ self, auth_client_with_valid_tokens
158
+ ):
159
+ """Send queued commands normally when session_present=True."""
160
+ client = NavienMqttClient(auth_client_with_valid_tokens)
161
+
162
+ # Mock the components
163
+ mock_command_queue = AsyncMock()
164
+ client._command_queue = mock_command_queue
165
+ client.config.enable_command_queue = True
166
+
167
+ # Mock the event emitter and diagnostics
168
+ client.emit = AsyncMock()
169
+ client._diagnostics = MagicMock()
170
+ client._diagnostics.record_connection_success = AsyncMock()
171
+
172
+ # Mock connection
173
+ mock_connection = MagicMock()
174
+
175
+ # Patch _send_queued_commands_internal to track if called
176
+ with patch.object(
177
+ client, "_send_queued_commands_internal", new_callable=AsyncMock
178
+ ):
179
+ # Call with session_present=True (session resumed)
180
+ client._on_connection_resumed_internal(
181
+ connection=mock_connection, return_code=0, session_present=True
182
+ )
183
+
184
+ # Give the scheduled coroutine time to run
185
+ import asyncio
186
+
187
+ await asyncio.sleep(0.1)
188
+
189
+ # Verify send_queued_commands_internal was scheduled
190
+ # (it will be called through _schedule_coroutine)