nwp500-python 7.4.5__tar.gz → 7.4.6__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 (182) hide show
  1. nwp500_python-7.4.6/.bandit +3 -0
  2. nwp500_python-7.4.6/.github/RESOLVING_PR_COMMENTS.md +376 -0
  3. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/.github/copilot-instructions.md +65 -0
  4. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/.github/workflows/ci.yml +19 -0
  5. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/.gitignore +1 -0
  6. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/CHANGELOG.rst +27 -0
  7. {nwp500_python-7.4.5/src/nwp500_python.egg-info → nwp500_python-7.4.6}/PKG-INFO +4 -2
  8. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/README.rst +1 -1
  9. nwp500_python-7.4.6/package-lock.json +6 -0
  10. nwp500_python-7.4.6/package.json +1 -0
  11. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/setup.cfg +1 -0
  12. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/api_client.py +2 -1
  13. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/auth.py +5 -0
  14. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/converters.py +2 -2
  15. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/factory.py +6 -1
  16. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/models.py +0 -1
  17. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/mqtt/client.py +13 -4
  18. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/mqtt/command_queue.py +1 -1
  19. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/mqtt/subscriptions.py +40 -9
  20. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/temperature.py +2 -2
  21. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/unit_system.py +2 -2
  22. {nwp500_python-7.4.5 → nwp500_python-7.4.6/src/nwp500_python.egg-info}/PKG-INFO +4 -2
  23. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500_python.egg-info/SOURCES.txt +5 -0
  24. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500_python.egg-info/requires.txt +2 -0
  25. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_model_converters.py +6 -8
  26. nwp500_python-7.4.6/tests/test_mqtt_hypothesis.py +180 -0
  27. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tox.ini +8 -1
  28. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/.agent/workflows/pre-completion-testing.md +0 -0
  29. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/.coveragerc +0 -0
  30. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/.github/workflows/release.yml +0 -0
  31. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/.pre-commit-config.yaml +0 -0
  32. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/.readthedocs.yml +0 -0
  33. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/AUTHORS.rst +0 -0
  34. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/CONTRIBUTING.rst +0 -0
  35. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/LICENSE.txt +0 -0
  36. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/Makefile +0 -0
  37. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/RELEASE.md +0 -0
  38. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/Makefile +0 -0
  39. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/_static/.gitignore +0 -0
  40. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/api/nwp500.rst +0 -0
  41. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/authors.rst +0 -0
  42. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/changelog.rst +0 -0
  43. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/conf.py +0 -0
  44. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/configuration.rst +0 -0
  45. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/development/contributing.rst +0 -0
  46. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/development/history.rst +0 -0
  47. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/enumerations.rst +0 -0
  48. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/guides/advanced_features_explained.rst +0 -0
  49. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/guides/authentication.rst +0 -0
  50. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/guides/auto_recovery.rst +0 -0
  51. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/guides/command_queue.rst +0 -0
  52. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/guides/energy_monitoring.rst +0 -0
  53. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/guides/event_system.rst +0 -0
  54. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/guides/home_assistant_integration.rst +0 -0
  55. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/guides/mqtt_diagnostics.rst +0 -0
  56. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/guides/reservations.rst +0 -0
  57. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/guides/scheduling_features.rst +0 -0
  58. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/guides/time_of_use.rst +0 -0
  59. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/guides/unit_conversion.rst +0 -0
  60. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/index.rst +0 -0
  61. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/installation.rst +0 -0
  62. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/license.rst +0 -0
  63. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/openapi.yaml +0 -0
  64. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/protocol/data_conversions.rst +0 -0
  65. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/protocol/device_features.rst +0 -0
  66. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/protocol/device_status.rst +0 -0
  67. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/protocol/error_codes.rst +0 -0
  68. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/protocol/mqtt_protocol.rst +0 -0
  69. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/protocol/quick_reference.rst +0 -0
  70. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/protocol/rest_api.rst +0 -0
  71. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/python_api/api_client.rst +0 -0
  72. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/python_api/auth_client.rst +0 -0
  73. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/python_api/cli.rst +0 -0
  74. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/python_api/device_control.rst +0 -0
  75. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/python_api/events.rst +0 -0
  76. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/python_api/exceptions.rst +0 -0
  77. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/python_api/models.rst +0 -0
  78. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/python_api/mqtt_client.rst +0 -0
  79. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/quickstart.rst +0 -0
  80. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/docs/requirements.txt +0 -0
  81. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/.ruff.toml +0 -0
  82. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/README.md +0 -0
  83. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/air_filter_reset.py +0 -0
  84. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/anti_legionella.py +0 -0
  85. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/auto_recovery.py +0 -0
  86. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/combined_callbacks.py +0 -0
  87. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/demand_response.py +0 -0
  88. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/device_capabilities.py +0 -0
  89. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/device_status_debug.py +0 -0
  90. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/energy_analytics.py +0 -0
  91. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/error_code_demo.py +0 -0
  92. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/mqtt_diagnostics.py +0 -0
  93. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/power_control.py +0 -0
  94. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/recirculation_control.py +0 -0
  95. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/reconnection_demo.py +0 -0
  96. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/reservation_schedule.py +0 -0
  97. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/simple_auto_recovery.py +0 -0
  98. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/token_restoration.py +0 -0
  99. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/tou_openei.py +0 -0
  100. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/tou_schedule.py +0 -0
  101. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/advanced/water_reservation.py +0 -0
  102. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/beginner/01_authentication.py +0 -0
  103. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/beginner/02_list_devices.py +0 -0
  104. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/beginner/03_get_status.py +0 -0
  105. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/beginner/04_set_temperature.py +0 -0
  106. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/intermediate/advanced_auth_patterns.py +0 -0
  107. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/intermediate/command_queue.py +0 -0
  108. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/intermediate/device_status_callback.py +0 -0
  109. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/intermediate/error_handling.py +0 -0
  110. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/intermediate/event_driven_control.py +0 -0
  111. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/intermediate/improved_auth.py +0 -0
  112. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/intermediate/legacy_auth_constructor.py +0 -0
  113. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  114. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/intermediate/periodic_requests.py +0 -0
  115. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/intermediate/set_mode.py +0 -0
  116. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/intermediate/vacation_mode.py +0 -0
  117. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/mask.py +0 -0
  118. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/testing/periodic_device_info.py +0 -0
  119. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/testing/simple_periodic_info.py +0 -0
  120. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/testing/test_api_client.py +0 -0
  121. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/testing/test_mqtt_connection.py +0 -0
  122. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/testing/test_mqtt_messaging.py +0 -0
  123. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/examples/testing/test_periodic_minimal.py +0 -0
  124. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/pyproject.toml +0 -0
  125. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/scripts/README.md +0 -0
  126. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/scripts/bump_version.py +0 -0
  127. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/scripts/diagnose_mqtt_connection.py +0 -0
  128. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/scripts/extract_changelog.py +0 -0
  129. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/scripts/format.py +0 -0
  130. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/scripts/lint.py +0 -0
  131. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/scripts/setup-dev.py +0 -0
  132. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/scripts/validate_version.py +0 -0
  133. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/setup.py +0 -0
  134. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/__init__.py +0 -0
  135. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/cli/__init__.py +0 -0
  136. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/cli/__main__.py +0 -0
  137. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/cli/commands.py +0 -0
  138. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/cli/handlers.py +0 -0
  139. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/cli/monitoring.py +0 -0
  140. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/cli/output_formatters.py +0 -0
  141. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/cli/rich_output.py +0 -0
  142. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/cli/token_storage.py +0 -0
  143. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/command_decorators.py +0 -0
  144. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/config.py +0 -0
  145. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/device_capabilities.py +0 -0
  146. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/device_info_cache.py +0 -0
  147. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/encoding.py +4 -4
  148. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/enums.py +0 -0
  149. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/events.py +0 -0
  150. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/exceptions.py +0 -0
  151. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/field_factory.py +0 -0
  152. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/mqtt/__init__.py +0 -0
  153. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/mqtt/connection.py +0 -0
  154. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/mqtt/control.py +0 -0
  155. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/mqtt/diagnostics.py +0 -0
  156. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/mqtt/periodic.py +0 -0
  157. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/mqtt/reconnection.py +0 -0
  158. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/mqtt/utils.py +0 -0
  159. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/mqtt_events.py +0 -0
  160. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/py.typed +0 -0
  161. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/topic_builder.py +0 -0
  162. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500/utils.py +0 -0
  163. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  164. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  165. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  166. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/src/nwp500_python.egg-info/top_level.txt +0 -0
  167. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/conftest.py +0 -0
  168. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_api_helpers.py +0 -0
  169. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_auth.py +0 -0
  170. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_cli_basic.py +0 -0
  171. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_cli_commands.py +0 -0
  172. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_command_decorators.py +0 -0
  173. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_command_queue.py +0 -0
  174. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_device_capabilities.py +0 -0
  175. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_device_info_cache.py +0 -0
  176. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_events.py +0 -0
  177. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_exceptions.py +0 -0
  178. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_models.py +0 -0
  179. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_mqtt_client_init.py +0 -0
  180. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_temperature_converters.py +0 -0
  181. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_unit_switching.py +0 -0
  182. {nwp500_python-7.4.5 → nwp500_python-7.4.6}/tests/test_utils.py +0 -0
@@ -0,0 +1,3 @@
1
+ assert_used:
2
+ skips:
3
+ - src/nwp500/cli/rich_output.py
@@ -0,0 +1,376 @@
1
+ # How to Resolve GitHub Review Comments
2
+
3
+ This guide documents how to mark review comment conversations as resolved after addressing reviewer feedback in pull requests.
4
+
5
+ ## Quick Reference
6
+
7
+ ```bash
8
+ # 1. Get all review threads for PR #74
9
+ gh api graphql -f query='
10
+ query {
11
+ repository(owner: "eman", name: "nwp500-python") {
12
+ pullRequest(number: 74) {
13
+ reviewThreads(first: 10) {
14
+ nodes {
15
+ id
16
+ path
17
+ line
18
+ isResolved
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ ' | jq '.data.repository.pullRequest.reviewThreads.nodes'
25
+
26
+ # 2. Resolve a specific thread
27
+ gh api graphql -f query='
28
+ mutation {
29
+ resolveReviewThread(input: {threadId: "PRRT_kwDOP_hNvM5ukIVT"}) {
30
+ thread { isResolved }
31
+ }
32
+ }
33
+ '
34
+ ```
35
+
36
+ ## Why Resolve Comments?
37
+
38
+ - **Clarity**: Shows reviewers their feedback has been acted upon
39
+ - **Workflow**: Prevents reviewers from re-reading already-addressed comments
40
+ - **Signal Readiness**: Indicates PR is ready for another review pass
41
+ - **Clean PR Interface**: Makes GitHub's PR conversation cleaner and easier to follow
42
+
43
+ ## Workflow: Address and Resolve
44
+
45
+ ### 1. Address the Comment
46
+
47
+ Make the code changes requested by the reviewer:
48
+ - Fix bugs
49
+ - Update documentation
50
+ - Refactor code
51
+ - Add tests
52
+
53
+ **Example:**
54
+ ```bash
55
+ # Edit file based on comment
56
+ nano src/nwp500/converters.py
57
+
58
+ # Commit the change
59
+ git add src/nwp500/converters.py
60
+ git commit -m "Fix div_10 converter to handle string inputs"
61
+
62
+ # Push to remote
63
+ git push
64
+ ```
65
+
66
+ ### 2. Verify Tests Pass
67
+
68
+ Run all validation before marking comments as resolved:
69
+
70
+ ```bash
71
+ # Run tests
72
+ pytest --ignore=tests/test_mqtt_hypothesis.py
73
+
74
+ # Run linting
75
+ make ci-lint
76
+
77
+ # Run type checking
78
+ python3 -m mypy src/nwp500 --config-file pyproject.toml
79
+ ```
80
+
81
+ All must pass before proceeding.
82
+
83
+ ### 3. Get Review Thread IDs
84
+
85
+ Query GraphQL to find threads that need resolving:
86
+
87
+ ```bash
88
+ gh api graphql -f query='
89
+ query {
90
+ repository(owner: "eman", name: "nwp500-python") {
91
+ pullRequest(number: 74) {
92
+ reviewThreads(first: 10) {
93
+ nodes {
94
+ id
95
+ path
96
+ line
97
+ isResolved
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ ' | jq '.data.repository.pullRequest.reviewThreads.nodes'
104
+ ```
105
+
106
+ **Output example:**
107
+ ```json
108
+ [
109
+ {
110
+ "id": "PRRT_kwDOP_hNvM5ukIVT",
111
+ "path": "src/nwp500/converters.py",
112
+ "line": 125,
113
+ "isResolved": false
114
+ },
115
+ {
116
+ "id": "PRRT_kwDOP_hNvM5ukIVo",
117
+ "path": "tests/test_model_converters.py",
118
+ "line": 212,
119
+ "isResolved": false
120
+ }
121
+ ]
122
+ ```
123
+
124
+ ### 4. Identify Which Threads to Resolve
125
+
126
+ Cross-reference the output with:
127
+ 1. Which file you modified
128
+ 2. Which line the comment was on (approximately)
129
+ 3. Whether `isResolved` is `false`
130
+
131
+ ### 5. Resolve the Threads
132
+
133
+ For each thread you addressed:
134
+
135
+ ```bash
136
+ gh api graphql -f query='
137
+ mutation {
138
+ resolveReviewThread(input: {threadId: "PRRT_kwDOP_hNvM5ukIVT"}) {
139
+ thread {
140
+ isResolved
141
+ }
142
+ }
143
+ }
144
+ '
145
+ ```
146
+
147
+ Success response:
148
+ ```json
149
+ {
150
+ "data": {
151
+ "resolveReviewThread": {
152
+ "thread": {
153
+ "isResolved": true
154
+ }
155
+ }
156
+ }
157
+ }
158
+ ```
159
+
160
+ ### 6. Verify All Are Resolved
161
+
162
+ Re-run the query from Step 3 to confirm all addressed threads now show `"isResolved": true`:
163
+
164
+ ```bash
165
+ gh api graphql -f query='
166
+ query {
167
+ repository(owner: "eman", name: "nwp500-python") {
168
+ pullRequest(number: 74) {
169
+ reviewThreads(first: 10) {
170
+ nodes {
171
+ path
172
+ isResolved
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+ ' | jq '.data.repository.pullRequest.reviewThreads.nodes'
179
+ ```
180
+
181
+ ## Batch Resolving Multiple Threads
182
+
183
+ If you have many threads to resolve, use a loop:
184
+
185
+ ```bash
186
+ #!/bin/bash
187
+
188
+ # Define thread IDs
189
+ THREAD_IDS=(
190
+ "PRRT_kwDOP_hNvM5ukIVT"
191
+ "PRRT_kwDOP_hNvM5ukIVo"
192
+ "PRRT_kwDOP_hNvM5ukIVx"
193
+ )
194
+
195
+ # Resolve each one
196
+ for thread_id in "${THREAD_IDS[@]}"; do
197
+ gh api graphql -f query="
198
+ mutation {
199
+ resolveReviewThread(input: {threadId: \"$thread_id\"}) {
200
+ thread { isResolved }
201
+ }
202
+ }
203
+ " && echo "✓ $thread_id resolved"
204
+ done
205
+
206
+ echo "All threads resolved!"
207
+ ```
208
+
209
+ Or as a one-liner:
210
+
211
+ ```bash
212
+ for id in PRRT_kwDOP_hNvM5ukIVT PRRT_kwDOP_hNvM5ukIVo PRRT_kwDOP_hNvM5ukIVx; do
213
+ gh api graphql -f query="mutation { resolveReviewThread(input: {threadId: \"$id\"}) { thread { isResolved } } }" && echo "✓ $id resolved"
214
+ done
215
+ ```
216
+
217
+ ## Shell Function (Optional)
218
+
219
+ Add this to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.):
220
+
221
+ ```bash
222
+ # Resolve a single GitHub PR review thread
223
+ resolve-pr-thread() {
224
+ local thread_id="$1"
225
+ if [[ -z "$thread_id" ]]; then
226
+ echo "Usage: resolve-pr-thread THREAD_ID"
227
+ echo "Example: resolve-pr-thread PRRT_kwDOP_hNvM5ukIVT"
228
+ return 1
229
+ fi
230
+
231
+ gh api graphql -f query="
232
+ mutation {
233
+ resolveReviewThread(input: {threadId: \"$thread_id\"}) {
234
+ thread { isResolved }
235
+ }
236
+ }
237
+ " | jq '.data.resolveReviewThread.thread.isResolved'
238
+ }
239
+
240
+ # Get all unresolved threads for a PR
241
+ pr-threads() {
242
+ local pr_num="${1:?PR number required}"
243
+ gh api graphql -f query="
244
+ query {
245
+ repository(owner: \"eman\", name: \"nwp500-python\") {
246
+ pullRequest(number: $pr_num) {
247
+ reviewThreads(first: 10) {
248
+ nodes {
249
+ id
250
+ path
251
+ line
252
+ isResolved
253
+ }
254
+ }
255
+ }
256
+ }
257
+ }
258
+ " | jq '.data.repository.pullRequest.reviewThreads.nodes'
259
+ }
260
+ ```
261
+
262
+ Usage:
263
+ ```bash
264
+ pr-threads 74 # List all threads for PR #74
265
+ resolve-pr-thread PRRT_kwDOP_hNvM5ukIVT # Resolve one thread
266
+ ```
267
+
268
+ ## Special Cases
269
+
270
+ ### Unresolving a Thread
271
+
272
+ If a reviewer asks for changes after you marked it resolved, unresolve it:
273
+
274
+ ```bash
275
+ gh api graphql -f query='
276
+ mutation {
277
+ unresolveReviewThread(input: {threadId: "PRRT_kwDOP_hNvM5ukIVT"}) {
278
+ thread {
279
+ isResolved
280
+ }
281
+ }
282
+ }
283
+ '
284
+ ```
285
+
286
+ ### Force-Pushed Commits
287
+
288
+ When you amend and force-push commits:
289
+ 1. Old review threads remain resolvable
290
+ 2. Thread IDs don't change
291
+ 3. Line numbers may be different in new commits
292
+ 4. Comments still point to old code but threads can be resolved
293
+ 5. This is normal GitHub behavior - resolve all threads once your changes are complete
294
+
295
+ ### Multiple Changes to Same File
296
+
297
+ If a reviewer left multiple comments on the same file and you addressed them all:
298
+ 1. Make all changes to the file
299
+ 2. Commit and push
300
+ 3. Get all thread IDs for that file
301
+ 4. Resolve each one individually
302
+
303
+ ## Troubleshooting
304
+
305
+ ### "Not Found" Error
306
+
307
+ **Problem:**
308
+ ```
309
+ {
310
+ "message": "Not Found",
311
+ "documentation_url": "https://docs.github.com/rest",
312
+ "status": 404
313
+ }
314
+ ```
315
+
316
+ **Solutions:**
317
+ - Verify PR number is correct
318
+ - Check you have access to the repository
319
+ - Verify `gh auth status` shows you're authenticated
320
+ - Try running `gh auth login` again
321
+
322
+ ### No Threads Returned
323
+
324
+ **Problem:**
325
+ ```
326
+ {
327
+ "data": {
328
+ "repository": {
329
+ "pullRequest": {
330
+ "reviewThreads": {
331
+ "nodes": []
332
+ }
333
+ }
334
+ }
335
+ }
336
+ }
337
+ ```
338
+
339
+ **Solutions:**
340
+ - Verify PR number is correct
341
+ - The PR may have only general comments (not review comments)
342
+ - Try increasing `first: 10` to `first: 100` if there are many threads
343
+ - Verify reviewers left inline code comments, not just PR comments
344
+
345
+ ### Thread Won't Resolve
346
+
347
+ **Problem:**
348
+ Mutation succeeds but `isResolved` still returns `false` on next check
349
+
350
+ **Solutions:**
351
+ - Wait a moment and query again (GitHub API may have delay)
352
+ - Verify you're using the exact same thread ID
353
+ - Check that you have write permissions on the repository
354
+ - Try running `gh auth refresh` to refresh your token
355
+
356
+ ### Can't Find Thread ID for Comment I Fixed
357
+
358
+ **Problem:**
359
+ You fixed the code but can't find the matching thread ID
360
+
361
+ **Possible Causes:**
362
+ 1. The comment was a general PR comment, not an inline code comment (can't be resolved)
363
+ 2. The thread was already resolved by someone else
364
+ 3. You're looking at the wrong PR number
365
+ 4. The comment was deleted by the reviewer
366
+
367
+ **Solution:**
368
+ Go to the PR on GitHub.com and manually verify the comment still exists and is an inline review comment (not a general comment).
369
+
370
+ ## References
371
+
372
+ - [GitHub GraphQL API: resolveReviewThread](https://docs.github.com/en/graphql/reference/mutations#resolvereviewthread)
373
+ - [GitHub GraphQL API: unresolveReviewThread](https://docs.github.com/en/graphql/reference/mutations#unresolvereviewthread)
374
+ - [GitHub GraphQL API: Review Threads](https://docs.github.com/en/graphql/reference/objects#reviewthread)
375
+ - [GitHub CLI: gh api](https://cli.github.com/manual/gh_api)
376
+ - [GitHub: Review conversations](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/commenting-on-a-pull-request#about-pull-request-reviews)
@@ -54,6 +54,71 @@ When working on pull requests, use the GitHub CLI to access review comments:
54
54
 
55
55
  This ensures you can address all feedback from code reviewers systematically.
56
56
 
57
+ #### Marking Conversations as Resolved
58
+
59
+ After addressing a review comment, mark the conversation as resolved to signal reviewers that the feedback has been acted upon.
60
+
61
+ **Step 1: Get review thread IDs**
62
+ ```bash
63
+ gh api graphql -f query='
64
+ query {
65
+ repository(owner: "eman", name: "nwp500-python") {
66
+ pullRequest(number: 74) {
67
+ reviewThreads(first: 10) {
68
+ nodes {
69
+ id
70
+ path
71
+ line
72
+ isResolved
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ '
79
+ ```
80
+
81
+ Replace `74` with the actual PR number. This returns thread IDs like `PRRT_kwDOP_hNvM5ukIVT`.
82
+
83
+ **Step 2: Resolve a thread**
84
+ ```bash
85
+ gh api graphql -f query='
86
+ mutation {
87
+ resolveReviewThread(input: {threadId: "PRRT_kwDOP_hNvM5ukIVT"}) {
88
+ thread {
89
+ isResolved
90
+ }
91
+ }
92
+ }
93
+ '
94
+ ```
95
+
96
+ Replace the thread ID with the one from Step 1. Success response shows `"isResolved": true`.
97
+
98
+ **Step 3: Resolve multiple threads efficiently**
99
+ ```bash
100
+ for id in PRRT_kwDOP_hNvM5ukIVT PRRT_kwDOP_hNvM5ukIVo PRRT_kwDOP_hNvM5ukIVx; do
101
+ gh api graphql -f query="
102
+ mutation {
103
+ resolveReviewThread(input: {threadId: \"$id\"}) {
104
+ thread { isResolved }
105
+ }
106
+ }
107
+ " && echo "✓ $id resolved"
108
+ done
109
+ ```
110
+
111
+ **Step 4: Verify all are resolved**
112
+ Re-run Step 1 query and confirm all addressed threads show `"isResolved": true`.
113
+
114
+ **Important Notes:**
115
+ - Only inline code review comments can be resolved (not general PR comments)
116
+ - Conversations remain resolvable even after force-pushing commits
117
+ - When commits are amended and force-pushed, old thread IDs remain valid
118
+ - If you need to reopen a resolved thread, use `unresolveReviewThread` mutation instead
119
+
120
+ See detailed instructions at `.github/copilot-instructions.md` for more examples and troubleshooting.
121
+
57
122
  ### Before Committing Changes
58
123
  Always run these checks before finalizing changes to ensure your code will pass CI:
59
124
  1. **Linting**: `make ci-lint` - Ensures code style matches CI requirements
@@ -28,6 +28,25 @@ jobs:
28
28
  - name: Run tox lint
29
29
  run: tox -e lint
30
30
 
31
+ security:
32
+ name: Security Check
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+
37
+ - name: Set up Python
38
+ uses: actions/setup-python@v5
39
+ with:
40
+ python-version: '3.13'
41
+
42
+ - name: Install tox
43
+ run: |
44
+ python -m pip install --upgrade pip
45
+ python -m pip install tox
46
+
47
+ - name: Run bandit
48
+ run: tox -e bandit
49
+
31
50
  test:
32
51
  name: Test on Python ${{ matrix.python-version }}
33
52
  runs-on: ubuntu-latest
@@ -38,6 +38,7 @@ htmlcov/*
38
38
  junit*.xml
39
39
  coverage.xml
40
40
  .pytest_cache/
41
+ .hypothesis/
41
42
 
42
43
  # Diagnostics output
43
44
  diagnostics_output/
@@ -2,6 +2,33 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version 7.4.6 (2026-02-13)
6
+ ==========================
7
+
8
+ Fixed
9
+ -----
10
+ - **Converter Consistency**: ``div_10()`` and ``mul_10()`` now correctly apply division/multiplication to all input types after ``float()`` conversion, not just ``int``/``float`` types
11
+ - **Reservation Decoding**: Fixed ``decode_reservation_hex()`` to validate chunk length before checking for empty entries, preventing potential out-of-bounds access
12
+ - **Factory Cleanup**: ``create_navien_clients()`` now properly cleans up auth session if authentication fails during context manager entry
13
+ - **MQTT Reconnection**: MQTT client now resubscribes to all topics after successful reconnection
14
+ - **Subscription Leak**: Fixed resource leak where ``wait_for_device_feature()`` did not unsubscribe its callback after completion
15
+ - **Duplicate Handlers**: Subscription manager now prevents duplicate callback registration for the same topic
16
+ - **Command Queue**: ``MqttCommandQueue`` now raises on ``QueueFull`` instead of silently swallowing the error
17
+ - **Flow Rate Metadata**: Removed hardcoded ``"GPM"`` unit from ``recirc_dhw_flow_rate`` field; unit is now dynamic based on unit system
18
+ - **Temperature Rounding**: ``RawCelsius`` Fahrenheit conversion now uses a catch-all default for standard rounding instead of matching only ``STANDARD`` enum value
19
+ - **Unit System Default**: ``is_metric_preferred()`` now returns ``False`` (Fahrenheit) instead of ``None`` when no unit system override or context is set
20
+
21
+ Security
22
+ --------
23
+ - **Sensitive Data Logging**: Redacted MQTT topics in subscription manager logging to prevent leaking device IDs (resolves CodeQL alerts)
24
+
25
+ Added
26
+ -----
27
+ - **Auth Session Property**: Added ``NavienAuthClient.session`` property to access the active ``aiohttp`` session without using ``getattr``
28
+ - **Unsubscribe Feature**: Added ``unsubscribe_device_feature()`` method to MQTT client and subscription manager for targeted callback removal
29
+ - **Hypothesis Fuzzing**: Added property-based fuzzing tests for MQTT payload handling
30
+ - **Bandit Security Scanning**: Added bandit configuration for security analysis in CI
31
+
5
32
  Version 7.4.5 (2026-02-04)
6
33
  ==========================
7
34
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 7.4.5
3
+ Version: 7.4.6
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
@@ -29,6 +29,7 @@ Requires-Dist: setuptools; extra == "testing"
29
29
  Requires-Dist: pytest; extra == "testing"
30
30
  Requires-Dist: pytest-cov; extra == "testing"
31
31
  Requires-Dist: pytest-asyncio; extra == "testing"
32
+ Requires-Dist: hypothesis; extra == "testing"
32
33
  Provides-Extra: dev
33
34
  Requires-Dist: ruff>=0.1.0; extra == "dev"
34
35
  Requires-Dist: pyright>=1.1.0; extra == "dev"
@@ -36,6 +37,7 @@ Requires-Dist: setuptools; extra == "dev"
36
37
  Requires-Dist: pytest; extra == "dev"
37
38
  Requires-Dist: pytest-cov; extra == "dev"
38
39
  Requires-Dist: pytest-asyncio; extra == "dev"
40
+ Requires-Dist: hypothesis; extra == "dev"
39
41
  Dynamic: license-file
40
42
 
41
43
  =============
@@ -110,7 +112,7 @@ Basic Usage
110
112
  if device:
111
113
  # Access status information
112
114
  status = device.status
113
- print(f"Water Temperature: {status.dhw_temperature}°F")
115
+ print(f"Water Temperature: {status.dhw_temperature}")
114
116
  print(f"Tank Charge: {status.dhw_charge_per}%")
115
117
  print(f"Power Consumption: {status.current_inst_power}W")
116
118
 
@@ -70,7 +70,7 @@ Basic Usage
70
70
  if device:
71
71
  # Access status information
72
72
  status = device.status
73
- print(f"Water Temperature: {status.dhw_temperature}°F")
73
+ print(f"Water Temperature: {status.dhw_temperature}")
74
74
  print(f"Tank Charge: {status.dhw_charge_per}%")
75
75
  print(f"Power Consumption: {status.current_inst_power}W")
76
76
 
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "nwp500-python",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {}
6
+ }
@@ -0,0 +1 @@
1
+ {}
@@ -46,6 +46,7 @@ testing =
46
46
  pytest
47
47
  pytest-cov
48
48
  pytest-asyncio
49
+ hypothesis
49
50
  dev =
50
51
  ruff>=0.1.0
51
52
  pyright>=1.1.0
@@ -78,7 +78,8 @@ class NavienAPIClient:
78
78
 
79
79
  self.base_url = base_url.rstrip("/")
80
80
  self._auth_client = auth_client
81
- self._session = session or getattr(auth_client, "_session", None)
81
+ self._session = session or auth_client.session
82
+
82
83
  if self._session is None:
83
84
  raise ValueError(
84
85
  "auth_client must have an active session or a session "
@@ -633,6 +633,11 @@ class NavienAuthClient:
633
633
 
634
634
  return tokens
635
635
 
636
+ @property
637
+ def session(self) -> aiohttp.ClientSession | None:
638
+ """Get the active aiohttp session."""
639
+ return self._session
640
+
636
641
  @property
637
642
  def is_authenticated(self) -> bool:
638
643
  """Check if client is currently authenticated."""
@@ -122,7 +122,7 @@ def div_10(value: Any) -> float:
122
122
  """
123
123
  if isinstance(value, (int, float)):
124
124
  return float(value) / 10.0
125
- return float(value)
125
+ return float(value) / 10.0
126
126
 
127
127
 
128
128
  def mul_10(value: Any) -> float:
@@ -145,7 +145,7 @@ def mul_10(value: Any) -> float:
145
145
  """
146
146
  if isinstance(value, (int, float)):
147
147
  return float(value) * 10.0
148
- return float(value)
148
+ return float(value) * 10.0
149
149
 
150
150
 
151
151
  def enum_validator(enum_class: type[Any]) -> Callable[[Any], Any]:
@@ -73,7 +73,12 @@ async def create_navien_clients(
73
73
  auth_client = NavienAuthClient(email, password)
74
74
 
75
75
  # Authenticate and enter context manager
76
- await auth_client.__aenter__()
76
+ try:
77
+ await auth_client.__aenter__()
78
+ except BaseException:
79
+ # Ensure session is cleaned up if authentication fails
80
+ await auth_client.__aexit__(None, None, None)
81
+ raise
77
82
 
78
83
  # Create API and MQTT clients that share the session
79
84
  api_client = NavienAPIClient(auth_client=auth_client)
@@ -775,7 +775,6 @@ class DeviceStatus(NavienBaseModel):
775
775
  recirc_dhw_flow_rate: FlowRate = Field(
776
776
  description="Recirculation DHW flow rate (dynamic units: LPM/GPM)",
777
777
  json_schema_extra={
778
- "unit_of_measurement": "GPM",
779
778
  "device_class": "flow_rate",
780
779
  },
781
780
  )
@@ -375,6 +375,7 @@ class NavienMqttClient(EventEmitter):
375
375
  self._subscription_manager.update_connection(
376
376
  self._connection
377
377
  )
378
+ await self._subscription_manager.resubscribe_all()
378
379
 
379
380
  _logger.info("Active reconnection successful")
380
381
  else:
@@ -894,6 +895,16 @@ class NavienMqttClient(EventEmitter):
894
895
  "subscribe_device_feature", device, callback
895
896
  )
896
897
 
898
+ async def unsubscribe_device_feature(
899
+ self, device: Device, callback: Callable[[DeviceFeature], None]
900
+ ) -> None:
901
+ """Unsubscribe a specific device feature callback."""
902
+ if not self._connected or not self._subscription_manager:
903
+ return
904
+ await self._subscription_manager.unsubscribe_device_feature(
905
+ device, callback
906
+ )
907
+
897
908
  async def subscribe_energy_usage(
898
909
  self,
899
910
  device: Device,
@@ -961,10 +972,8 @@ class NavienMqttClient(EventEmitter):
961
972
  )
962
973
  return False
963
974
  finally:
964
- # Note: We don't unsubscribe token here because it might
965
- # interfere with other subscribers if we're not careful.
966
- # But the subscription manager handles multiple callbacks.
967
- pass
975
+ # Unsubscribe using the specific callback to avoid leaking resources
976
+ await self.unsubscribe_device_feature(device, on_feature)
968
977
 
969
978
  @property
970
979
  def control(self) -> MqttDeviceController: