nwp500-python 9.4.1__tar.gz → 9.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/CHANGELOG.rst +16 -0
  2. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/Makefile +13 -5
  3. {nwp500_python-9.4.1/src/nwp500_python.egg-info → nwp500_python-9.4.2}/PKG-INFO +1 -1
  4. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/subscriptions.py +38 -1
  5. {nwp500_python-9.4.1 → nwp500_python-9.4.2/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  6. nwp500_python-9.4.2/src/nwp500_python.egg-info/scm_version.json +8 -0
  7. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_multi_device.py +149 -0
  8. nwp500_python-9.4.1/src/nwp500_python.egg-info/scm_version.json +0 -8
  9. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.coveragerc +0 -0
  10. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  11. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.github/copilot-instructions.md +0 -0
  12. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.github/workflows/ci.yml +0 -0
  13. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.github/workflows/release.yml +0 -0
  14. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.gitignore +0 -0
  15. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.pre-commit-config.yaml +0 -0
  16. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/.readthedocs.yml +0 -0
  17. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/AUTHORS.rst +0 -0
  18. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/CONTRIBUTING.rst +0 -0
  19. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/LICENSE.txt +0 -0
  20. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/README.rst +0 -0
  21. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/RELEASE.md +0 -0
  22. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/Makefile +0 -0
  23. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/_static/.gitignore +0 -0
  24. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/conf.py +0 -0
  25. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/explanation/advanced-features.rst +0 -0
  26. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/explanation/architecture.rst +0 -0
  27. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/explanation/index.rst +0 -0
  28. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/explanation/tank-energy.rst +0 -0
  29. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/explanation/tou-recovery-cap.rst +0 -0
  30. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/explanation/unknown-values.rst +0 -0
  31. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/authenticate.rst +0 -0
  32. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/auto-recovery.rst +0 -0
  33. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/diagnose-mqtt.rst +0 -0
  34. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/home-assistant.rst +0 -0
  35. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/index.rst +0 -0
  36. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/maintenance.rst +0 -0
  37. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/manage-units.rst +0 -0
  38. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/monitor-status.rst +0 -0
  39. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/optimize-tou.rst +0 -0
  40. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/queue-commands.rst +0 -0
  41. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/schedule-operation.rst +0 -0
  42. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/how-to/track-energy.rst +0 -0
  43. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/index.rst +0 -0
  44. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/openapi.yaml +0 -0
  45. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/project/authors.rst +0 -0
  46. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/project/changelog.rst +0 -0
  47. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/project/contributing.rst +0 -0
  48. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/project/history.rst +0 -0
  49. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/project/license.rst +0 -0
  50. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/configuration.rst +0 -0
  51. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/enumerations.rst +0 -0
  52. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/index.rst +0 -0
  53. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/installation.rst +0 -0
  54. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/data_conversions.rst +0 -0
  55. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/device_features.rst +0 -0
  56. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/device_status.rst +0 -0
  57. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/error_codes.rst +0 -0
  58. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  59. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/quick_reference.rst +0 -0
  60. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/protocol/rest_api.rst +0 -0
  61. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/api_client.rst +0 -0
  62. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/auth_client.rst +0 -0
  63. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/cli.rst +0 -0
  64. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/events.rst +0 -0
  65. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/exceptions.rst +0 -0
  66. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/models.rst +0 -0
  67. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/reference/python_api/mqtt_client.rst +0 -0
  68. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/requirements.txt +0 -0
  69. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/docs/tutorials/getting-started.rst +0 -0
  70. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/.ruff.toml +0 -0
  71. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/README.md +0 -0
  72. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/air_filter_reset.py +0 -0
  73. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/anti_legionella.py +0 -0
  74. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/auto_recovery.py +0 -0
  75. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/combined_callbacks.py +0 -0
  76. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/demand_response.py +0 -0
  77. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/device_capabilities.py +0 -0
  78. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/device_status_debug.py +0 -0
  79. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/energy_analytics.py +0 -0
  80. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/error_code_demo.py +0 -0
  81. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/firmware_payload_capture.py +0 -0
  82. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/mqtt_diagnostics.py +0 -0
  83. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/power_control.py +0 -0
  84. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/recirculation_control.py +0 -0
  85. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/reconnection_demo.py +0 -0
  86. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/reservation_schedule.py +0 -0
  87. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/simple_auto_recovery.py +0 -0
  88. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/token_restoration.py +0 -0
  89. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/tou_openei.py +0 -0
  90. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/tou_schedule.py +0 -0
  91. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/advanced/water_reservation.py +0 -0
  92. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/beginner/01_authentication.py +0 -0
  93. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/beginner/02_list_devices.py +0 -0
  94. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/beginner/03_get_status.py +0 -0
  95. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/beginner/04_set_temperature.py +0 -0
  96. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/advanced_auth_patterns.py +0 -0
  97. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/command_queue.py +0 -0
  98. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/device_status_callback.py +0 -0
  99. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/error_handling.py +0 -0
  100. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/event_driven_control.py +0 -0
  101. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/improved_auth.py +0 -0
  102. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/legacy_auth_constructor.py +0 -0
  103. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  104. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/periodic_requests.py +0 -0
  105. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/set_mode.py +0 -0
  106. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/intermediate/vacation_mode.py +0 -0
  107. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/mask.py +0 -0
  108. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/testing/periodic_device_info.py +0 -0
  109. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/testing/simple_periodic_info.py +0 -0
  110. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/testing/test_api_client.py +0 -0
  111. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/testing/test_mqtt_connection.py +0 -0
  112. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/testing/test_mqtt_messaging.py +0 -0
  113. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/examples/testing/test_periodic_minimal.py +0 -0
  114. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/pyproject.toml +0 -0
  115. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/README.md +0 -0
  116. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/bump_version.py +0 -0
  117. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/diagnose_mqtt_connection.py +0 -0
  118. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/extract_changelog.py +0 -0
  119. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/format.py +0 -0
  120. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/lint.py +0 -0
  121. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/setup-dev.py +0 -0
  122. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/scripts/validate_version.py +0 -0
  123. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/setup.cfg +0 -0
  124. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/setup.py +0 -0
  125. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/__init__.py +0 -0
  126. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/_base.py +0 -0
  127. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/api_client.py +0 -0
  128. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/auth.py +0 -0
  129. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/__init__.py +0 -0
  130. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/__main__.py +0 -0
  131. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/handlers.py +0 -0
  132. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/monitoring.py +0 -0
  133. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/output_formatters.py +0 -0
  134. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/presentation.py +0 -0
  135. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/rich_output.py +0 -0
  136. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/cli/token_storage.py +0 -0
  137. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/command_decorators.py +0 -0
  138. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/config.py +0 -0
  139. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/converters.py +0 -0
  140. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/device_capabilities.py +0 -0
  141. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/device_info_cache.py +0 -0
  142. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/encoding.py +0 -0
  143. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/enums.py +0 -0
  144. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/events.py +0 -0
  145. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/exceptions.py +0 -0
  146. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/factory.py +0 -0
  147. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/field_factory.py +0 -0
  148. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/__init__.py +0 -0
  149. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/_converters.py +0 -0
  150. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/device.py +0 -0
  151. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/diagnostics.py +0 -0
  152. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/energy.py +0 -0
  153. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/feature.py +0 -0
  154. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/firmware.py +0 -0
  155. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/mqtt_models.py +0 -0
  156. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/schedule.py +0 -0
  157. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/status.py +0 -0
  158. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/models/tou.py +0 -0
  159. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/__init__.py +0 -0
  160. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/_control_commands.py +0 -0
  161. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
  162. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/client.py +0 -0
  163. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/command_queue.py +0 -0
  164. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/connection.py +0 -0
  165. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/control.py +0 -0
  166. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/diagnostics.py +0 -0
  167. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/periodic.py +0 -0
  168. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/reconnection.py +0 -0
  169. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/state_tracker.py +0 -0
  170. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/types.py +0 -0
  171. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt/utils.py +0 -0
  172. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/mqtt_events.py +0 -0
  173. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/openei.py +0 -0
  174. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/py.typed +0 -0
  175. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/reservations.py +0 -0
  176. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/temperature.py +0 -0
  177. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/topic_builder.py +0 -0
  178. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/tou_schedule.py +0 -0
  179. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/unit_system.py +0 -0
  180. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500/utils.py +0 -0
  181. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/SOURCES.txt +0 -0
  182. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  183. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  184. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  185. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/requires.txt +0 -0
  186. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/scm_file_list.json +0 -0
  187. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/src/nwp500_python.egg-info/top_level.txt +0 -0
  188. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/conftest.py +0 -0
  189. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_api_helpers.py +0 -0
  190. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_app_queries.py +0 -0
  191. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_auth.py +0 -0
  192. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_auth_session_lifecycle.py +0 -0
  193. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_bug_fixes.py +0 -0
  194. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_canonical_schedule.py +0 -0
  195. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_cli_basic.py +0 -0
  196. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_cli_commands.py +0 -0
  197. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_cli_tristate_presentation.py +0 -0
  198. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_command_decorators.py +0 -0
  199. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_command_queue.py +0 -0
  200. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_device_capabilities.py +0 -0
  201. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_device_info_cache.py +0 -0
  202. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_device_rest_models.py +0 -0
  203. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_events.py +0 -0
  204. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_exceptions.py +0 -0
  205. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_model_converters.py +0 -0
  206. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_models.py +0 -0
  207. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_clean_session_resume.py +0 -0
  208. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_client_init.py +0 -0
  209. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_events.py +0 -0
  210. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_hypothesis.py +0 -0
  211. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_reconnection.py +0 -0
  212. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_reconnection_storm.py +0 -0
  213. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_mqtt_reliability.py +0 -0
  214. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_openei.py +0 -0
  215. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_protocol_correctness.py +0 -0
  216. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_public_api.py +0 -0
  217. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_reservations.py +0 -0
  218. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_temperature_converters.py +0 -0
  219. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_threading_model.py +0 -0
  220. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_token_storage.py +0 -0
  221. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_tou_api.py +0 -0
  222. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_tou_schedule.py +0 -0
  223. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_unit_switching.py +0 -0
  224. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_unit_system_process_wide.py +0 -0
  225. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_utility_modules.py +0 -0
  226. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tests/test_utils.py +0 -0
  227. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/tox.ini +0 -0
  228. {nwp500_python-9.4.1 → nwp500_python-9.4.2}/uv.lock +0 -0
@@ -5,6 +5,22 @@ Changelog
5
5
  Unreleased
6
6
  ==========
7
7
 
8
+ Version 9.4.2 (2026-09-17)
9
+ ==========================
10
+
11
+ Changed
12
+ -------
13
+ - **A dropped response message now says why it was dropped.** The typed
14
+ response handlers declined a message in three places without a word: a
15
+ topic that does not end in the required suffix, a payload with no data
16
+ under the expected key, and a reply whose ``macAddress`` names another
17
+ device. A caller watching for a reply that never reached its callback
18
+ could not tell a dropped message from one the cloud never published, and
19
+ the two want opposite fixes. Each branch now logs its reason at DEBUG,
20
+ with the topic and MAC redacted as elsewhere, and guarded by
21
+ ``isEnabledFor`` so the redaction regexes do not run on the normal
22
+ dispatch path when debug logging is off.
23
+
8
24
  Version 9.4.1 (2026-09-17)
9
25
  ==========================
10
26
 
@@ -1,4 +1,4 @@
1
- .PHONY: help install install-dev lint format test clean build release check-release ci-lint ci-format ci-check version-bump validate-version
1
+ .PHONY: help install install-dev lint format test test-cov require-python clean build release check-release ci-lint ci-format ci-check version-bump validate-version
2
2
 
3
3
  help: ## Show this help message
4
4
  @echo 'Usage: make [target]'
@@ -35,11 +35,19 @@ ci-format: ## Run formatting exactly as CI does (via tox format)
35
35
  ci-check: ci-lint ## Run the same checks as CI (ensures local/CI consistency)
36
36
  @echo "🎉 All CI checks passed locally!"
37
37
 
38
- test: ## Run tests with pytest
39
- pytest
38
+ # The package declares python_requires = >=3.14 and uses 3.14 syntax, but
39
+ # that floor is only enforced when installing - running pytest against the
40
+ # source tree happens on whatever interpreter is active. Fail with a clear
41
+ # message instead of a SyntaxError in every test module.
42
+ require-python:
43
+ @python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 14) else 1)' \
44
+ || { echo "Python 3.14+ required, found $$(python3 -V 2>&1). Activate the project environment."; exit 1; }
40
45
 
