nwp500-python 7.4.5__tar.gz → 7.4.7__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 (185) hide show
  1. nwp500_python-7.4.7/.github/RESOLVING_PR_COMMENTS.md +376 -0
  2. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/.github/copilot-instructions.md +65 -0
  3. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/.github/workflows/ci.yml +19 -0
  4. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/.gitignore +7 -0
  5. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/CHANGELOG.rst +53 -0
  6. {nwp500_python-7.4.5/src/nwp500_python.egg-info → nwp500_python-7.4.7}/PKG-INFO +4 -2
  7. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/README.rst +1 -1
  8. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/api/nwp500.rst +8 -0
  9. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/guides/advanced_features_explained.rst +1 -1
  10. nwp500_python-7.4.7/docs/guides/scheduling.rst +681 -0
  11. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/guides/time_of_use.rst +163 -155
  12. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/index.rst +1 -2
  13. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/openapi.yaml +231 -0
  14. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/python_api/cli.rst +85 -31
  15. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/python_api/device_control.rst +1 -2
  16. nwp500_python-7.4.7/examples/advanced/tou_openei.py +121 -0
  17. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/pyproject.toml +5 -2
  18. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/setup.cfg +1 -0
  19. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/__init__.py +11 -0
  20. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/api_client.py +106 -2
  21. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/auth.py +5 -0
  22. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/cli/__init__.py +6 -0
  23. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/cli/__main__.py +294 -5
  24. nwp500_python-7.4.7/src/nwp500/cli/handlers.py +1131 -0
  25. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/cli/rich_output.py +364 -0
  26. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/converters.py +2 -2
  27. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/encoding.py +92 -37
  28. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/factory.py +6 -1
  29. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/models.py +126 -1
  30. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/mqtt/client.py +13 -4
  31. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/mqtt/command_queue.py +1 -1
  32. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/mqtt/control.py +2 -2
  33. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/mqtt/subscriptions.py +40 -9
  34. nwp500_python-7.4.7/src/nwp500/openei.py +213 -0
  35. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/temperature.py +2 -2
  36. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/unit_system.py +2 -2
  37. {nwp500_python-7.4.5 → nwp500_python-7.4.7/src/nwp500_python.egg-info}/PKG-INFO +4 -2
  38. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500_python.egg-info/SOURCES.txt +6 -3
  39. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500_python.egg-info/requires.txt +2 -0
  40. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_api_helpers.py +44 -7
  41. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_model_converters.py +6 -8
  42. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_models.py +133 -1
  43. nwp500_python-7.4.7/tests/test_mqtt_hypothesis.py +180 -0
  44. nwp500_python-7.4.7/tests/test_openei.py +219 -0
  45. nwp500_python-7.4.7/tests/test_tou_api.py +262 -0
  46. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tox.ini +1 -0
  47. nwp500_python-7.4.5/.agent/workflows/pre-completion-testing.md +0 -73
  48. nwp500_python-7.4.5/docs/guides/reservations.rst +0 -721
  49. nwp500_python-7.4.5/docs/guides/scheduling_features.rst +0 -406
  50. nwp500_python-7.4.5/examples/advanced/tou_openei.py +0 -328
  51. nwp500_python-7.4.5/src/nwp500/cli/handlers.py +0 -564
  52. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/.coveragerc +0 -0
  53. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/.github/workflows/release.yml +0 -0
  54. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/.pre-commit-config.yaml +0 -0
  55. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/.readthedocs.yml +0 -0
  56. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/AUTHORS.rst +0 -0
  57. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/CONTRIBUTING.rst +0 -0
  58. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/LICENSE.txt +0 -0
  59. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/Makefile +0 -0
  60. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/RELEASE.md +0 -0
  61. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/Makefile +0 -0
  62. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/_static/.gitignore +0 -0
  63. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/authors.rst +0 -0
  64. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/changelog.rst +0 -0
  65. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/conf.py +0 -0
  66. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/configuration.rst +0 -0
  67. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/development/contributing.rst +0 -0
  68. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/development/history.rst +0 -0
  69. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/enumerations.rst +0 -0
  70. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/guides/authentication.rst +0 -0
  71. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/guides/auto_recovery.rst +0 -0
  72. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/guides/command_queue.rst +0 -0
  73. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/guides/energy_monitoring.rst +0 -0
  74. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/guides/event_system.rst +0 -0
  75. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/guides/home_assistant_integration.rst +0 -0
  76. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/guides/mqtt_diagnostics.rst +0 -0
  77. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/guides/unit_conversion.rst +0 -0
  78. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/installation.rst +0 -0
  79. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/license.rst +0 -0
  80. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/protocol/data_conversions.rst +0 -0
  81. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/protocol/device_features.rst +0 -0
  82. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/protocol/device_status.rst +0 -0
  83. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/protocol/error_codes.rst +0 -0
  84. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/protocol/mqtt_protocol.rst +0 -0
  85. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/protocol/quick_reference.rst +0 -0
  86. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/protocol/rest_api.rst +0 -0
  87. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/python_api/api_client.rst +0 -0
  88. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/python_api/auth_client.rst +0 -0
  89. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/python_api/events.rst +0 -0
  90. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/python_api/exceptions.rst +0 -0
  91. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/python_api/models.rst +0 -0
  92. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/python_api/mqtt_client.rst +0 -0
  93. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/quickstart.rst +0 -0
  94. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/docs/requirements.txt +0 -0
  95. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/.ruff.toml +0 -0
  96. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/README.md +0 -0
  97. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/air_filter_reset.py +0 -0
  98. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/anti_legionella.py +0 -0
  99. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/auto_recovery.py +0 -0
  100. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/combined_callbacks.py +0 -0
  101. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/demand_response.py +0 -0
  102. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/device_capabilities.py +0 -0
  103. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/device_status_debug.py +0 -0
  104. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/energy_analytics.py +0 -0
  105. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/error_code_demo.py +0 -0
  106. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/mqtt_diagnostics.py +0 -0
  107. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/power_control.py +0 -0
  108. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/recirculation_control.py +0 -0
  109. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/reconnection_demo.py +0 -0
  110. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/reservation_schedule.py +0 -0
  111. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/simple_auto_recovery.py +0 -0
  112. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/token_restoration.py +0 -0
  113. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/tou_schedule.py +0 -0
  114. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/advanced/water_reservation.py +0 -0
  115. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/beginner/01_authentication.py +0 -0
  116. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/beginner/02_list_devices.py +0 -0
  117. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/beginner/03_get_status.py +0 -0
  118. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/beginner/04_set_temperature.py +0 -0
  119. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/intermediate/advanced_auth_patterns.py +0 -0
  120. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/intermediate/command_queue.py +0 -0
  121. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/intermediate/device_status_callback.py +0 -0
  122. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/intermediate/error_handling.py +0 -0
  123. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/intermediate/event_driven_control.py +0 -0
  124. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/intermediate/improved_auth.py +0 -0
  125. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/intermediate/legacy_auth_constructor.py +0 -0
  126. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  127. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/intermediate/periodic_requests.py +0 -0
  128. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/intermediate/set_mode.py +0 -0
  129. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/intermediate/vacation_mode.py +0 -0
  130. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/mask.py +0 -0
  131. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/testing/periodic_device_info.py +0 -0
  132. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/testing/simple_periodic_info.py +0 -0
  133. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/testing/test_api_client.py +0 -0
  134. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/testing/test_mqtt_connection.py +0 -0
  135. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/testing/test_mqtt_messaging.py +0 -0
  136. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/examples/testing/test_periodic_minimal.py +0 -0
  137. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/scripts/README.md +0 -0
  138. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/scripts/bump_version.py +0 -0
  139. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/scripts/diagnose_mqtt_connection.py +0 -0
  140. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/scripts/extract_changelog.py +0 -0
  141. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/scripts/format.py +0 -0
  142. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/scripts/lint.py +0 -0
  143. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/scripts/setup-dev.py +0 -0
  144. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/scripts/validate_version.py +0 -0
  145. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/setup.py +0 -0
  146. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/cli/commands.py +0 -0
  147. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/cli/monitoring.py +0 -0
  148. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/cli/output_formatters.py +0 -0
  149. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/cli/token_storage.py +0 -0
  150. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/command_decorators.py +0 -0
  151. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/config.py +0 -0
  152. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/device_capabilities.py +0 -0
  153. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/device_info_cache.py +0 -0
  154. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/enums.py +0 -0
  155. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/events.py +0 -0
  156. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/exceptions.py +0 -0
  157. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/field_factory.py +0 -0
  158. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/mqtt/__init__.py +0 -0
  159. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/mqtt/connection.py +0 -0
  160. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/mqtt/diagnostics.py +0 -0
  161. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/mqtt/periodic.py +0 -0
  162. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/mqtt/reconnection.py +0 -0
  163. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/mqtt/utils.py +0 -0
  164. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/mqtt_events.py +0 -0
  165. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/py.typed +0 -0
  166. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/topic_builder.py +0 -0
  167. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500/utils.py +0 -0
  168. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  169. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  170. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  171. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/src/nwp500_python.egg-info/top_level.txt +0 -0
  172. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/conftest.py +0 -0
  173. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_auth.py +0 -0
  174. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_cli_basic.py +0 -0
  175. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_cli_commands.py +0 -0
  176. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_command_decorators.py +0 -0
  177. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_command_queue.py +0 -0
  178. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_device_capabilities.py +0 -0
  179. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_device_info_cache.py +0 -0
  180. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_events.py +0 -0
  181. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_exceptions.py +0 -0
  182. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_mqtt_client_init.py +0 -0
  183. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_temperature_converters.py +0 -0
  184. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_unit_switching.py +0 -0
  185. {nwp500_python-7.4.5 → nwp500_python-7.4.7}/tests/test_utils.py +0 -0
@@ -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 ruff
43
+ run: |
44
+ python -m pip install --upgrade pip
45
+ python -m pip install ruff
46
+
47
+ - name: Run security checks (ruff bandit)
48
+ run: ruff check --select S src/
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/
@@ -61,3 +62,9 @@ venv*/
61
62
  .secrets
62
63
  reference/*
63
64
  resources/*
65
+
66
+ # Tool caches
67
+ .mypy_cache/
68
+ .ruff_cache/
69
+ .bandit
70
+ .agent/
@@ -2,6 +2,59 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version 7.4.7 (2026-02-17)
6
+ ==========================
7
+
8
+ Added
9
+ -----
10
+ - **OpenEI Client Module**: New ``OpenEIClient`` async client (``nwp500.openei``) for browsing utility rate plans from the OpenEI API by zip code. Supports listing utilities, filtering rate plans, and fetching plan details. API key read from ``OPENEI_API_KEY`` environment variable.
11
+ - **Convert TOU API**: ``NavienAPIClient.convert_tou()`` sends raw OpenEI rate data to the Navien backend for server-side conversion into device-ready TOU schedules with season/week bitfields and scaled pricing.
12
+ - **Update TOU API**: ``NavienAPIClient.update_tou()`` applies a converted TOU rate plan to a device, matching the mobile app's ``PUT /device/tou`` endpoint.
13
+ - **ConvertedTOUPlan Model**: New Pydantic model for parsed ``convert_tou()`` results (utility, name, schedule).
14
+ - **CLI ``tou rates``**: Browse utilities and rate plans for a zip code (``nwp500 tou rates 94903``).
15
+ - **CLI ``tou plan``**: View converted rate plan details with decoded pricing (``nwp500 tou plan 94903 "EV Rate A"``).
16
+ - **CLI ``tou apply``**: Apply a rate plan to the water heater with optional ``--enable`` flag to activate TOU via MQTT.
17
+ - **CLI Reservations Table Output**: ``nwp-cli reservations get`` now displays reservations as a formatted table by default with global status indicator (ENABLED/DISABLED). Use ``--json`` flag for JSON output.
18
+ - **CLI ``anti-legionella set-period``**: New subcommand to change the Anti-Legionella cycle period (1-30 days) without toggling the feature. Use ``nwp-cli anti-legionella set-period 7`` to update cycle period.
19
+
20
+ Changed
21
+ -------
22
+ - **``examples/advanced/tou_openei.py``**: Rewritten to use the new ``OpenEIClient`` and ``convert_tou()``/``update_tou()`` library methods instead of inline OpenEI API calls and client-side conversion.
23
+
24
+ Fixed
25
+ -----
26
+ - **Week Bitfield Encoding (CRITICAL)**: Fixed MGPP week bitfield encoding to match NaviLink APK protocol. Sunday is now correctly bit 7 (128), Monday bit 6 (64), ..., Saturday bit 1 (2); bit 0 is unused. Affects all reservation and TOU schedule operations. Verified against reference captures.
27
+ - **Enable/Disable Convention**: Fixed reservation and TOU enable/disable flags to use standard device boolean convention (1=OFF, 2=ON) instead of inverted logic. This aligns with other device binary sensors and matches app behavior. Global reservation status now correctly shows DISABLED when ``reservationUse=1``.
28
+ - **Reservation Set Command Timeout**: Fixed ``reservations set`` subscription pattern that had extra wildcards preventing response matching. Command now receives confirmations correctly.
29
+ - **Intermittent Fetch Bug**: Tightened MQTT topic filter for reservation fetch from ``/res/`` to ``/res/rsv/`` with content validation to prevent false matches on unrelated response messages.
30
+
31
+ Version 7.4.6 (2026-02-13)
32
+ ==========================
33
+
34
+ Fixed
35
+ -----
36
+ - **Converter Consistency**: ``div_10()`` and ``mul_10()`` now correctly apply division/multiplication to all input types after ``float()`` conversion, not just ``int``/``float`` types
37
+ - **Reservation Decoding**: Fixed ``decode_reservation_hex()`` to validate chunk length before checking for empty entries, preventing potential out-of-bounds access
38
+ - **Factory Cleanup**: ``create_navien_clients()`` now properly cleans up auth session if authentication fails during context manager entry
39
+ - **MQTT Reconnection**: MQTT client now resubscribes to all topics after successful reconnection
40
+ - **Subscription Leak**: Fixed resource leak where ``wait_for_device_feature()`` did not unsubscribe its callback after completion
41
+ - **Duplicate Handlers**: Subscription manager now prevents duplicate callback registration for the same topic
42
+ - **Command Queue**: ``MqttCommandQueue`` now raises on ``QueueFull`` instead of silently swallowing the error
43
+ - **Flow Rate Metadata**: Removed hardcoded ``"GPM"`` unit from ``recirc_dhw_flow_rate`` field; unit is now dynamic based on unit system
44
+ - **Temperature Rounding**: ``RawCelsius`` Fahrenheit conversion now uses a catch-all default for standard rounding instead of matching only ``STANDARD`` enum value
45
+ - **Unit System Default**: ``is_metric_preferred()`` now returns ``False`` (Fahrenheit) instead of ``None`` when no unit system override or context is set
46
+
47
+ Security
48
+ --------
49
+ - **Sensitive Data Logging**: Redacted MQTT topics in subscription manager logging to prevent leaking device IDs (resolves CodeQL alerts)
50
+
51
+ Added
52
+ -----
53
+ - **Auth Session Property**: Added ``NavienAuthClient.session`` property to access the active ``aiohttp`` session without using ``getattr``
54
+ - **Unsubscribe Feature**: Added ``unsubscribe_device_feature()`` method to MQTT client and subscription manager for targeted callback removal
55
+ - **Hypothesis Fuzzing**: Added property-based fuzzing tests for MQTT payload handling
56
+ - **Bandit Security Scanning**: Added bandit configuration for security analysis in CI
57
+
5
58
  Version 7.4.5 (2026-02-04)
6
59
  ==========================
7
60
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 7.4.5
3
+ Version: 7.4.7
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
 
@@ -133,6 +133,14 @@ nwp500.mqtt\_events module
133
133
  :show-inheritance:
134
134
  :undoc-members:
135
135
 
136
+ nwp500.openei module
137
+ --------------------
138
+
139
+ .. automodule:: nwp500.openei
140
+ :members:
141
+ :show-inheritance:
142
+ :undoc-members:
143
+
136
144
  nwp500.temperature module
137
145
  -------------------------
138
146
 
@@ -499,5 +499,5 @@ See Also
499
499
 
500
500
  * :doc:`../protocol/data_conversions` - Temperature field conversions (HalfCelsiusToF, DeciCelsiusToF)
501
501
  * :doc:`../protocol/device_status` - Complete device status field reference
502
- * :doc:`scheduling_features` - Reservation and TOU integration points
502
+ * :doc:`scheduling` - Scheduling and automation guide
503
503
  * :doc:`../python_api/models` - DeviceStatus model field definitions