nwp500-python 8.0.0__tar.gz → 8.1.1__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 (200) hide show
  1. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/CHANGELOG.rst +67 -2
  2. {nwp500_python-8.0.0/src/nwp500_python.egg-info → nwp500_python-8.1.1}/PKG-INFO +1 -1
  3. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/scripts/bump_version.py +77 -0
  4. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/device_info_cache.py +9 -6
  5. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/encoding.py +30 -8
  6. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/events.py +1 -1
  7. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/mqtt/client.py +60 -18
  8. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/mqtt/connection.py +38 -0
  9. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/mqtt/diagnostics.py +6 -1
  10. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/mqtt/periodic.py +6 -0
  11. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/mqtt/reconnection.py +17 -2
  12. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/mqtt/utils.py +3 -1
  13. {nwp500_python-8.0.0 → nwp500_python-8.1.1/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  14. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500_python.egg-info/SOURCES.txt +3 -0
  15. nwp500_python-8.1.1/tests/test_bug_fixes.py +198 -0
  16. nwp500_python-8.1.1/tests/test_mqtt_reconnection.py +130 -0
  17. nwp500_python-8.1.1/tests/test_mqtt_reconnection_storm.py +447 -0
  18. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/.coveragerc +0 -0
  19. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  20. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/.github/copilot-instructions.md +0 -0
  21. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/.github/workflows/ci.yml +0 -0
  22. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/.github/workflows/release.yml +0 -0
  23. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/.gitignore +0 -0
  24. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/.pre-commit-config.yaml +0 -0
  25. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/.readthedocs.yml +0 -0
  26. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/AUTHORS.rst +0 -0
  27. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/CONTRIBUTING.rst +0 -0
  28. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/LICENSE.txt +0 -0
  29. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/Makefile +0 -0
  30. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/README.rst +0 -0
  31. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/RELEASE.md +0 -0
  32. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/Makefile +0 -0
  33. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/_static/.gitignore +0 -0
  34. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/conf.py +0 -0
  35. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/explanation/advanced-features.rst +0 -0
  36. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/explanation/architecture.rst +0 -0
  37. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/explanation/index.rst +0 -0
  38. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/how-to/authenticate.rst +0 -0
  39. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/how-to/auto-recovery.rst +0 -0
  40. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/how-to/diagnose-mqtt.rst +0 -0
  41. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/how-to/home-assistant.rst +0 -0
  42. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/how-to/index.rst +0 -0
  43. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/how-to/maintenance.rst +0 -0
  44. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/how-to/manage-units.rst +0 -0
  45. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/how-to/monitor-status.rst +0 -0
  46. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/how-to/optimize-tou.rst +0 -0
  47. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/how-to/queue-commands.rst +0 -0
  48. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/how-to/schedule-operation.rst +0 -0
  49. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/how-to/track-energy.rst +0 -0
  50. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/index.rst +0 -0
  51. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/openapi.yaml +0 -0
  52. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/project/authors.rst +0 -0
  53. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/project/changelog.rst +0 -0
  54. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/project/contributing.rst +0 -0
  55. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/project/history.rst +0 -0
  56. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/project/license.rst +0 -0
  57. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/configuration.rst +0 -0
  58. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/enumerations.rst +0 -0
  59. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/index.rst +0 -0
  60. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/installation.rst +0 -0
  61. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/protocol/data_conversions.rst +0 -0
  62. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/protocol/device_features.rst +0 -0
  63. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/protocol/device_status.rst +0 -0
  64. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/protocol/error_codes.rst +0 -0
  65. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  66. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/protocol/quick_reference.rst +0 -0
  67. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/protocol/rest_api.rst +0 -0
  68. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/python_api/api_client.rst +0 -0
  69. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/python_api/auth_client.rst +0 -0
  70. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/python_api/cli.rst +0 -0
  71. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/python_api/events.rst +0 -0
  72. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/python_api/exceptions.rst +0 -0
  73. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/python_api/models.rst +0 -0
  74. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/reference/python_api/mqtt_client.rst +0 -0
  75. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/requirements.txt +0 -0
  76. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/docs/tutorials/getting-started.rst +0 -0
  77. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/.ruff.toml +0 -0
  78. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/README.md +0 -0
  79. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/air_filter_reset.py +0 -0
  80. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/anti_legionella.py +0 -0
  81. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/auto_recovery.py +0 -0
  82. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/combined_callbacks.py +0 -0
  83. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/demand_response.py +0 -0
  84. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/device_capabilities.py +0 -0
  85. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/device_status_debug.py +0 -0
  86. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/energy_analytics.py +0 -0
  87. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/error_code_demo.py +0 -0
  88. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/firmware_payload_capture.py +0 -0
  89. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/mqtt_diagnostics.py +0 -0
  90. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/power_control.py +0 -0
  91. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/recirculation_control.py +0 -0
  92. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/reconnection_demo.py +0 -0
  93. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/reservation_schedule.py +0 -0
  94. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/simple_auto_recovery.py +0 -0
  95. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/token_restoration.py +0 -0
  96. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/tou_openei.py +0 -0
  97. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/tou_schedule.py +0 -0
  98. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/advanced/water_reservation.py +0 -0
  99. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/beginner/01_authentication.py +0 -0
  100. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/beginner/02_list_devices.py +0 -0
  101. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/beginner/03_get_status.py +0 -0
  102. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/beginner/04_set_temperature.py +0 -0
  103. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/intermediate/advanced_auth_patterns.py +0 -0
  104. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/intermediate/command_queue.py +0 -0
  105. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/intermediate/device_status_callback.py +0 -0
  106. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/intermediate/error_handling.py +0 -0
  107. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/intermediate/event_driven_control.py +0 -0
  108. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/intermediate/improved_auth.py +0 -0
  109. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/intermediate/legacy_auth_constructor.py +0 -0
  110. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  111. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/intermediate/periodic_requests.py +0 -0
  112. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/intermediate/set_mode.py +0 -0
  113. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/intermediate/vacation_mode.py +0 -0
  114. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/mask.py +0 -0
  115. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/testing/periodic_device_info.py +0 -0
  116. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/testing/simple_periodic_info.py +0 -0
  117. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/testing/test_api_client.py +0 -0
  118. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/testing/test_mqtt_connection.py +0 -0
  119. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/testing/test_mqtt_messaging.py +0 -0
  120. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/examples/testing/test_periodic_minimal.py +0 -0
  121. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/pyproject.toml +0 -0
  122. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/scripts/README.md +0 -0
  123. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/scripts/diagnose_mqtt_connection.py +0 -0
  124. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/scripts/extract_changelog.py +0 -0
  125. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/scripts/format.py +0 -0
  126. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/scripts/lint.py +0 -0
  127. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/scripts/setup-dev.py +0 -0
  128. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/scripts/validate_version.py +0 -0
  129. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/setup.cfg +0 -0
  130. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/setup.py +0 -0
  131. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/__init__.py +0 -0
  132. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/_base.py +0 -0
  133. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/api_client.py +0 -0
  134. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/auth.py +0 -0
  135. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/cli/__init__.py +0 -0
  136. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/cli/__main__.py +0 -0
  137. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/cli/commands.py +0 -0
  138. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/cli/handlers.py +0 -0
  139. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/cli/monitoring.py +0 -0
  140. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/cli/output_formatters.py +0 -0
  141. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/cli/rich_output.py +0 -0
  142. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/cli/token_storage.py +0 -0
  143. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/command_decorators.py +0 -0
  144. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/config.py +0 -0
  145. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/converters.py +0 -0
  146. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/device_capabilities.py +0 -0
  147. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/enums.py +0 -0
  148. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/exceptions.py +0 -0
  149. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/factory.py +0 -0
  150. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/field_factory.py +0 -0
  151. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/models/__init__.py +0 -0
  152. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/models/_converters.py +0 -0
  153. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/models/device.py +0 -0
  154. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/models/energy.py +0 -0
  155. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/models/feature.py +0 -0
  156. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/models/mqtt_models.py +0 -0
  157. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/models/schedule.py +0 -0
  158. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/models/status.py +0 -0
  159. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/models/tou.py +0 -0
  160. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/mqtt/__init__.py +0 -0
  161. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/mqtt/command_queue.py +0 -0
  162. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/mqtt/control.py +0 -0
  163. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/mqtt/state_tracker.py +0 -0
  164. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/mqtt/subscriptions.py +0 -0
  165. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/mqtt_events.py +0 -0
  166. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/openei.py +0 -0
  167. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/py.typed +0 -0
  168. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/reservations.py +0 -0
  169. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/temperature.py +0 -0
  170. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/topic_builder.py +0 -0
  171. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/unit_system.py +0 -0
  172. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500/utils.py +0 -0
  173. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  174. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  175. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  176. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500_python.egg-info/requires.txt +0 -0
  177. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/src/nwp500_python.egg-info/top_level.txt +0 -0
  178. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/conftest.py +0 -0
  179. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_api_helpers.py +0 -0
  180. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_auth.py +0 -0
  181. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_cli_basic.py +0 -0
  182. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_cli_commands.py +0 -0
  183. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_command_decorators.py +0 -0
  184. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_command_queue.py +0 -0
  185. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_device_capabilities.py +0 -0
  186. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_device_info_cache.py +0 -0
  187. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_events.py +0 -0
  188. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_exceptions.py +0 -0
  189. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_model_converters.py +0 -0
  190. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_models.py +0 -0
  191. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_mqtt_client_init.py +0 -0
  192. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_mqtt_hypothesis.py +0 -0
  193. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_multi_device.py +0 -0
  194. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_openei.py +0 -0
  195. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_reservations.py +0 -0
  196. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_temperature_converters.py +0 -0
  197. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_tou_api.py +0 -0
  198. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_unit_switching.py +0 -0
  199. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tests/test_utils.py +0 -0
  200. {nwp500_python-8.0.0 → nwp500_python-8.1.1}/tox.ini +0 -0
@@ -2,8 +2,73 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
- Unreleased (8.x)
6
- ================
5
+ Unreleased
6
+ ==========
7
+
8
+ Version 8.1.1 (2026-05-18)
9
+ ==========================
10
+
11
+ Version 8.1.0 (2026-05-16)
12
+ ==========================
13
+
14
+ Bug Fixes
15
+ ---------
16
+ - **Fix MQTT connection flapping after reconnect**: When ``_active_reconnect()``
17
+ created a new ``MqttConnection``, the old connection was never closed. The old
18
+ SDK connection's built-in auto-reconnect would eventually succeed, creating two
19
+ active connections sharing the same client ID. Because AWS IoT allows only one
20
+ connection per client ID, the broker would kick one off, triggering
21
+ ``on_connection_interrupted`` and starting yet another reconnection — an
22
+ infinite connect/disconnect loop. Fixed by adding ``MqttConnection.close()``
23
+ (unconditional teardown regardless of ``_connected`` state) and calling it
24
+ before creating the replacement connection in both ``_active_reconnect()`` and
25
+ ``_deep_reconnect()``.
26
+
27
+ - **Thread-safety race in ``ensure_device_info_cached``**: The ``future.done()``
28
+ check and ``future.set_result()`` were performed in the AWS SDK callback thread
29
+ without synchronisation, creating a race against the asyncio event loop thread.
30
+ Moved both operations inside a ``call_soon_threadsafe`` callback so they execute
31
+ atomically on the event loop thread.
32
+
33
+ - **ZeroDivisionError when ``deep_reconnect_threshold`` is 0**: Config validation
34
+ now clamps ``deep_reconnect_threshold`` to a minimum of 1, preventing a
35
+ ``ZeroDivisionError`` in the exponential-backoff reconnection logic.
36
+
37
+ - **Reconnect counter never incremented**: ``total_reconnect_attempts`` in
38
+ diagnostics was not incremented on connection drops, so it always reported 0
39
+ despite active reconnections. Counter is now incremented on each
40
+ ``on_connection_interrupted`` event.
41
+
42
+ - **``shortest_session_seconds`` not JSON-serialisable**: The diagnostics
43
+ ``to_dict()`` method used ``float('inf')`` as the initial value for
44
+ ``shortest_session_seconds``, which is not valid JSON. Changed to ``None``
45
+ so serialisation succeeds when no session has completed yet.
46
+
47
+ - **``wait_for()`` future not bound to running loop**: ``wait_for()`` created a
48
+ bare ``asyncio.Future()`` rather than
49
+ ``asyncio.get_running_loop().create_future()``, which could bind the future to
50
+ a different loop in multi-loop test setups.
51
+
52
+ - **Reservation temperature validation was US-only**: ``build_reservation_entry``
53
+ validated set-point temperatures against hardcoded Fahrenheit bounds (95–150 °F)
54
+ regardless of the active unit system. Validation now uses the current unit system
55
+ context: 35–65 °C in metric mode, 95–150 °F in US mode. Celsius users previously
56
+ received spurious ``ValueError`` rejections for valid temperatures.
57
+
58
+ - **Malformed reservation data silently dropped**: ``build_reservation_entry`` now
59
+ logs a warning when reservation hex data contains unexpected trailing bytes
60
+ instead of silently dropping partial entries.
61
+
62
+ - **Unknown ``PeriodicRequestType`` silently ignored**: The periodic-request handler
63
+ now logs an error and breaks when it encounters an unknown request type instead of
64
+ doing nothing.
65
+
66
+ - **Memory leak in device info cache**: ``get_all_cached()`` only filtered expired
67
+ entries from its return value but left them in the cache dictionary. Expired
68
+ entries are now evicted during ``get_all_cached()`` to prevent unbounded growth.
69
+
70
+ Version 8.0.0 (2026-05-13)
71
+ ===========================
7
72
 
8
73
  Bug Fixes
9
74
  ---------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 8.0.0
3
+ Version: 8.1.1
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
@@ -22,6 +22,8 @@ field is for the PyScaffold tool version, not the package version!
22
22
  import re
23
23
  import subprocess
24
24
  import sys
25
+ from datetime import date
26
+ from pathlib import Path
25
27
 
26
28
 
27
29
  def run_git_command(args: list) -> str:
@@ -140,6 +142,75 @@ def check_working_directory_clean() -> None:
140
142
  sys.exit(1)
141
143
 
142
144
 
145
+ def update_changelog(version: str) -> None:
146
+ """Insert a version heading into CHANGELOG.rst below the Unreleased section.
147
+
148
+ Transforms:
149
+
150
+ Unreleased
151
+ ==========
152
+
153
+ <content...>
154
+
155
+ into:
156
+
157
+ Unreleased
158
+ ==========
159
+
160
+ Version X.Y.Z (YYYY-MM-DD)
161
+ ===========================
162
+
163
+ <content...>
164
+ """
165
+ changelog_path = Path("CHANGELOG.rst")
166
+ if not changelog_path.exists():
167
+ print("Warning: CHANGELOG.rst not found, skipping changelog update.")
168
+ return
169
+
170
+ content = changelog_path.read_text(encoding="utf-8")
171
+
172
+ heading = f"Version {version} ({date.today().isoformat()})"
173
+ underline = "=" * len(heading)
174
+ version_block = f"{heading}\n{underline}\n"
175
+
176
+ # Match "Unreleased\n==========\n" (any number of = signs) followed by
177
+ # one or more blank lines, then insert the version block after them.
178
+ pattern = re.compile(
179
+ r"(Unreleased\n=+\n)" # group 1: Unreleased heading
180
+ r"(\n+)", # group 2: blank line(s) separator
181
+ re.MULTILINE,
182
+ )
183
+
184
+ match = pattern.search(content)
185
+ if not match:
186
+ print(
187
+ "Warning: Could not find 'Unreleased' section in CHANGELOG.rst. "
188
+ "Skipping changelog update.",
189
+ file=sys.stderr,
190
+ )
191
+ return
192
+
193
+ # Insert the version block after the blank lines that follow "Unreleased"
194
+ new_content = (
195
+ content[: match.end()]
196
+ + version_block
197
+ + "\n"
198
+ + content[match.end() :]
199
+ )
200
+
201
+ changelog_path.write_text(new_content, encoding="utf-8")
202
+ print(f"[OK] Updated CHANGELOG.rst with {heading}")
203
+
204
+
205
+ def commit_changelog(version: str) -> None:
206
+ """Stage and commit the CHANGELOG.rst update."""
207
+ run_git_command(["add", "CHANGELOG.rst"])
208
+ run_git_command(
209
+ ["commit", "-m", f"Update changelog for v{version}"]
210
+ )
211
+ print("[OK] Committed changelog update")
212
+
213
+
143
214
  def create_tag(version: str, message: str = None) -> None:
144
215
  """Create a git tag for the version."""
145
216
  tag_name = f"v{version}"
@@ -223,6 +294,11 @@ def main() -> None:
223
294
  # Validate version progression
224
295
  validate_version_progression(current_version, new_version)
225
296
 
297
+ # Update CHANGELOG.rst and commit, then create the tag
298
+ print("\nUpdating CHANGELOG.rst...")
299
+ update_changelog(new_version)
300
+ commit_changelog(new_version)
301
+
226
302
  # Create the tag
227
303
  print(f"\nCreating tag v{new_version}...")
228
304
  create_tag(new_version)
@@ -230,6 +306,7 @@ def main() -> None:
230
306
  print("\n[OK] Version bump complete!")
231
307
  print("\nNext steps:")
232
308
  print(f" 1. Push the tag: git push origin v{new_version}")
309
+ print(" (also push the changelog commit: git push origin HEAD)")
233
310
  print(" 2. Build release: make build")
234
311
  print(" 3. Test on TestPyPI: make publish-test")
235
312
  print(" 4. Publish to PyPI: make publish")
@@ -137,12 +137,15 @@ class MqttDeviceInfoCache:
137
137
  Dictionary mapping MAC addresses to DeviceFeature objects
138
138
  """
139
139
  async with self._lock:
140
- # Filter out expired entries
141
- return {
142
- mac: features
143
- for mac, (features, timestamp) in self._cache.items()
144
- if not self.is_expired(timestamp)
145
- }
140
+ # Filter out expired entries and purge them from cache
141
+ expired_keys = [
142
+ mac
143
+ for mac, (_, timestamp) in self._cache.items()
144
+ if self.is_expired(timestamp)
145
+ ]
146
+ for mac in expired_keys:
147
+ del self._cache[mac]
148
+ return {mac: features for mac, (features, _) in self._cache.items()}
146
149
 
147
150
  async def get_cache_info(
148
151
  self,
@@ -8,11 +8,14 @@ These utilities are used by both the API client and MQTT client.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import logging
11
12
  from collections.abc import Iterable
12
13
  from numbers import Real
13
14
 
14
15
  from .exceptions import ParameterValidationError, RangeValidationError
15
16
 
17
+ _logger = logging.getLogger(__name__)
18
+
16
19
  # MGPP Week Bitfield Encoding (from NaviLink APK KDEnum.MgppReservationWeek).
17
20
  # Uses a single byte where bits 1-7 represent days; bit 0 is unused.
18
21
  #
@@ -342,14 +345,18 @@ def decode_reservation_hex(hex_string: str) -> list[dict[str, int]]:
342
345
  data = bytes.fromhex(hex_string)
343
346
  reservations = []
344
347
 
348
+ if len(data) % 6 != 0:
349
+ _logger.warning(
350
+ "Reservation hex data length %d is not a multiple of 6; "
351
+ "trailing %d bytes will be ignored",
352
+ len(data),
353
+ len(data) % 6,
354
+ )
355
+
345
356
  # Process 6 bytes at a time
346
- for i in range(0, len(data), 6):
357
+ for i in range(0, len(data) - (len(data) % 6), 6):
347
358
  chunk = data[i : i + 6]
348
359
 
349
- # Ensure we have a full 6-byte entry
350
- if len(chunk) != 6:
351
- break
352
-
353
360
  # Skip empty entries (all zeros)
354
361
  if all(b == 0 for b in chunk):
355
362
  continue
@@ -425,11 +432,26 @@ def build_reservation_entry(
425
432
  """
426
433
  # Import here to avoid circular import
427
434
  from .models import preferred_to_half_celsius
435
+ from .unit_system import get_unit_system
436
+
437
+ # Read unit system once to keep min/max bounds consistent
438
+ unit_system = get_unit_system()
428
439
 
429
440
  # Use device-provided limits if available, otherwise use defaults
430
- # Defaults are conservative: 95°F / 35°C minimum, 150°F / 65°C maximum
431
- min_temp = temperature_min if temperature_min is not None else 95
432
- max_temp = temperature_max if temperature_max is not None else 150
441
+ # in the user's preferred unit system.
442
+ if temperature_min is not None:
443
+ min_temp = temperature_min
444
+ elif unit_system == "metric":
445
+ min_temp = 35.0 # ~35°C
446
+ else:
447
+ min_temp = 95.0 # 95°F
448
+
449
+ if temperature_max is not None:
450
+ max_temp = temperature_max
451
+ elif unit_system == "metric":
452
+ max_temp = 65.0 # ~65°C
453
+ else:
454
+ max_temp = 150.0 # 150°F
433
455
 
434
456
  if not 0 <= hour <= 23:
435
457
  raise RangeValidationError(
@@ -396,7 +396,7 @@ class EventEmitter:
396
396
  current_temp = temperature_event.new_temperature
397
397
  """
398
398
  future: asyncio.Future[tuple[tuple[Any, ...], dict[str, Any]]] = (
399
- asyncio.Future()
399
+ asyncio.get_running_loop().create_future()
400
400
  )
401
401
 
402
402
  def handler(*args: Any, **kwargs: Any) -> None:
@@ -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
@@ -371,26 +385,40 @@ class NavienMqttClient(EventEmitter):
371
385
  reconnect instead of passively waiting for AWS IoT SDK.
372
386
 
373
387
  Note: This creates a new connection while preserving subscriptions
374
- and configuration.
388
+ and configuration. The old connection is closed first to prevent
389
+ its SDK auto-reconnect from creating a competing connection with
390
+ the same client ID (which causes the broker to kick one off,
391
+ leading to an infinite connect/disconnect loop).
375
392
  """
376
393
  if self._connected:
377
394
  _logger.debug("Already connected, skipping reconnection")
378
395
  return
379
396
 
397
+ if self._actively_reconnecting:
398
+ _logger.debug("Active reconnection already in progress, skipping")
399
+ return
400
+
380
401
  _logger.info("Attempting active reconnection...")
381
402
 
403
+ self._actively_reconnecting = True
382
404
  try:
383
405
  # Ensure tokens are still valid
384
406
  await self._auth_client.ensure_valid_token()
385
407
 
386
408
  # If we have a connection manager, try to reconnect using it
387
409
  if self._connection_manager:
388
- # The connection might be in a bad state, so we need to
389
- # recreate the underlying connection
410
+ # Close old connection to stop SDK auto-reconnect and
411
+ # prevent two connections with the same client ID.
412
+ # _actively_reconnecting suppresses the
413
+ # on_connection_interrupted callback that closing triggers,
414
+ # preventing a competing backoff loop from being spawned.
390
415
  _logger.debug("Recreating MQTT connection...")
416
+ try:
417
+ await self._connection_manager.close()
418
+ except (AwsCrtError, RuntimeError) as e:
419
+ _logger.debug(f"Old connection cleanup (benign): {e}")
391
420
 
392
421
  # Create a new connection manager with same config
393
- old_connection_manager = self._connection_manager
394
422
  self._connection_manager = MqttConnection(
395
423
  config=self.config,
396
424
  auth_client=self._auth_client,
@@ -415,9 +443,6 @@ class NavienMqttClient(EventEmitter):
415
443
 
416
444
  _logger.info("Active reconnection successful")
417
445
  else:
418
- # Restore old connection manager and connection reference
419
- self._connection_manager = old_connection_manager
420
- self._connection = old_connection_manager.connection
421
446
  _logger.warning("Active reconnection failed")
422
447
  else:
423
448
  _logger.warning(
@@ -429,6 +454,8 @@ class NavienMqttClient(EventEmitter):
429
454
  f"Error during active reconnection: {e}", exc_info=True
430
455
  )
431
456
  raise
457
+ finally:
458
+ self._actively_reconnecting = False
432
459
 
433
460
  async def _deep_reconnect(self) -> None:
434
461
  """
@@ -448,18 +475,25 @@ class NavienMqttClient(EventEmitter):
448
475
  _logger.debug("Already connected, skipping deep reconnection")
449
476
  return
450
477
 
478
+ if self._actively_reconnecting:
479
+ _logger.debug("Active reconnection already in progress, skipping")
480
+ return
481
+
451
482
  _logger.warning(
452
483
  "Performing deep reconnection (full rebuild)... "
453
484
  "This may take longer."
454
485
  )
455
486
 
487
+ self._actively_reconnecting = True
456
488
  try:
457
- # Step 1: Clean up existing connection if any
489
+ # Step 1: Clean up existing connection if any.
490
+ # _actively_reconnecting suppresses the on_connection_interrupted
491
+ # callback that closing triggers, preventing a competing backoff
492
+ # loop from being spawned.
458
493
  if self._connection_manager:
459
494
  _logger.debug("Cleaning up old connection...")
460
495
  try:
461
- if self._connection_manager.is_connected:
462
- await self._connection_manager.disconnect()
496
+ await self._connection_manager.close()
463
497
  except (AwsCrtError, RuntimeError) as e:
464
498
  # Expected: connection already dead or in bad state
465
499
  _logger.debug(f"Error during cleanup: {e} (expected)")
@@ -532,6 +566,8 @@ class NavienMqttClient(EventEmitter):
532
566
  ) as e:
533
567
  _logger.error(f"Error during deep reconnection: {e}", exc_info=True)
534
568
  raise
569
+ finally:
570
+ self._actively_reconnecting = False
535
571
 
536
572
  async def connect(self) -> bool:
537
573
  """
@@ -1294,14 +1330,20 @@ class NavienMqttClient(EventEmitter):
1294
1330
  return True
1295
1331
 
1296
1332
  # Not cached, request and wait
1297
- future: asyncio.Future[DeviceFeature] = (
1298
- asyncio.get_running_loop().create_future()
1299
- )
1333
+ loop = asyncio.get_running_loop()
1334
+ future: asyncio.Future[DeviceFeature] = loop.create_future()
1300
1335
 
1301
1336
  def on_feature(feature: DeviceFeature) -> None:
1302
- if not future.done():
1303
- _logger.info(f"Device feature received for {redacted_mac}")
1304
- future.set_result(feature)
1337
+ # Called from AWS SDK thread — schedule onto the event loop
1338
+ # thread-safely. The done() check is inside the scheduled
1339
+ # callback so it runs on the event loop thread, eliminating
1340
+ # the race between the check and set_result.
1341
+ def _set_result() -> None:
1342
+ if not future.done():
1343
+ _logger.info(f"Device feature received for {redacted_mac}")
1344
+ future.set_result(feature)
1345
+
1346
+ loop.call_soon_threadsafe(_set_result)
1305
1347
 
1306
1348
  _logger.info(f"Ensuring device info cached for {redacted_mac}")
1307
1349
  await self.subscribe_device_feature(device, on_feature)
@@ -245,6 +245,44 @@ class MqttConnection:
245
245
  _logger.error(f"Error during disconnect: {e}")
246
246
  raise
247
247
 
248
+ async def close(self) -> None:
249
+ """Unconditionally close the underlying SDK connection.
250
+
251
+ Unlike :meth:`disconnect`, this method closes the connection
252
+ regardless of the ``_connected`` flag. After a connection
253
+ interruption, ``_connected`` is ``False`` but the SDK connection
254
+ object is still alive and its built-in auto-reconnect can still
255
+ fire. Calling ``close()`` ensures the SDK connection is fully
256
+ torn down so its callbacks and auto-reconnect cannot interfere
257
+ with a replacement connection.
258
+
259
+ This method is safe to call multiple times or on already-closed
260
+ connections.
261
+ """
262
+ connection = self._connection
263
+ self._connection = None
264
+ self._connected = False
265
+
266
+ if connection is None:
267
+ return
268
+
269
+ _logger.debug("Closing underlying SDK connection...")
270
+ try:
271
+ disconnect_future = cast(
272
+ asyncio.Future[Any], connection.disconnect()
273
+ )
274
+ await asyncio.shield(asyncio.wrap_future(disconnect_future))
275
+ _logger.debug("SDK connection closed")
276
+ except (AwsCrtError, RuntimeError) as e:
277
+ # Expected when connection is already dead or in bad state
278
+ _logger.debug(f"SDK connection close (benign): {e}")
279
+ except asyncio.CancelledError:
280
+ _logger.debug(
281
+ "Close operation cancelled but SDK disconnect "
282
+ "will complete in background"
283
+ )
284
+ raise
285
+
248
286
  async def subscribe(
249
287
  self,
250
288
  topic: str,
@@ -95,7 +95,11 @@ class MqttMetrics:
95
95
 
96
96
  def to_dict(self) -> dict[str, Any]:
97
97
  """Convert to dictionary for JSON serialization."""
98
- return asdict(self)
98
+ d = asdict(self)
99
+ # Replace inf with None for JSON compatibility
100
+ if d.get("shortest_session_seconds") == float("inf"):
101
+ d["shortest_session_seconds"] = None
102
+ return d
99
103
 
100
104
 
101
105
  class MqttDiagnosticsCollector:
@@ -213,6 +217,7 @@ class MqttDiagnosticsCollector:
213
217
 
214
218
  # Update metrics
215
219
  self._metrics.total_connection_drops += 1
220
+ self._metrics.total_reconnect_attempts += 1
216
221
  if error_name:
217
222
  self._metrics.connection_drops_by_error[error_name] = (
218
223
  self._metrics.connection_drops_by_error.get(error_name, 0) + 1
@@ -173,6 +173,12 @@ 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
176
182
 
177
183
  _logger.debug(
178
184
  "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
  )
@@ -233,11 +233,13 @@ class MqttConnectionConfig:
233
233
  max_queued_commands: int = 100
234
234
 
235
235
  def __post_init__(self) -> None:
236
- """Generate client ID if not provided."""
236
+ """Generate client ID if not provided and validate settings."""
237
237
  if not self.client_id:
238
238
  object.__setattr__(
239
239
  self, "client_id", f"navien-client-{uuid.uuid4().hex[:8]}"
240
240
  )
241
+ if self.deep_reconnect_threshold < 1:
242
+ object.__setattr__(self, "deep_reconnect_threshold", 1)
241
243
 
242
244
 
243
245
  @dataclass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 8.0.0
3
+ Version: 8.1.1
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
@@ -175,6 +175,7 @@ src/nwp500_python.egg-info/top_level.txt
175
175
  tests/conftest.py
176
176
  tests/test_api_helpers.py
177
177
  tests/test_auth.py
178
+ tests/test_bug_fixes.py
178
179
  tests/test_cli_basic.py
179
180
  tests/test_cli_commands.py
180
181
  tests/test_command_decorators.py
@@ -187,6 +188,8 @@ tests/test_model_converters.py
187
188
  tests/test_models.py
188
189
  tests/test_mqtt_client_init.py
189
190
  tests/test_mqtt_hypothesis.py
191
+ tests/test_mqtt_reconnection.py
192
+ tests/test_mqtt_reconnection_storm.py
190
193
  tests/test_multi_device.py
191
194
  tests/test_openei.py
192
195
  tests/test_reservations.py