nwp500-python 8.1.3__tar.gz → 9.0.0__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 (212) hide show
  1. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/.gitignore +3 -0
  2. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/CHANGELOG.rst +302 -0
  3. {nwp500_python-8.1.3/src/nwp500_python.egg-info → nwp500_python-9.0.0}/PKG-INFO +1 -1
  4. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/how-to/optimize-tou.rst +7 -6
  5. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/how-to/schedule-operation.rst +3 -3
  6. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/python_api/auth_client.rst +0 -4
  7. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/python_api/cli.rst +4 -9
  8. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/python_api/exceptions.rst +0 -57
  9. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/python_api/mqtt_client.rst +1 -1
  10. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/mqtt_diagnostics.py +4 -5
  11. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/tou_openei.py +2 -0
  12. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/mask.py +0 -2
  13. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/pyproject.toml +19 -0
  14. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/__init__.py +1 -49
  15. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/_base.py +11 -2
  16. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/api_client.py +19 -7
  17. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/auth.py +124 -36
  18. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/cli/__init__.py +0 -2
  19. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/cli/__main__.py +7 -5
  20. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/cli/handlers.py +2 -4
  21. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/cli/monitoring.py +0 -2
  22. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/cli/output_formatters.py +4 -19
  23. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/cli/rich_output.py +2 -3
  24. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/cli/token_storage.py +7 -4
  25. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/command_decorators.py +1 -3
  26. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/config.py +0 -2
  27. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/converters.py +0 -37
  28. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/device_capabilities.py +2 -4
  29. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/device_info_cache.py +0 -2
  30. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/encoding.py +39 -13
  31. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/enums.py +0 -2
  32. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/events.py +22 -22
  33. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/exceptions.py +0 -59
  34. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/factory.py +0 -2
  35. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/field_factory.py +57 -67
  36. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/models/__init__.py +0 -2
  37. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/models/_converters.py +0 -2
  38. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/models/device.py +0 -2
  39. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/models/energy.py +0 -2
  40. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/models/feature.py +0 -2
  41. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/models/mqtt_models.py +0 -2
  42. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/models/schedule.py +0 -2
  43. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/models/status.py +6 -4
  44. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/models/tou.py +0 -2
  45. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/mqtt/__init__.py +0 -2
  46. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/mqtt/client.py +77 -46
  47. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/mqtt/command_queue.py +47 -58
  48. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/mqtt/connection.py +68 -73
  49. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/mqtt/control.py +16 -6
  50. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/mqtt/diagnostics.py +0 -2
  51. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/mqtt/periodic.py +20 -9
  52. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/mqtt/reconnection.py +72 -15
  53. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/mqtt/state_tracker.py +4 -4
  54. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/mqtt/subscriptions.py +63 -39
  55. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/mqtt/utils.py +35 -4
  56. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/mqtt_events.py +12 -14
  57. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/openei.py +15 -3
  58. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/reservations.py +0 -2
  59. nwp500_python-9.0.0/src/nwp500/temperature.py +254 -0
  60. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/topic_builder.py +0 -2
  61. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/unit_system.py +24 -23
  62. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/utils.py +0 -2
  63. {nwp500_python-8.1.3 → nwp500_python-9.0.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
  64. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500_python.egg-info/SOURCES.txt +9 -1
  65. nwp500_python-9.0.0/src/nwp500_python.egg-info/scm_file_list.json +203 -0
  66. nwp500_python-9.0.0/src/nwp500_python.egg-info/scm_version.json +8 -0
  67. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_auth.py +2 -11
  68. nwp500_python-9.0.0/tests/test_auth_session_lifecycle.py +275 -0
  69. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_bug_fixes.py +2 -3
  70. nwp500_python-9.0.0/tests/test_cli_basic.py +111 -0
  71. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_cli_commands.py +15 -0
  72. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_events.py +2 -1
  73. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_exceptions.py +0 -39
  74. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_mqtt_clean_session_resume.py +0 -2
  75. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_mqtt_client_init.py +49 -0
  76. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_mqtt_reconnection.py +0 -2
  77. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_mqtt_reconnection_storm.py +1 -3
  78. nwp500_python-9.0.0/tests/test_mqtt_reliability.py +630 -0
  79. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_multi_device.py +2 -2
  80. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_openei.py +23 -0
  81. nwp500_python-9.0.0/tests/test_protocol_correctness.py +455 -0
  82. nwp500_python-9.0.0/tests/test_public_api.py +61 -0
  83. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_reservations.py +0 -2
  84. nwp500_python-9.0.0/tests/test_threading_model.py +305 -0
  85. nwp500_python-9.0.0/tests/test_token_storage.py +61 -0
  86. nwp500_python-9.0.0/tests/test_utility_modules.py +149 -0
  87. nwp500_python-8.1.3/src/nwp500/cli/commands.py +0 -91
  88. nwp500_python-8.1.3/src/nwp500/temperature.py +0 -447
  89. nwp500_python-8.1.3/tests/test_cli_basic.py +0 -27
  90. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/.coveragerc +0 -0
  91. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
  92. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/.github/copilot-instructions.md +0 -0
  93. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/.github/workflows/ci.yml +0 -0
  94. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/.github/workflows/release.yml +0 -0
  95. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/.pre-commit-config.yaml +0 -0
  96. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/.readthedocs.yml +0 -0
  97. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/AUTHORS.rst +0 -0
  98. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/CONTRIBUTING.rst +0 -0
  99. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/LICENSE.txt +0 -0
  100. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/Makefile +0 -0
  101. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/README.rst +0 -0
  102. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/RELEASE.md +0 -0
  103. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/Makefile +0 -0
  104. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/_static/.gitignore +0 -0
  105. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/conf.py +0 -0
  106. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/explanation/advanced-features.rst +0 -0
  107. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/explanation/architecture.rst +0 -0
  108. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/explanation/index.rst +0 -0
  109. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/how-to/authenticate.rst +0 -0
  110. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/how-to/auto-recovery.rst +0 -0
  111. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/how-to/diagnose-mqtt.rst +0 -0
  112. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/how-to/home-assistant.rst +0 -0
  113. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/how-to/index.rst +0 -0
  114. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/how-to/maintenance.rst +0 -0
  115. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/how-to/manage-units.rst +0 -0
  116. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/how-to/monitor-status.rst +0 -0
  117. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/how-to/queue-commands.rst +0 -0
  118. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/how-to/track-energy.rst +0 -0
  119. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/index.rst +0 -0
  120. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/openapi.yaml +0 -0
  121. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/project/authors.rst +0 -0
  122. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/project/changelog.rst +0 -0
  123. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/project/contributing.rst +0 -0
  124. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/project/history.rst +0 -0
  125. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/project/license.rst +0 -0
  126. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/configuration.rst +0 -0
  127. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/enumerations.rst +0 -0
  128. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/index.rst +0 -0
  129. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/installation.rst +0 -0
  130. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/protocol/data_conversions.rst +0 -0
  131. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/protocol/device_features.rst +0 -0
  132. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/protocol/device_status.rst +0 -0
  133. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/protocol/error_codes.rst +0 -0
  134. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
  135. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/protocol/quick_reference.rst +0 -0
  136. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/protocol/rest_api.rst +0 -0
  137. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/python_api/api_client.rst +0 -0
  138. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/python_api/events.rst +0 -0
  139. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/reference/python_api/models.rst +0 -0
  140. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/requirements.txt +0 -0
  141. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/docs/tutorials/getting-started.rst +0 -0
  142. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/.ruff.toml +0 -0
  143. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/README.md +0 -0
  144. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/air_filter_reset.py +0 -0
  145. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/anti_legionella.py +0 -0
  146. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/auto_recovery.py +0 -0
  147. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/combined_callbacks.py +0 -0
  148. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/demand_response.py +0 -0
  149. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/device_capabilities.py +0 -0
  150. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/device_status_debug.py +0 -0
  151. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/energy_analytics.py +0 -0
  152. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/error_code_demo.py +0 -0
  153. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/firmware_payload_capture.py +0 -0
  154. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/power_control.py +0 -0
  155. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/recirculation_control.py +0 -0
  156. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/reconnection_demo.py +0 -0
  157. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/reservation_schedule.py +0 -0
  158. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/simple_auto_recovery.py +0 -0
  159. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/token_restoration.py +0 -0
  160. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/tou_schedule.py +0 -0
  161. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/advanced/water_reservation.py +0 -0
  162. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/beginner/01_authentication.py +0 -0
  163. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/beginner/02_list_devices.py +0 -0
  164. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/beginner/03_get_status.py +0 -0
  165. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/beginner/04_set_temperature.py +0 -0
  166. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
  167. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/intermediate/command_queue.py +0 -0
  168. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/intermediate/device_status_callback.py +0 -0
  169. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/intermediate/error_handling.py +0 -0
  170. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/intermediate/event_driven_control.py +0 -0
  171. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/intermediate/improved_auth.py +0 -0
  172. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
  173. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
  174. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/intermediate/periodic_requests.py +0 -0
  175. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/intermediate/set_mode.py +0 -0
  176. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/intermediate/vacation_mode.py +0 -0
  177. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/testing/periodic_device_info.py +0 -0
  178. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/testing/simple_periodic_info.py +0 -0
  179. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/testing/test_api_client.py +0 -0
  180. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/testing/test_mqtt_connection.py +0 -0
  181. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/testing/test_mqtt_messaging.py +0 -0
  182. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/examples/testing/test_periodic_minimal.py +0 -0
  183. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/scripts/README.md +0 -0
  184. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/scripts/bump_version.py +0 -0
  185. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/scripts/diagnose_mqtt_connection.py +0 -0
  186. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/scripts/extract_changelog.py +0 -0
  187. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/scripts/format.py +0 -0
  188. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/scripts/lint.py +0 -0
  189. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/scripts/setup-dev.py +0 -0
  190. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/scripts/validate_version.py +0 -0
  191. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/setup.cfg +0 -0
  192. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/setup.py +0 -0
  193. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500/py.typed +0 -0
  194. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
  195. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
  196. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
  197. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500_python.egg-info/requires.txt +0 -0
  198. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
  199. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/conftest.py +0 -0
  200. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_api_helpers.py +0 -0
  201. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_command_decorators.py +0 -0
  202. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_command_queue.py +0 -0
  203. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_device_capabilities.py +0 -0
  204. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_device_info_cache.py +0 -0
  205. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_model_converters.py +0 -0
  206. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_models.py +0 -0
  207. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_mqtt_hypothesis.py +0 -0
  208. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_temperature_converters.py +0 -0
  209. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_tou_api.py +0 -0
  210. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_unit_switching.py +0 -0
  211. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tests/test_utils.py +0 -0
  212. {nwp500_python-8.1.3 → nwp500_python-9.0.0}/tox.ini +0 -0
@@ -68,3 +68,6 @@ resources/*
68
68
  .ruff_cache/
69
69
  .bandit
70
70
  .agent/
71
+ .agents/
72
+ .env
73
+ skills-lock.json
@@ -5,6 +5,308 @@ Changelog
5
5
  Unreleased
6
6
  ==========
7
7
 
8
+ Version 9.0.0 (2026-07-05)
9
+ ==========================
10
+
11
+ **BREAKING CHANGES**: Public API surface trimmed and dead code removed.
12
+ These changes require a major version bump.
13
+
14
+ Modernization (Python 3.14)
15
+ ---------------------------
16
+ - **Removed** ``from __future__ import annotations`` **project-wide**
17
+ (57 files): redundant on a Python >=3.14-only package where lazy
18
+ annotation evaluation (PEP 649) is the default.
19
+ - **Adopted additional ruff rule sets**: ``RUF`` (Ruff-specific),
20
+ ``DTZ`` (naive datetime), ``PTH`` (pathlib), ``ASYNC``, and ``PERF``,
21
+ with documented ignores for intentional patterns (grouped ``__all__``
22
+ lists, unicode degree signs, public ``timeout`` parameters). Fixes
23
+ applied for all resulting findings, including a timezone-naive
24
+ timestamp in CSV exports (now timezone-aware local time),
25
+ ``Path.open()`` usage, ``itertools.pairwise()`` in the CLI range
26
+ collapser, a ``ClassVar`` annotation on the capability map, and
27
+ dangling ``asyncio.create_task`` references in tests.
28
+ - **PeriodicRequestType is now a StrEnum** (was a plain ``Enum`` with
29
+ string values), matching the enum style used elsewhere.
30
+ - **Event payload dataclasses use** ``slots=True``: the frozen event
31
+ dataclasses in ``mqtt_events.py`` and ``events.EventListener`` are
32
+ created per state change; slots reduce their memory footprint.
33
+ - **match statement** for the periodic request-type dispatch.
34
+
35
+ Testing
36
+ -------
37
+ - New unit tests for previously untested modules:
38
+ ``topic_builder.py`` (full topic schema), ``field_factory.py``
39
+ (metadata defaults, overrides, merge semantics), and
40
+ ``models/_converters.py`` (unit-preference conversions and
41
+ round-trips).
42
+
43
+ Removed
44
+ -------
45
+ - **Deprecated ``.control`` property**: removed
46
+ ``NavienMqttClient.control`` (deprecated shim slated for v9.0.0; the
47
+ project policy is to remove rather than deprecate). Use the delegated
48
+ methods on the client directly:
49
+
50
+ .. code-block:: python
51
+
52
+ # OLD (removed)
53
+ await client.control.set_power(device, True)
54
+
55
+ # NEW
56
+ await client.set_power(device, True)
57
+
58
+ - **Unused exception classes**: removed ``TokenExpiredError``,
59
+ ``MqttSubscriptionError``, ``DeviceNotFoundError``,
60
+ ``DeviceOfflineError``, and ``DeviceOperationError``. None of them was
61
+ ever raised by the library, so no working error handling can break;
62
+ catch the parent classes (``AuthenticationError``, ``MqttError``,
63
+ ``DeviceError``) instead.
64
+ - **Internal plumbing removed from the top-level namespace**: the
65
+ package no longer re-exports ``requires_capability``,
66
+ ``MqttDeviceInfoCache``, ``MqttDeviceCapabilityChecker``,
67
+ ``log_performance``, or the bit-encoding helpers
68
+ (``encode_week_bitfield``, ``decode_week_bitfield``,
69
+ ``encode_season_bitfield``, ``decode_season_bitfield``,
70
+ ``encode_price``, ``decode_price``, ``build_reservation_entry``,
71
+ ``build_tou_period``). Import them from their owning modules:
72
+
73
+ .. code-block:: python
74
+
75
+ # OLD (removed)
76
+ from nwp500 import build_reservation_entry, encode_price
77
+
78
+ # NEW
79
+ from nwp500.encoding import build_reservation_entry, encode_price
80
+
81
+ - **Dead code**: removed the never-imported ``nwp500.cli.commands``
82
+ module (its metadata had drifted from the real click commands), the
83
+ unused ``converters.str_enum_validator``, the unused module-level
84
+ ``temperature.half_celsius_to_fahrenheit`` /
85
+ ``deci_celsius_to_fahrenheit`` wrappers, the no-op
86
+ ``NavienMqttClient._on_message_received`` placeholder, and unused
87
+ width calculations in the CLI formatters.
88
+
89
+ Changed
90
+ -------
91
+ - **Temperature classes deduplicated**: ``HalfCelsius``, ``DeciCelsius``,
92
+ ``RawCelsius``, and ``DeciCelsiusDelta`` were four near-identical
93
+ copies differing only in a scale constant. All conversions are now
94
+ implemented once on the ``Temperature`` base class with a per-class
95
+ ``_scale``; only special rounding (``RawCelsius``) and delta semantics
96
+ (``DeciCelsiusDelta``) are overridden. Behavior is unchanged
97
+ (~200 lines removed).
98
+ - **field_factory deduplicated**: the four field factories shared a
99
+ copy-pasted metadata-merge block, now extracted into a single private
100
+ helper. Behavior is unchanged.
101
+ - **Device boolean encoding centralized**: ``mqtt/control.py`` now uses
102
+ ``converters.device_bool_from_python()`` instead of inline
103
+ ``2 if enabled else 1`` literals.
104
+ - **Version resolution decoupled**: ``auth.py`` resolves the package
105
+ version from distribution metadata instead of ``from . import
106
+ __version__``, removing an order-dependent near-circular import.
107
+
108
+ Bug Fixes
109
+ ---------
110
+ - **Fix malformed weekly reservation and recirculation schedule
111
+ payloads**: ``update_weekly_reservation()`` and
112
+ ``configure_recirculation_schedule()`` dumped the schedule models
113
+ as-is, double-nesting the request (``request.reservation.reservation``
114
+ / ``request.schedule.schedule``) and leaking pydantic computed display
115
+ fields — including a unit-converted ``temperature`` alongside the raw
116
+ half-Celsius ``param`` — into device commands. Both now send the flat,
117
+ raw protocol shape used by ``update_reservations()``. A new
118
+ ``NavienBaseModel.to_protocol_dict()`` dumps only declared protocol
119
+ fields.
120
+ - **Preserve command order when a queued flush fails**: a command that
121
+ failed mid-flush was re-queued at the tail, behind commands queued
122
+ after it, inverting order-sensitive sequences (e.g. ``set_temp``
123
+ replayed before ``power_on``). The queue is now a deque and failed
124
+ commands are re-inserted at the front.
125
+ - **Expire stale queued commands**: queued commands stored a timestamp
126
+ that was never checked, so a multi-hour outage replayed hours-old
127
+ control commands (e.g. ``set_power``) to the appliance on reconnect.
128
+ Commands older than ``MqttConnectionConfig.max_queued_command_age``
129
+ (default 300 s, ``None`` to disable) are now discarded at send time.
130
+ - **Fix Fahrenheit conversion for sub-zero temperatures** (ASYMMETRIC
131
+ formula): the rounding used Python's floored ``%``, which is always
132
+ non-negative, while the firmware/app uses a truncated remainder. Raw
133
+ ``-11`` (-5.5 °C) decoded to 22 °F instead of the app's 23 °F. Now
134
+ uses ``math.fmod`` semantics.
135
+ - **Fix freeze protection default limits**: the defaults (43/65) were
136
+ Fahrenheit display values stored in raw half-Celsius fields, decoding
137
+ to 70.7 °F / 90.5 °F when the device omitted them. Corrected to raw
138
+ 12/20 (43 °F / 50 °F), matching the documented fixed limits.
139
+ - **Emit error_detected when the error code changes**: a transition
140
+ between two non-zero error codes (e.g. E799 → E407) emitted no event,
141
+ so consumers kept displaying the stale error.
142
+ - **Fix TOU price encoding**: ``encode_price()`` used banker's rounding,
143
+ under-encoding exact half values at even boundaries (0.125 at
144
+ ``decimal_point=2`` encoded to 12 instead of 13) — now uses
145
+ ``Decimal`` half-up rounding. ``build_tou_period()`` also treated
146
+ ``bool`` prices as pre-encoded integers (``True`` sent as price 1).
147
+ - **Fix protocol documentation errors**: ``decode_reservation_hex()``
148
+ documented the enable flag inverted (1=enabled instead of 2=enabled);
149
+ the ``build_reservation_entry()`` example showed ``week: 158`` for
150
+ Mon/Wed/Fri instead of the correct 84; temperature doctest examples
151
+ showed ``int`` raw values where ``float`` is returned.
152
+ - **Run MQTT message dispatch on the event loop**: JSON parsing, pydantic
153
+ model validation, and user callbacks all executed directly on the AWS
154
+ CRT network thread. A slow or blocking callback (e.g. the CLI monitor's
155
+ CSV writes) stalled all MQTT message processing, user callbacks ran on
156
+ an undocumented SDK thread where asyncio operations are unsafe, and the
157
+ handler registry could be mutated on the event loop while the CRT
158
+ thread iterated it (``RuntimeError: dictionary changed size during
159
+ iteration``, dropping the message — most likely during the
160
+ reconnect/resubscribe window). The awscrt callback now only marshals
161
+ the raw payload onto the event loop; parsing and dispatch run there,
162
+ iterating snapshots of the handler registries.
163
+ - **Stop one raising handler from aborting message delivery**: handler
164
+ dispatch caught only ``(TypeError, AttributeError, KeyError)``; a user
165
+ callback raising anything else (e.g. ``ValueError``) escaped into the
166
+ awscrt callback machinery and skipped the remaining handlers for that
167
+ message. Individual handler failures are now logged and isolated.
168
+ - **Make the unit system preference process-wide**:
169
+ ``set_unit_system()`` stored the preference in a ``ContextVar`` set in
170
+ the caller's task. Tasks scheduled from AWS CRT callback threads never
171
+ inherit that context, so the preference silently reverted to
172
+ auto-detect for all MQTT-delivered data — ``nwp-cli --unit-system
173
+ metric monitor`` logged temperatures in the device's native unit while
174
+ labeling them °C. The preference is now a process-wide setting visible
175
+ from every task and thread.
176
+ - **Fix once-listeners firing more than once**: ``EventEmitter.emit()``
177
+ removed one-time listeners only after invoking them, so a callback
178
+ that raised stayed registered forever, and two overlapping emits could
179
+ both fire the same once-listener. Once-listeners are now removed
180
+ before invocation.
181
+ - **Fix wait_for() leaking its listener on cancellation**:
182
+ ``EventEmitter.wait_for()`` removed its listener only on timeout; a
183
+ cancelled waiter left the listener registered until the event next
184
+ fired, setting a result on a dead future. Cleanup now happens in a
185
+ ``finally`` block.
186
+ - **Fix wait_for() docstring examples**: examples showed
187
+ ``args, _ = await emitter.wait_for(...)``, but ``wait_for`` returns
188
+ just the args tuple — following the documented pattern raised
189
+ ``ValueError`` or silently mis-assigned.
190
+ - **Serialize concurrent token refresh**: ``ensure_valid_token()`` and
191
+ ``refresh_token()`` had no lock, so concurrent callers (API 401 retry,
192
+ MQTT reconnect, periodic requests) at token expiry fired parallel
193
+ refresh requests; with token rotation the losers were left holding
194
+ invalidated tokens. Refreshes are now serialized behind an
195
+ ``asyncio.Lock`` with a post-acquire re-check: callers that lose the
196
+ race receive the already-refreshed tokens, callers passing a stale
197
+ (pre-rotation) refresh token get the fresh tokens instead of a
198
+ guaranteed failure, and an explicit refresh with the current token
199
+ still forces a refresh (deep-reconnect behavior preserved).
200
+ - **Preserve refresh_token/id_token across refreshes**: the refresh
201
+ response merge preserved AWS credential fields but not
202
+ ``refresh_token``/``id_token``. A refresh response omitting them wiped
203
+ the stored refresh token, so the next refresh posted an empty string
204
+ and failed unconditionally.
205
+ - **Fix aiohttp session leak when authentication fails in**
206
+ ``__aenter__``: Python never calls ``__aexit__`` when ``__aenter__``
207
+ raises, so bad credentials or a network error leaked the owned
208
+ ``ClientSession`` (one per retry attempt). The session is now closed
209
+ before the exception propagates.
210
+ - **Make** ``NavienAuthClient.__aenter__`` **idempotent**:
211
+ ``create_navien_clients()`` pre-enters the context and its docstring
212
+ instructs users to enter again with ``async with auth:``; the second
213
+ entry created a fresh session and orphaned the first — which the API
214
+ client was still pinned to. Re-entering now reuses the existing
215
+ session.
216
+ - **Fall back to full sign-in when stored-token refresh fails**:
217
+ restoring expired stored tokens raised ``TokenRefreshError`` even
218
+ though credentials for a full ``sign_in()`` were available.
219
+ - **Stop pinning the auth session in the API client**:
220
+ ``NavienAPIClient`` captured ``auth_client.session`` at construction
221
+ and kept using it after the auth client recreated its session
222
+ (``RuntimeError: Session is closed``). The session is now resolved per
223
+ request; an explicitly provided session still takes precedence.
224
+ - **Add HTTP timeouts to unguarded sessions**: the standalone
225
+ ``refresh_access_token()`` helper and ``OpenEIClient`` created
226
+ ``ClientSession``s without a ``ClientTimeout``; requests could hang
227
+ indefinitely. Both now use a 30-second total timeout.
228
+ - **Fix reconnection loop dying on authentication errors**: the backoff
229
+ loop caught only ``AwsCrtError`` and ``RuntimeError``, so
230
+ ``TokenRefreshError``, ``AuthenticationError``, and
231
+ ``MqttCredentialsError`` raised during quick/deep reconnection escaped
232
+ and silently killed the reconnect task. A routine outage coinciding
233
+ with token expiry left the client permanently offline despite unlimited
234
+ retries. All library errors (``Nwp500Error``) and operation timeouts
235
+ are now treated as failed attempts and retried; only
236
+ ``InvalidCredentialsError`` is fatal and stops the loop with a
237
+ ``reconnection_failed`` event.
238
+ - **Fix disconnect() being a no-op while the connection is interrupted**:
239
+ calling ``disconnect()`` during an interruption returned early without
240
+ disabling automatic reconnection or stopping periodic tasks, so the
241
+ backoff loop would resurrect the connection after the application shut
242
+ the client down. ``disconnect()`` now always disables reconnection and
243
+ stops periodic tasks, and tears down the SDK connection even when not
244
+ connected.
245
+ - **Fix queued commands being lost after active/deep reconnection**: the
246
+ command queue was only flushed from the SDK's ``on_connection_resumed``
247
+ callback, which never fires for the new connection built by
248
+ active/deep reconnection. Commands queued while offline were silently
249
+ dropped. Both reconnect paths now flush the queue after subscriptions
250
+ are restored.
251
+ - **Fix periodic request tasks dying on MQTT errors**: the periodic loop
252
+ caught only ``AwsCrtError`` and ``RuntimeError``;
253
+ ``MqttNotConnectedError``/``MqttPublishError`` raised by a publish
254
+ racing a disconnection permanently killed the polling task while it
255
+ still appeared active. The loop now survives all library errors.
256
+ - **Fix silent failures in thread-scheduled coroutines**: futures
257
+ returned by ``run_coroutine_threadsafe`` were discarded, so exceptions
258
+ from scheduled work (e.g. a failed resubscribe after a clean-session
259
+ resume, leaving the client connected but deaf) vanished. A done
260
+ callback now logs them.
261
+ - **Fix CancelledError being swallowed in reconnection and periodic
262
+ loops**: both loops caught ``asyncio.CancelledError`` and ``break``-ed,
263
+ so cancelled tasks ended "successfully" (and the reconnection loop
264
+ could emit ``reconnection_failed`` during a manual disconnect).
265
+ Cancellation now propagates correctly.
266
+ - **Fix AttributeError in configure_reservation_water_program**: The
267
+ ``NavienMqttClient`` proxy referenced ``self._control``, which is never
268
+ assigned (the attribute is ``_device_controller``), so every call raised
269
+ ``AttributeError``. Now delegates correctly; a regression test guards all
270
+ proxies against references to the undefined attribute.
271
+ - **Fix broken CLI mode choices**: ``nwp-cli mode vacation`` always failed
272
+ because vacation mode (5) requires a day count that was never supplied, and
273
+ ``mode standby`` sent the invalid writable mode value ``0``. Both choices
274
+ were removed from the ``mode`` command; use the dedicated ``vacation DAYS``
275
+ and ``power off`` commands instead.
276
+ - **Fix CLI exit codes**: click ignores command return values in standalone
277
+ mode, so the CLI always exited ``0`` even when a command failed. Failures
278
+ now propagate through ``ctx.exit()`` and produce a non-zero exit code for
279
+ scripts and automation.
280
+ - **Fix cached tokens being reused for a different account**: passing
281
+ ``--email`` for account B while tokens for account A were cached silently
282
+ ran commands against account A's session. Cached tokens are now discarded
283
+ when the provided email does not match the cached one.
284
+ - **Surface OpenEI application errors**: OpenEI reports errors such as an
285
+ invalid API key in the body of an HTTP 200 response; these were masked as
286
+ "no rate plans found". ``fetch_rates()`` now raises ``APIError`` with the
287
+ API's error message.
288
+ - **Fix broken example import**: ``examples/advanced/mqtt_diagnostics.py``
289
+ imported ``MqttConnectionConfig`` from the nonexistent ``nwp500.mqtt_utils``
290
+ module and used the deprecated ``datetime.utcnow()``.
291
+
292
+ Improvements
293
+ ------------
294
+ - **MQTT operation acknowledgement timeouts**: connect, publish,
295
+ subscribe, unsubscribe, and disconnect acknowledgements are now awaited
296
+ with a timeout (``MqttConnectionConfig.operation_timeout``, default 30
297
+ seconds). Previously a half-open TCP connection could hang callers
298
+ until the 20-minute keep-alive expired.
299
+ - **Reconnection backoff jitter**: reconnect delays are now randomized
300
+ (±50%, capped at ``max_reconnect_delay``) so fleets of clients
301
+ disconnected simultaneously (e.g. the AWS IoT 24-hour disconnect) no
302
+ longer reconnect in synchronized waves.
303
+
304
+ Security
305
+ --------
306
+ - **Restrict token cache file permissions**: ``~/.nwp500_tokens.json``
307
+ (refresh token and AWS credentials) was written world-readable. It is now
308
+ created with mode ``0600``, and existing files are tightened on save.
309
+
8
310
  Version 8.1.3 (2026-06-15)
9
311
  ==========================
10
312
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nwp500-python
3
- Version: 8.1.3
3
+ Version: 9.0.0
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
@@ -418,7 +418,7 @@ Encodes a floating-point price into an integer for transmission.
418
418
 
419
419
  .. code-block:: python
420
420
 
421
- from nwp500 import encode_price
421
+ from nwp500.encoding import encode_price
422
422
 
423
423
  # Encode $0.45000 per kWh
424
424
  encoded = encode_price(0.45, decimal_point=5)
@@ -437,7 +437,7 @@ Decodes an integer price back to floating-point.
437
437
 
438
438
  .. code-block:: python
439
439
 
440
- from nwp500 import decode_price
440
+ from nwp500.encoding import decode_price
441
441
 
442
442
  # Decode price from device
443
443
  price = decode_price(45000, decimal_point=5)
@@ -466,7 +466,7 @@ Encodes a list of day names into a bitfield.
466
466
 
467
467
  .. code-block:: python
468
468
 
469
- from nwp500 import encode_week_bitfield
469
+ from nwp500.encoding import encode_week_bitfield
470
470
 
471
471
  # Weekdays only
472
472
  bitfield = encode_week_bitfield([
@@ -487,7 +487,7 @@ Decodes a bitfield back into a list of day names.
487
487
 
488
488
  .. code-block:: python
489
489
 
490
- from nwp500 import decode_week_bitfield
490
+ from nwp500.encoding import decode_week_bitfield
491
491
 
492
492
  # Decode weekday bitfield
493
493
  days = decode_week_bitfield(62)
@@ -504,7 +504,8 @@ Configure two rate periods - off-peak and peak pricing:
504
504
  .. code-block:: python
505
505
 
506
506
  import asyncio
507
- from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient, build_tou_period
507
+ from nwp500 import NavienAPIClient, NavienAuthClient, NavienMqttClient
508
+ from nwp500.encoding import build_tou_period
508
509
 
509
510
  async def configure_simple_tou():
510
511
  async with NavienAuthClient("user@example.com", "password") as auth_client:
@@ -654,7 +655,7 @@ Query the device for its current TOU configuration:
654
655
 
655
656
  .. code-block:: python
656
657
 
657
- from nwp500 import decode_week_bitfield, decode_price
658
+ from nwp500.encoding import decode_week_bitfield, decode_price
658
659
 
659
660
  async def check_tou_settings():
660
661
  async with NavienAuthClient("user@example.com", "password") as auth_client:
@@ -64,8 +64,8 @@ Quick Example
64
64
  NavienAuthClient,
65
65
  NavienAPIClient,
66
66
  NavienMqttClient,
67
- build_reservation_entry,
68
67
  )
68
+ from nwp500.encoding import build_reservation_entry
69
69
 
70
70
  async def main():
71
71
  async with NavienAuthClient(
@@ -279,7 +279,7 @@ Helper Functions
279
279
 
280
280
  .. code-block:: python
281
281
 
282
- from nwp500 import build_reservation_entry
282
+ from nwp500.encoding import build_reservation_entry
283
283
 
284
284
  entry = build_reservation_entry(
285
285
  enabled=True,
@@ -589,8 +589,8 @@ want to send the whole weekly program as one typed object.
589
589
  from nwp500 import (
590
590
  WeeklyReservationEntry,
591
591
  WeeklyReservationSchedule,
592
- build_reservation_entry,
593
592
  )
593
+ from nwp500.encoding import build_reservation_entry
594
594
 
595
595
  morning = WeeklyReservationEntry.model_validate(
596
596
  build_reservation_entry(
@@ -529,7 +529,6 @@ Error Handling
529
529
 
530
530
  from nwp500 import (
531
531
  InvalidCredentialsError,
532
- TokenExpiredError,
533
532
  TokenRefreshError,
534
533
  AuthenticationError
535
534
  )
@@ -543,9 +542,6 @@ Error Handling
543
542
  except InvalidCredentialsError:
544
543
  print("Wrong email or password")
545
544
 
546
- except TokenExpiredError:
547
- print("Token expired and refresh failed")
548
-
549
545
  except TokenRefreshError:
550
546
  print("Could not refresh token - sign in again")
551
547
 
@@ -224,12 +224,6 @@ Set operation mode.
224
224
  # High Demand (maximum capacity)
225
225
  python3 -m nwp500.cli mode high-demand
226
226
 
227
- # Vacation Mode
228
- python3 -m nwp500.cli mode vacation
229
-
230
- # Standby
231
- python3 -m nwp500.cli mode standby
232
-
233
227
  **Syntax:**
234
228
 
235
229
  .. code-block:: bash
@@ -238,12 +232,13 @@ Set operation mode.
238
232
 
239
233
  **Available Modes:**
240
234
 
241
- * ``standby`` - Device off but ready
242
- * ``heat-pump`` - Heat pump only (0)
235
+ * ``heat-pump`` - Heat pump only (1)
243
236
  * ``electric`` - Electric heating only (2)
244
237
  * ``energy-saver`` - Hybrid/balanced mode (3) **recommended**
245
238
  * ``high-demand`` - Maximum heating capacity (4)
246
- * ``vacation`` - Extended vacancy mode (5)
239
+
240
+ For vacation mode use the ``vacation`` command (it requires a day count);
241
+ to power the unit off use the ``power`` command.
247
242
 
248
243
  **Output:** Confirmation message and updated device status.
249
244
 
@@ -15,22 +15,17 @@ All library exceptions inherit from ``Nwp500Error``::
15
15
  Nwp500Error (base)
16
16
  ├── AuthenticationError
17
17
  │ ├── InvalidCredentialsError
18
- │ ├── TokenExpiredError
19
18
  │ └── TokenRefreshError
20
19
  ├── APIError
21
20
  ├── MqttError
22
21
  │ ├── MqttConnectionError
23
22
  │ ├── MqttNotConnectedError
24
23
  │ ├── MqttPublishError
25
- │ ├── MqttSubscriptionError
26
24
  │ └── MqttCredentialsError
27
25
  ├── ValidationError
28
26
  │ ├── ParameterValidationError
29
27
  │ └── RangeValidationError
30
28
  └── DeviceError
31
- ├── DeviceNotFoundError
32
- ├── DeviceOfflineError
33
- ├── DeviceOperationError
34
29
  └── DeviceCapabilityError
35
30
 
36
31
  Base Exception
@@ -134,16 +129,6 @@ InvalidCredentialsError
134
129
  print("Please check your email and password")
135
130
  # Prompt user to re-enter credentials
136
131
 
137
- TokenExpiredError
138
- -----------------
139
-
140
- .. py:class:: TokenExpiredError
141
-
142
- Raised when an authentication token has expired.
143
-
144
- Subclass of :py:class:`AuthenticationError`. Tokens have a limited lifetime
145
- and must be refreshed periodically.
146
-
147
132
  TokenRefreshError
148
133
  -----------------
149
134
 
@@ -305,16 +290,6 @@ MqttPublishError
305
290
  else:
306
291
  raise # Not retriable or max retries reached
307
292
 
308
- MqttSubscriptionError
309
- ---------------------
310
-
311
- .. py:class:: MqttSubscriptionError
312
-
313
- Failed to subscribe to MQTT topic.
314
-
315
- Raised when subscription to an MQTT topic fails. This may occur if the
316
- connection is interrupted or if the client lacks permissions for the topic.
317
-
318
293
  MqttCredentialsError
319
294
  --------------------
320
295
 
@@ -409,38 +384,6 @@ DeviceError
409
384
 
410
385
  All device-related errors inherit from this base class.
411
386
 
412
- DeviceNotFoundError
413
- -------------------
414
-
415
- .. py:class:: DeviceNotFoundError
416
-
417
- Requested device not found.
418
-
419
- Raised when a device cannot be found in the user's device list or when
420
- attempting to access a non-existent device.
421
-
422
- DeviceOfflineError
423
- ------------------
424
-
425
- .. py:class:: DeviceOfflineError
426
-
427
- Device is offline or unreachable.
428
-
429
- Raised when a device is offline and cannot respond to commands or status
430
- requests. The device may be powered off, disconnected from the network,
431
- or experiencing connectivity issues.
432
-
433
- DeviceOperationError
434
- --------------------
435
-
436
- .. py:class:: DeviceOperationError
437
-
438
- Device operation failed.
439
-
440
- Raised when a device operation (mode change, temperature setting, etc.)
441
- fails. This may occur due to invalid commands, device restrictions, or
442
- device-side errors.
443
-
444
387
  DeviceCapabilityError
445
388
  ---------------------
446
389
 
@@ -467,7 +467,7 @@ update_reservations()
467
467
 
468
468
  .. code-block:: python
469
469
 
470
- from nwp500 import build_reservation_entry
470
+ from nwp500.encoding import build_reservation_entry
471
471
 
472
472
  reservations = [
473
473
  build_reservation_entry(
@@ -27,8 +27,7 @@ from datetime import UTC, datetime
27
27
  from pathlib import Path
28
28
 
29
29
  from nwp500 import NavienAuthClient, NavienMqttClient
30
- from nwp500.mqtt import MqttDiagnosticsCollector
31
- from nwp500.mqtt_utils import MqttConnectionConfig
30
+ from nwp500.mqtt import MqttConnectionConfig, MqttDiagnosticsCollector
32
31
 
33
32
  # Configure logging to show detailed MQTT information
34
33
  logging.basicConfig(
@@ -85,7 +84,7 @@ class MqttDiagnosticsExample:
85
84
  output_file = self.output_dir / f"diagnostics_{timestamp}.json"
86
85
 
87
86
  json_data = self.diagnostics.export_json()
88
- with open(output_file, "w") as f:
87
+ with output_file.open("w") as f:
89
88
  f.write(json_data)
90
89
 
91
90
  _logger.info(f"Exported diagnostics to {output_file}")
@@ -275,9 +274,9 @@ class MqttDiagnosticsExample:
275
274
 
276
275
  # Final export
277
276
  _logger.info("Exporting final diagnostics...")
278
- timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
277
+ timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
279
278
  final_file = self.output_dir / f"diagnostics_final_{timestamp}.json"
280
- with open(final_file, "w") as f:
279
+ with final_file.open("w") as f:
281
280
  f.write(self.diagnostics.export_json())
282
281
  _logger.info(f"Final diagnostics saved to {final_file}")
283
282
 
@@ -21,6 +21,8 @@ from nwp500 import (
21
21
  NavienAuthClient,
22
22
  NavienMqttClient,
23
23
  OpenEIClient,
24
+ )
25
+ from nwp500.encoding import (
24
26
  decode_price,
25
27
  decode_week_bitfield,
26
28
  )
@@ -4,8 +4,6 @@ Place this file in the examples/ directory. Example scripts will try to import
4
4
  these helpers; if that import fails we leave a small fallback in each script.
5
5
  """
6
6
 
7
- from __future__ import annotations
8
-
9
7
  import re
10
8
 
11
9
 
@@ -57,11 +57,30 @@ select = [
57
57
  "C4", # flake8-comprehensions
58
58
  "SIM", # flake8-simplify
59
59
  "S", # flake8-bandit (security)
60
+ "RUF", # Ruff-specific rules (incl. unused noqa)
61
+ "DTZ", # flake8-datetimez (naive datetime usage)
62
+ "PTH", # flake8-use-pathlib
63
+ "ASYNC", # flake8-async
64
+ "PERF", # perflint
60
65
  ]
61
66
 
62
67
  ignore = [
63
68
  "E203", # whitespace before ':' (conflicts with black/ruff format)
64
69
  "B904", # raise from - will be addressed in a future update
70
+ # Async functions taking a `timeout:` parameter are a deliberate,
71
+ # documented public API of this library (e.g. wait_for, command
72
+ # helpers); switching callers to asyncio.timeout() would be a
73
+ # breaking change with no behavioral benefit.
74
+ "ASYNC109",
75
+ # Degree signs, en-dashes, and multiplication signs in docstrings
76
+ # and user-facing strings are intentional (temperature units,
77
+ # ranges), not typos of ASCII look-alikes.
78
+ "RUF001",
79
+ "RUF002",
80
+ "RUF003",
81
+ # __all__ lists are deliberately grouped by category with comments,
82
+ # not sorted alphabetically.
83
+ "RUF022",
65
84
  ]
66
85
 
67
86
  # Allow autofix for all enabled rules (when `--fix` is provided)