41
- test-cov: ## Run tests with coverage report
42
- pytest --cov=nwp500 --cov-report=html --cov-report=term-missing
46
+ test: require-python ## Run tests with pytest
47
+ python3 -m pytest
48
+
49
+ test-cov: require-python ## Run tests with coverage report
50
+ python3 -m pytest --cov=nwp500 --cov-report=html --cov-report=term-missing
43
51
 
44
52
  clean: ## Remove build artifacts and cache files
45
53
  rm -rf build dist *.egg-info .eggs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.4.1
3
+ Version: 9.4.2
4
4
  Summary: A library for controlling Navien NWP500 Water Heaters via NaviLink
5
5
  Home-page: https://github.com/eman/nwp500-python
6
6
  Author: Emmanuel Levijarvi
@@ -40,7 +40,12 @@ from ..mqtt_events import (
40
40
  from ..topic_builder import MqttTopicBuilder
41
41
  from .state_tracker import DeviceStateTracker
42
42
  from .types import QoS, to_awscrt_qos
43
- from .utils import get_response_data, redact_topic, topic_matches_pattern
43
+ from .utils import (
44
+ get_response_data,
45
+ redact_mac,
46
+ redact_topic,
47
+ topic_matches_pattern,
48
+ )
44
49
 
45
50
  if TYPE_CHECKING:
46
51
  from ..device_info_cache import MqttDeviceInfoCache
@@ -614,13 +619,45 @@ class MqttSubscriptionManager:
614
619
  """
615
620
 
616
621
  def handler(topic: str, message: dict[str, Any]) -> None:
622
+ # Every branch that declines a message says so at debug level.
623
+ # Silence here is expensive to diagnose: a caller watching for a
624
+ # reply that never reaches its callback cannot tell a dropped
625
+ # message from one the cloud never sent, and the two want
626
+ # opposite fixes. The logs are the only evidence either way.
627
+ #
628
+ # Each log is guarded: redact_topic runs a dozen regexes, and
629
+ # these branches are the normal case for a handler registered
630
+ # under the device wildcard, which sees every message for that
631
+ # device and keeps only its own.
617
632
  if topic_suffix and not topic.endswith(topic_suffix):
633
+ if _logger.isEnabledFor(logging.DEBUG):
634
+ _logger.debug(
635
+ "Ignoring a message on %s: the topic does not end "
636
+ "with %r, which this handler requires",
637
+ redact_topic(topic),
638
+ topic_suffix,
639
+ )
618
640
  return
619
641
  try:
620
642
  data = get_response_data(message, key)
621
643
  if not data:
644
+ if _logger.isEnabledFor(logging.DEBUG):
645
+ _logger.debug(
646
+ "Ignoring a message on %s: no %s data to parse "
647
+ "as %s",
648
+ redact_topic(topic),
649
+ f"{key!r}" if key else "response",
650
+ model.__name__,
651
+ )
622
652
  return
623
653
  if only_mac and not _same_device(data, message, only_mac):
654
+ if _logger.isEnabledFor(logging.DEBUG):
655
+ _logger.debug(
656
+ "Ignoring a message on %s: it names another "
657
+ "device, not %s",
658
+ redact_topic(topic),
659
+ redact_mac(only_mac),
660
+ )
624
661
  return
625
662
 
626
663
  parsed = parse(data) if parse else model.model_validate(data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 9.4.1
3
+ Version: 9.4.2
4
4
  Summary: A library for controlling Navien NWP500 Water Heaters via NaviLink
5
5
  Home-page: https://github.com/eman/nwp500-python
6
6
  Author: Emmanuel Levijarvi
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "9.4.2",
3
+ "distance": 0,
4
+ "node": "gfe7fe156d7d292a934e3bfa48eb7f274b00ffe33",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-18"
8
+ }
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  from unittest.mock import AsyncMock, MagicMock
2
3
 
3
4
  import pytest
@@ -5,6 +6,7 @@ import pytest
5
6
  from nwp500.enums import CurrentOperationMode
6
7
  from nwp500.events import EventEmitter
7
8
  from nwp500.models import DeviceFeature, DeviceStatus
9
+ from nwp500.mqtt import subscriptions as subscriptions_module
8
10
  from nwp500.mqtt.state_tracker import DeviceStateTracker
9
11
  from nwp500.mqtt.subscriptions import MqttSubscriptionManager
10
12
  from nwp500.mqtt_events import (
@@ -241,3 +243,150 @@ def test_make_handler_injects_mac():
241
243
  parsed = callback_called[0]
242
244
  assert isinstance(parsed, DeviceStatus)
243
245
  assert parsed.mac_address == mac
246
+
247
+
248
+ def _manager() -> MqttSubscriptionManager:
249
+ return MqttSubscriptionManager(
250
+ connection=MagicMock(),
251
+ client_id="test_client",
252
+ event_emitter=MagicMock(),
253
+ schedule_coroutine=MagicMock(),
254
+ )
255
+
256
+
257
+ @pytest.mark.parametrize(
258
+ ("kwargs", "topic", "message", "expected"),
259
+ [
260
+ (
261
+ {"topic_suffix": "st/rsv"},
262
+ "cmd/52/client/res/td/rd",
263
+ {"response": {"reservation": []}},
264
+ "does not end with",
265
+ ),
266
+ (
267
+ {"key": "status"},
268
+ "cmd/52/client/res/st",
269
+ {"response": {}},
270
+ "no 'status' data to parse",
271
+ ),
272
+ (
273
+ {"key": "status", "only_mac": "00:11:22:33:44:55"},
274
+ "cmd/52/client/res/st",
275
+ {"status": {"macAddress": "aa:bb:cc:dd:ee:ff"}},
276
+ "names another device",
277
+ ),
278
+ ],
279
+ ids=["topic-suffix", "no-data", "other-device"],
280
+ )
281
+ def test_a_declined_message_says_why(kwargs, topic, message, expected, caplog):
282
+ """Every branch that drops a message logs its reason.
283
+
284
+ Silence here cost real diagnosis time: a reply that never reaches its
285
+ callback looks identical to one the cloud never sent, and the two want
286
+ opposite fixes.
287
+ """
288
+ delivered = []
289
+ handler = _manager()._make_handler(
290
+ model=DeviceStatus, callback=delivered.append, **kwargs
291
+ )
292
+
293
+ with caplog.at_level(logging.DEBUG, logger="nwp500.mqtt.subscriptions"):
294
+ handler(topic, message)
295
+
296
+ assert delivered == []
297
+ assert any(expected in r.getMessage() for r in caplog.records), caplog.text
298
+
299
+
300
+ def test_a_declined_message_never_logs_a_raw_identifier(caplog):
301
+ """The reason is logged; the MAC in it is not.
302
+
303
+ These lines carry a topic and a MAC straight from the wire, so the
304
+ redaction is the whole reason they are safe to emit at all.
305
+ """
306
+ mac = "04786332fca0"
307
+ other = "aabbccddeeff"
308
+ topic = f"cmd/52/navilink-{mac}/st/td/rd"
309
+ delivered = []
310
+ handler = _manager()._make_handler(
311
+ model=DeviceStatus,
312
+ callback=delivered.append,
313
+ key="status",
314
+ only_mac=mac,
315
+ parse=lambda data: data,
316
+ )
317
+
318
+ with caplog.at_level(logging.DEBUG, logger="nwp500.mqtt.subscriptions"):
319
+ handler(topic, {"status": {"macAddress": other, "command": 0}})
320
+
321
+ assert delivered == []
322
+ assert "names another device" in caplog.text
323
+ assert mac not in caplog.text
324
+ assert other not in caplog.text
325
+ assert "REDACTED" in caplog.text
326
+
327
+
328
+ @pytest.mark.parametrize(
329
+ ("level", "redactions"),
330
+ [(logging.INFO, 0), (logging.DEBUG, 1)],
331
+ ids=["debug-off", "debug-on"],
332
+ )
333
+ def test_declining_a_message_redacts_only_when_debug_is_on(
334
+ level, redactions, monkeypatch
335
+ ):
336
+ """The guard, not just the log level.
337
+
338
+ redact_topic is a dozen regexes, and a handler registered under the
339
+ device wildcard declines most of what it sees, so an unguarded call
340
+ would run them on the normal dispatch path for nothing.
341
+ """
342
+ calls = []
343
+ real = subscriptions_module.redact_topic
344
+
345
+ def counting_redact_topic(topic):
346
+ calls.append(topic)
347
+ return real(topic)
348
+
349
+ monkeypatch.setattr(
350
+ subscriptions_module, "redact_topic", counting_redact_topic
351
+ )
352
+ # setLevel, not the attribute: isEnabledFor caches its answer, and
353
+ # only setLevel invalidates that cache.
354
+ logger = subscriptions_module._logger
355
+ original_level = logger.level
356
+ logger.setLevel(level)
357
+ monkeypatch.setattr(logger, "propagate", False)
358
+
359
+ handler = _manager()._make_handler(
360
+ model=DeviceStatus,
361
+ callback=lambda parsed: None,
362
+ key="status",
363
+ topic_suffix="st/rsv",
364
+ )
365
+ try:
366
+ handler("cmd/52/client/res/td/rd", {"status": {"command": 0}})
367
+ finally:
368
+ logger.setLevel(original_level)
369
+
370
+ assert len(calls) == redactions
371
+
372
+
373
+ def test_a_message_for_this_device_is_still_delivered(caplog):
374
+ """The mac filter accepts its own device, and says nothing about it."""
375
+ mac = "00:11:22:33:44:55"
376
+ delivered = []
377
+ handler = _manager()._make_handler(
378
+ model=DeviceStatus,
379
+ callback=delivered.append,
380
+ key="status",
381
+ only_mac=mac,
382
+ parse=lambda data: data,
383
+ )
384
+
385
+ with caplog.at_level(logging.DEBUG, logger="nwp500.mqtt.subscriptions"):
386
+ handler(
387
+ "cmd/52/client/res/st",
388
+ {"response": {"status": {"macAddress": mac, "command": 0}}},
389
+ )
390
+
391
+ assert len(delivered) == 1
392
+ assert "Ignoring" not in caplog.text
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "9.4.1",
3
- "distance": 0,
4
- "node": "g36a97ba8f8ce20912973b352fe380686a4f46ad1",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-09-17"
8
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes