nwp500-python 8.1.2__tar.gz → 8.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/CHANGELOG.rst +18 -1
  2. {nwp500_python-8.1.2/src/nwp500_python.egg-info → nwp500_python-8.1.3}/PKG-INFO +1 -3
  3. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/README.rst +0 -2
  4. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/mqtt/reconnection.py +37 -4
  5. {nwp500_python-8.1.2 → nwp500_python-8.1.3/src/nwp500_python.egg-info}/PKG-INFO +1 -3
  6. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_mqtt_reconnection_storm.py +155 -9
  7. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/.coveragerc +0 -0
  8. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  9. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/.github/copilot-instructions.md +0 -0
  10. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/.github/workflows/ci.yml +0 -0
  11. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/.github/workflows/release.yml +0 -0
  12. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/.gitignore +0 -0
  13. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/.pre-commit-config.yaml +0 -0
  14. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/.readthedocs.yml +0 -0
  15. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/AUTHORS.rst +0 -0
  16. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/CONTRIBUTING.rst +0 -0
  17. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/LICENSE.txt +0 -0
  18. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/Makefile +0 -0
  19. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/RELEASE.md +0 -0
  20. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/Makefile +0 -0
  21. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/_static/.gitignore +0 -0
  22. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/conf.py +0 -0
  23. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/explanation/advanced-features.rst +0 -0
  24. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/explanation/architecture.rst +0 -0
  25. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/explanation/index.rst +0 -0
  26. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/how-to/authenticate.rst +0 -0
  27. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/how-to/auto-recovery.rst +0 -0
  28. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/how-to/diagnose-mqtt.rst +0 -0
  29. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/how-to/home-assistant.rst +0 -0
  30. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/how-to/index.rst +0 -0
  31. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/how-to/maintenance.rst +0 -0
  32. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/how-to/manage-units.rst +0 -0
  33. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/how-to/monitor-status.rst +0 -0
  34. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/how-to/optimize-tou.rst +0 -0
  35. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/how-to/queue-commands.rst +0 -0
  36. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/how-to/schedule-operation.rst +0 -0
  37. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/how-to/track-energy.rst +0 -0
  38. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/index.rst +0 -0
  39. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/openapi.yaml +0 -0
  40. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/project/authors.rst +0 -0
  41. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/project/changelog.rst +0 -0
  42. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/project/contributing.rst +0 -0
  43. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/project/history.rst +0 -0
  44. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/project/license.rst +0 -0
  45. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/configuration.rst +0 -0
  46. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/enumerations.rst +0 -0
  47. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/index.rst +0 -0
  48. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/installation.rst +0 -0
  49. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/protocol/data_conversions.rst +0 -0
  50. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/protocol/device_features.rst +0 -0
  51. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/protocol/device_status.rst +0 -0
  52. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/protocol/error_codes.rst +0 -0
  53. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  54. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/protocol/quick_reference.rst +0 -0
  55. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/protocol/rest_api.rst +0 -0
  56. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/python_api/api_client.rst +0 -0
  57. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/python_api/auth_client.rst +0 -0
  58. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/python_api/cli.rst +0 -0
  59. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/python_api/events.rst +0 -0
  60. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/python_api/exceptions.rst +0 -0
  61. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/python_api/models.rst +0 -0
  62. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/reference/python_api/mqtt_client.rst +0 -0
  63. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/requirements.txt +0 -0
  64. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/docs/tutorials/getting-started.rst +0 -0
  65. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/.ruff.toml +0 -0
  66. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/README.md +0 -0
  67. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/air_filter_reset.py +0 -0
  68. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/anti_legionella.py +0 -0
  69. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/auto_recovery.py +0 -0
  70. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/combined_callbacks.py +0 -0
  71. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/demand_response.py +0 -0
  72. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/device_capabilities.py +0 -0
  73. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/device_status_debug.py +0 -0
  74. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/energy_analytics.py +0 -0
  75. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/error_code_demo.py +0 -0
  76. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/firmware_payload_capture.py +0 -0
  77. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/mqtt_diagnostics.py +0 -0
  78. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/power_control.py +0 -0
  79. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/recirculation_control.py +0 -0
  80. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/reconnection_demo.py +0 -0
  81. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/reservation_schedule.py +0 -0
  82. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/simple_auto_recovery.py +0 -0
  83. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/token_restoration.py +0 -0
  84. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/tou_openei.py +0 -0
  85. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/tou_schedule.py +0 -0
  86. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/advanced/water_reservation.py +0 -0
  87. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/beginner/01_authentication.py +0 -0
  88. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/beginner/02_list_devices.py +0 -0
  89. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/beginner/03_get_status.py +0 -0
  90. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/beginner/04_set_temperature.py +0 -0
  91. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/intermediate/advanced_auth_patterns.py +0 -0
  92. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/intermediate/command_queue.py +0 -0
  93. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/intermediate/device_status_callback.py +0 -0
  94. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/intermediate/error_handling.py +0 -0
  95. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/intermediate/event_driven_control.py +0 -0
  96. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/intermediate/improved_auth.py +0 -0
  97. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/intermediate/legacy_auth_constructor.py +0 -0
  98. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  99. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/intermediate/periodic_requests.py +0 -0
  100. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/intermediate/set_mode.py +0 -0
  101. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/intermediate/vacation_mode.py +0 -0
  102. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/mask.py +0 -0
  103. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/testing/periodic_device_info.py +0 -0
  104. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/testing/simple_periodic_info.py +0 -0
  105. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/testing/test_api_client.py +0 -0
  106. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/testing/test_mqtt_connection.py +0 -0
  107. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/testing/test_mqtt_messaging.py +0 -0
  108. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/examples/testing/test_periodic_minimal.py +0 -0
  109. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/pyproject.toml +0 -0
  110. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/scripts/README.md +0 -0
  111. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/scripts/bump_version.py +0 -0
  112. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/scripts/diagnose_mqtt_connection.py +0 -0
  113. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/scripts/extract_changelog.py +0 -0
  114. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/scripts/format.py +0 -0
  115. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/scripts/lint.py +0 -0
  116. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/scripts/setup-dev.py +0 -0
  117. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/scripts/validate_version.py +0 -0
  118. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/setup.cfg +0 -0
  119. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/setup.py +0 -0
  120. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/__init__.py +0 -0
  121. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/_base.py +0 -0
  122. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/api_client.py +0 -0
  123. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/auth.py +0 -0
  124. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/cli/__init__.py +0 -0
  125. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/cli/__main__.py +0 -0
  126. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/cli/commands.py +0 -0
  127. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/cli/handlers.py +0 -0
  128. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/cli/monitoring.py +0 -0
  129. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/cli/output_formatters.py +0 -0
  130. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/cli/rich_output.py +0 -0
  131. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/cli/token_storage.py +0 -0
  132. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/command_decorators.py +0 -0
  133. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/config.py +0 -0
  134. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/converters.py +0 -0
  135. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/device_capabilities.py +0 -0
  136. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/device_info_cache.py +0 -0
  137. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/encoding.py +0 -0
  138. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/enums.py +0 -0
  139. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/events.py +0 -0
  140. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/exceptions.py +0 -0
  141. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/factory.py +0 -0
  142. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/field_factory.py +0 -0
  143. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/models/__init__.py +0 -0
  144. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/models/_converters.py +0 -0
  145. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/models/device.py +0 -0
  146. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/models/energy.py +0 -0
  147. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/models/feature.py +0 -0
  148. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/models/mqtt_models.py +0 -0
  149. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/models/schedule.py +0 -0
  150. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/models/status.py +0 -0
  151. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/models/tou.py +0 -0
  152. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/mqtt/__init__.py +0 -0
  153. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/mqtt/client.py +0 -0
  154. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/mqtt/command_queue.py +0 -0
  155. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/mqtt/connection.py +0 -0
  156. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/mqtt/control.py +0 -0
  157. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/mqtt/diagnostics.py +0 -0
  158. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/mqtt/periodic.py +0 -0
  159. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/mqtt/state_tracker.py +0 -0
  160. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/mqtt/subscriptions.py +0 -0
  161. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/mqtt/utils.py +0 -0
  162. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/mqtt_events.py +0 -0
  163. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/openei.py +0 -0
  164. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/py.typed +0 -0
  165. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/reservations.py +0 -0
  166. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/temperature.py +0 -0
  167. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/topic_builder.py +0 -0
  168. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/unit_system.py +0 -0
  169. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500/utils.py +0 -0
  170. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500_python.egg-info/SOURCES.txt +0 -0
  171. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  172. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  173. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  174. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500_python.egg-info/requires.txt +0 -0
  175. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/src/nwp500_python.egg-info/top_level.txt +0 -0
  176. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/conftest.py +0 -0
  177. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_api_helpers.py +0 -0
  178. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_auth.py +0 -0
  179. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_bug_fixes.py +0 -0
  180. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_cli_basic.py +0 -0
  181. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_cli_commands.py +0 -0
  182. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_command_decorators.py +0 -0
  183. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_command_queue.py +0 -0
  184. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_device_capabilities.py +0 -0
  185. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_device_info_cache.py +0 -0
  186. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_events.py +0 -0
  187. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_exceptions.py +0 -0
  188. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_model_converters.py +0 -0
  189. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_models.py +0 -0
  190. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_mqtt_clean_session_resume.py +0 -0
  191. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_mqtt_client_init.py +0 -0
  192. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_mqtt_hypothesis.py +0 -0
  193. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_mqtt_reconnection.py +0 -0
  194. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_multi_device.py +0 -0
  195. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_openei.py +0 -0
  196. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_reservations.py +0 -0
  197. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_temperature_converters.py +0 -0
  198. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_tou_api.py +0 -0
  199. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_unit_switching.py +0 -0
  200. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tests/test_utils.py +0 -0
  201. {nwp500_python-8.1.2 → nwp500_python-8.1.3}/tox.ini +0 -0
@@ -5,9 +5,26 @@ Changelog
5
5
  Unreleased
6
6
  ==========
7
7
 
8
- Version 8.1.2 (2026-05-25)
8
+ Version 8.1.3 (2026-06-15)
9
9
  ==========================
10
10
 
11
+ Bug Fixes
12
+ ---------
13
+ - **Fix MQTT reconnection storm caused by non-thread-safe Task.cancel()**: The
14
+ ``on_connection_resumed`` callback is invoked from an AWS IoT SDK background
15
+ thread. It was calling ``asyncio.Task.cancel()`` directly on that thread, which
16
+ is not thread-safe. When the event loop was busy at the moment of cancellation
17
+ (e.g. the sleeping task's timer callback had already been enqueued), the
18
+ cancellation was silently dropped. The stale ``_reconnect_with_backoff`` task
19
+ would then complete its sleep, call ``_reconnect_func``, and tear down an
20
+ otherwise healthy connection — restarting the entire disconnect → reconnect →
21
+ ``AWS_ERROR_MQTT_UNEXPECTED_HANGUP`` cycle. Fixed by replacing the direct
22
+ ``task.cancel()`` call with a ``_cancel_pending_reconnect()`` coroutine scheduled
23
+ via ``_schedule_coroutine``, so the cancellation runs on the event loop where
24
+ asyncio operations are safe. The method also uses an identity check before
25
+ clearing ``_reconnect_task`` to avoid wiping a newer task created during the
26
+ await, and clears stale references to already-done tasks.
27
+
11
28
  Version 8.1.0 (2026-05-16)
12
29
  ==========================
13
30
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 8.1.2
3
+ Version: 8.1.3
4
4
  Summary: A library for controlling Navien NWP500 Water Heaters via NaviLink
5
5
  Home-page: https://github.com/eman/nwp500-python
6
6
  Author: Emmanuel Levijarvi
@@ -89,8 +89,6 @@ Quick Example
89
89
  Documentation
90
90
  =============
91
91
 
92
- The documentation follows the `Diátaxis <https://diataxis.fr/>`_ framework:
93
-
94
92
  * `Tutorials <https://nwp500-python.readthedocs.io/en/latest/tutorials/getting-started.html>`_: Start here if you're new to the library.
95
93
  * `How-to Guides <https://nwp500-python.readthedocs.io/en/latest/how-to/index.html>`_: Practical step-by-step recipes for specific tasks.
96
94
  * `Reference <https://nwp500-python.readthedocs.io/en/latest/reference/index.html>`_: Technical descriptions of the API, models, and protocol.
@@ -47,8 +47,6 @@ Quick Example
47
47
  Documentation
48
48
  =============
49
49
 
50
- The documentation follows the `Diátaxis <https://diataxis.fr/>`_ framework:
51
-
52
50
  * `Tutorials <https://nwp500-python.readthedocs.io/en/latest/tutorials/getting-started.html>`_: Start here if you're new to the library.
53
51
  * `How-to Guides <https://nwp500-python.readthedocs.io/en/latest/how-to/index.html>`_: Practical step-by-step recipes for specific tasks.
54
52
  * `Reference <https://nwp500-python.readthedocs.io/en/latest/reference/index.html>`_: Technical descriptions of the API, models, and protocol.
@@ -128,9 +128,42 @@ class MqttReconnectionHandler:
128
128
  # Reset reconnection attempts on successful connection
129
129
  self._reconnect_attempts = 0
130
130
 
131
- # Cancel any pending reconnection task
132
- if self._reconnect_task and not self._reconnect_task.done():
133
- self._reconnect_task.cancel()
131
+ # Schedule cancellation of any pending reconnect task on the event loop.
132
+ # This method is called from an AWS SDK background thread; asyncio's
133
+ # Task.cancel() is NOT thread-safe when invoked directly from a
134
+ # non-event-loop thread. If the event loop is busy (e.g. the sleeping
135
+ # task's timer callback was already queued) the cancellation can be
136
+ # silently dropped, leaving the stale _reconnect_with_backoff loop
137
+ # alive. That loop then completes its sleep and calls _reconnect_func,
138
+ # tearing down a perfectly healthy connection and restarting the
139
+ # disconnect/reconnect cycle.
140
+ self._schedule_coroutine(self._cancel_pending_reconnect())
141
+
142
+ async def _cancel_pending_reconnect(self) -> None:
143
+ """Cancel any pending reconnect task.
144
+
145
+ Must be called on the event loop (via _schedule_coroutine) so that
146
+ asyncio Task operations are thread-safe.
147
+
148
+ Uses an identity check before clearing _reconnect_task to avoid
149
+ accidentally wiping a new task that was created while the cancelled
150
+ task was being awaited. Also clears stale references to already-done
151
+ tasks so the handler never holds on to finished task objects.
152
+ """
153
+ task = self._reconnect_task
154
+ if task is None:
155
+ return
156
+ if task.done():
157
+ # Clear stale reference to an already-finished task.
158
+ if self._reconnect_task is task:
159
+ self._reconnect_task = None
160
+ return
161
+ task.cancel()
162
+ with contextlib.suppress(asyncio.CancelledError):
163
+ await task
164
+ # Only clear the reference if it still points to the same task.
165
+ # A new reconnect task may have been created while we were awaiting.
166
+ if self._reconnect_task is task:
134
167
  self._reconnect_task = None
135
168
 
136
169
  async def _start_reconnect_task(self) -> None:
@@ -142,7 +175,7 @@ class MqttReconnectionHandler:
142
175
 
143
176
  The is_connected guard is re-checked here because this coroutine may
144
177
  be queued via run_coroutine_threadsafe and run after the connection
145
- has already been restored (e.g. by on_connection_resumed cancelling
178
+ has already been restored (e.g. by _cancel_pending_reconnect clearing
146
179
  _reconnect_task), in which case starting a new backoff loop would
147
180
  incorrectly tear down a healthy connection.
148
181
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 8.1.2
3
+ Version: 8.1.3
4
4
  Summary: A library for controlling Navien NWP500 Water Heaters via NaviLink
5
5
  Home-page: https://github.com/eman/nwp500-python
6
6
  Author: Emmanuel Levijarvi
@@ -89,8 +89,6 @@ Quick Example
89
89
  Documentation
90
90
  =============
91
91
 
92
- The documentation follows the `Diátaxis <https://diataxis.fr/>`_ framework:
93
-
94
92
  * `Tutorials <https://nwp500-python.readthedocs.io/en/latest/tutorials/getting-started.html>`_: Start here if you're new to the library.
95
93
  * `How-to Guides <https://nwp500-python.readthedocs.io/en/latest/how-to/index.html>`_: Practical step-by-step recipes for specific tasks.
96
94
  * `Reference <https://nwp500-python.readthedocs.io/en/latest/reference/index.html>`_: Technical descriptions of the API, models, and protocol.
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Tests for the MQTT reconnection storm fix.
3
3
 
4
- Two bugs were fixed:
4
+ Three bugs were fixed:
5
5
 
6
6
  Bug 1 — Stale interruption events fire after a resume clears _reconnect_task.
7
7
  The AWS SDK fires on_connection_interrupted callbacks from background threads
@@ -21,6 +21,18 @@ Bug 2 — Closing the old connection inside _active_reconnect / _deep_reconnect
21
21
  Fix: _actively_reconnecting flag suppresses the reconnection-handler
22
22
  delegation in _on_connection_interrupted_internal while the intentional
23
23
  teardown is in progress.
24
+
25
+ Bug 3 — on_connection_resumed calls Task.cancel() directly from an AWS SDK
26
+ background thread. asyncio.Task.cancel() is NOT thread-safe; when the event
27
+ loop is busy (e.g. the sleeping task's timer callback was already enqueued)
28
+ the cancellation can be silently dropped. The stale _reconnect_with_backoff
29
+ task then completes its sleep, calls _reconnect_func, and tears down an
30
+ otherwise healthy connection, restarting the entire
31
+ disconnect/reconnect cycle.
32
+
33
+ Fix: on_connection_resumed schedules _cancel_pending_reconnect() via
34
+ _schedule_coroutine so the cancellation runs on the event loop where asyncio
35
+ Task operations are safe.
24
36
  """
25
37
 
26
38
  from __future__ import annotations
@@ -177,10 +189,11 @@ class TestReconnectionHandlerIsConnectedGuard:
177
189
 
178
190
  1. Connection drops → on_connection_interrupted schedules
179
191
  _start_reconnect_task (coroutine A queued but not yet run).
180
- 2. Connection resumes → on_connection_resumed cancels task, sets
181
- _reconnect_task = None.
182
- 3. Coroutine A finally runs – without the fix it would see
183
- _reconnect_task=None and create a new backoff loop.
192
+ 2. Connection resumes → on_connection_resumed schedules
193
+ _cancel_pending_reconnect (no task to cancel yet since A hasn't run).
194
+ 3. Both queued coroutines finally run – without the is_connected guard
195
+ coroutine A would see _reconnect_task=None and create a new backoff
196
+ loop even though the client is now healthy.
184
197
  """
185
198
  state = {"connected": False}
186
199
  config = MqttConnectionConfig(auto_reconnect=True)
@@ -200,13 +213,16 @@ class TestReconnectionHandlerIsConnectedGuard:
200
213
  handler.on_connection_interrupted(Exception("dropped"))
201
214
  assert len(scheduled) == 1
202
215
 
203
- # Step 2: connection resumes before the scheduled coroutine runs
216
+ # Step 2: connection resumes before the scheduled coroutine runs.
217
+ # on_connection_resumed now schedules _cancel_pending_reconnect rather
218
+ # than calling task.cancel() directly (Bug 3 fix).
204
219
  state["connected"] = True
205
220
  handler.on_connection_resumed(return_code=0, session_present=False)
206
- assert handler._reconnect_task is None
221
+ assert handler._reconnect_task is None # no task was ever created
207
222
 
208
- # Step 3: the stale coroutine from step 1 runs now
209
- await scheduled[0]
223
+ # Step 3: run all scheduled coroutines
224
+ # (_start_reconnect_task + _cancel_pending_reconnect)
225
+ await asyncio.gather(*scheduled, return_exceptions=True)
210
226
 
211
227
  # With the fix, no new task must have been created
212
228
  assert handler._reconnect_task is None
@@ -232,6 +248,136 @@ class TestReconnectionHandlerIsConnectedGuard:
232
248
  await asyncio.gather(handler._reconnect_task, return_exceptions=True)
233
249
 
234
250
 
251
+ # ---------------------------------------------------------------------------
252
+ # Bug 3: on_connection_resumed must cancel via the event loop, not directly
253
+ # ---------------------------------------------------------------------------
254
+
255
+
256
+ class TestThreadSafeTaskCancellation:
257
+ """Bug 3 – on_connection_resumed must not call Task.cancel() from a thread.
258
+
259
+ asyncio.Task.cancel() is NOT thread-safe. When called from an AWS SDK
260
+ background thread, the cancellation can be silently dropped if the event
261
+ loop is busy (e.g. the sleep timer fires at the same moment). The stale
262
+ task then triggers a spurious reconnection.
263
+
264
+ Fix: on_connection_resumed schedules _cancel_pending_reconnect() via
265
+ _schedule_coroutine so the cancellation happens on the event loop.
266
+ """
267
+
268
+ @pytest.mark.asyncio(loop_scope="function")
269
+ async def test_on_connection_resumed_schedules_cancel_not_direct_call(
270
+ self,
271
+ ):
272
+ """
273
+ on_connection_resumed must schedule _cancel_pending_reconnect via
274
+ _schedule_coroutine rather than calling task.cancel() directly.
275
+ """
276
+ handler, scheduled = _make_handler(connected=False)
277
+
278
+ # Let a reconnect task start (sleeping in backoff)
279
+ handler.on_connection_interrupted(Exception("dropped"))
280
+ await asyncio.gather(*scheduled, return_exceptions=True)
281
+ scheduled.clear()
282
+
283
+ assert handler._reconnect_task is not None
284
+ assert not handler._reconnect_task.done()
285
+
286
+ # Simulate connection resuming from a background thread
287
+ handler.on_connection_resumed(return_code=0, session_present=True)
288
+
289
+ # _cancel_pending_reconnect must have been *scheduled*, not run yet
290
+ assert len(scheduled) == 1
291
+ # No direct Task.cancel() was called from the background thread:
292
+ # task.cancelling() == 0 proves no cancellation request is pending
293
+ # before the event loop runs _cancel_pending_reconnect.
294
+ assert handler._reconnect_task is not None
295
+ assert handler._reconnect_task.cancelling() == 0
296
+
297
+ # Now let the event loop process the cancellation
298
+ await asyncio.gather(*scheduled, return_exceptions=True)
299
+
300
+ # After the event-loop cancellation, the task must be cleared
301
+ assert handler._reconnect_task is None
302
+
303
+ @pytest.mark.asyncio(loop_scope="function")
304
+ async def test_cancel_pending_reconnect_is_idempotent_with_no_task(self):
305
+ """_cancel_pending_reconnect is a no-op when _reconnect_task is None."""
306
+ handler, _ = _make_handler(connected=True)
307
+ assert handler._reconnect_task is None
308
+
309
+ # Should not raise
310
+ await handler._cancel_pending_reconnect()
311
+
312
+ assert handler._reconnect_task is None
313
+
314
+ @pytest.mark.asyncio(loop_scope="function")
315
+ async def test_cancel_pending_reconnect_clears_completed_task(self):
316
+ """_cancel_pending_reconnect clears a stale reference to a done task."""
317
+ handler, scheduled = _make_handler(connected=False)
318
+
319
+ handler.on_connection_interrupted(Exception("dropped"))
320
+ await asyncio.gather(*scheduled, return_exceptions=True)
321
+ scheduled.clear()
322
+
323
+ task = handler._reconnect_task
324
+ assert task is not None
325
+
326
+ # Cancel and drain the task manually, leaving a stale reference
327
+ task.cancel()
328
+ await asyncio.gather(task, return_exceptions=True)
329
+ assert task.done()
330
+ assert handler._reconnect_task is task # stale reference still held
331
+
332
+ # _cancel_pending_reconnect must clear the stale reference
333
+ await handler._cancel_pending_reconnect()
334
+ assert handler._reconnect_task is None
335
+
336
+ @pytest.mark.asyncio(loop_scope="function")
337
+ async def test_resumed_then_interrupted_creates_new_task(self):
338
+ """
339
+ After resume cancels an existing task, a subsequent genuine drop must
340
+ still be able to start a fresh reconnect task.
341
+ """
342
+ state = {"connected": False}
343
+ config = MqttConnectionConfig(auto_reconnect=True)
344
+ scheduled = []
345
+
346
+ handler = MqttReconnectionHandler(
347
+ config=config,
348
+ is_connected_func=lambda: state["connected"],
349
+ schedule_coroutine_func=lambda coro: scheduled.append(
350
+ asyncio.ensure_future(coro)
351
+ ),
352
+ reconnect_func=AsyncMock(),
353
+ )
354
+ handler.enable()
355
+
356
+ # Connection drops → backoff task starts
357
+ handler.on_connection_interrupted(Exception("first drop"))
358
+ await asyncio.gather(*scheduled, return_exceptions=True)
359
+ scheduled.clear()
360
+ assert handler._reconnect_task is not None
361
+
362
+ # Connection resumes → cancel scheduled on loop
363
+ state["connected"] = True
364
+ handler.on_connection_resumed(return_code=0, session_present=True)
365
+ await asyncio.gather(*scheduled, return_exceptions=True)
366
+ scheduled.clear()
367
+ assert handler._reconnect_task is None
368
+
369
+ # Connection drops again (genuine)
370
+ state["connected"] = False
371
+ handler.on_connection_interrupted(Exception("second drop"))
372
+ await asyncio.gather(*scheduled, return_exceptions=True)
373
+ scheduled.clear()
374
+
375
+ # A new reconnect task must have been created
376
+ assert handler._reconnect_task is not None
377
+ handler._reconnect_task.cancel()
378
+ await asyncio.gather(handler._reconnect_task, return_exceptions=True)
379
+
380
+
235
381
  # ---------------------------------------------------------------------------
236
382
  # Bug 2: _actively_reconnecting suppresses spurious interrupt callbacks
237
383
  # ---------------------------------------------------------------------------
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes