PyPlumIO 0.5.28__tar.gz → 0.5.30__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 (143) hide show
  1. {pyplumio-0.5.28 → pyplumio-0.5.30}/.pre-commit-config.yaml +2 -2
  2. {pyplumio-0.5.28 → pyplumio-0.5.30}/PKG-INFO +10 -10
  3. {pyplumio-0.5.28 → pyplumio-0.5.30}/PyPlumIO.egg-info/PKG-INFO +10 -10
  4. {pyplumio-0.5.28 → pyplumio-0.5.30}/PyPlumIO.egg-info/requires.txt +9 -9
  5. {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/connecting.rst +9 -5
  6. {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/mixers_thermostats.rst +2 -2
  7. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/_version.py +2 -2
  8. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/devices/__init__.py +42 -5
  9. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/devices/ecomax.py +2 -31
  10. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/devices/mixer.py +1 -1
  11. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/devices/thermostat.py +1 -1
  12. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/filters.py +10 -10
  13. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/frames/__init__.py +13 -6
  14. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/data_types.py +7 -7
  15. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/parameter.py +38 -2
  16. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/schedule.py +1 -1
  17. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/stream.py +33 -31
  18. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/ecomax_parameters.py +37 -26
  19. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/mixer_parameters.py +20 -10
  20. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/regulator_data.py +1 -1
  21. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/schedules.py +13 -2
  22. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/thermostat_parameters.py +24 -6
  23. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyproject.toml +10 -9
  24. pyplumio-0.5.30/requirements_docs.txt +3 -0
  25. {pyplumio-0.5.28 → pyplumio-0.5.30}/requirements_test.txt +7 -7
  26. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/frames/test_messages.py +3 -3
  27. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/frames/test_responses.py +3 -3
  28. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_parameter.py +18 -4
  29. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_devices.py +49 -8
  30. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_stream.py +48 -58
  31. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/ecomax_parameters.json +2 -2
  32. pyplumio-0.5.28/requirements_docs.txt +0 -3
  33. {pyplumio-0.5.28 → pyplumio-0.5.30}/.gitattributes +0 -0
  34. {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/CODE_OF_CONDUCT.md +0 -0
  35. {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  36. {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  37. {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/dependabot.yml +0 -0
  38. {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/workflows/ci.yml +0 -0
  39. {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/workflows/codeql-analysis.yml +0 -0
  40. {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/workflows/deploy.yml +0 -0
  41. {pyplumio-0.5.28 → pyplumio-0.5.30}/.github/workflows/documentation.yml +0 -0
  42. {pyplumio-0.5.28 → pyplumio-0.5.30}/.gitignore +0 -0
  43. {pyplumio-0.5.28 → pyplumio-0.5.30}/.vscode/settings.json +0 -0
  44. {pyplumio-0.5.28 → pyplumio-0.5.30}/LICENSE +0 -0
  45. {pyplumio-0.5.28 → pyplumio-0.5.30}/MANIFEST.in +0 -0
  46. {pyplumio-0.5.28 → pyplumio-0.5.30}/PyPlumIO.egg-info/SOURCES.txt +0 -0
  47. {pyplumio-0.5.28 → pyplumio-0.5.30}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  48. {pyplumio-0.5.28 → pyplumio-0.5.30}/PyPlumIO.egg-info/top_level.txt +0 -0
  49. {pyplumio-0.5.28 → pyplumio-0.5.30}/README.md +0 -0
  50. {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/Makefile +0 -0
  51. {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/make.bat +0 -0
  52. {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/callbacks.rst +0 -0
  53. {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/conf.py +0 -0
  54. {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/frames.rst +0 -0
  55. {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/index.rst +0 -0
  56. {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/protocol.rst +0 -0
  57. {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/reading.rst +0 -0
  58. {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/schedules.rst +0 -0
  59. {pyplumio-0.5.28 → pyplumio-0.5.30}/docs/source/writing.rst +0 -0
  60. {pyplumio-0.5.28 → pyplumio-0.5.30}/images/ecomax.png +0 -0
  61. {pyplumio-0.5.28 → pyplumio-0.5.30}/images/rs485.png +0 -0
  62. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/__init__.py +0 -0
  63. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/__main__.py +0 -0
  64. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/connection.py +0 -0
  65. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/const.py +0 -0
  66. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/devices/ecoster.py +0 -0
  67. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/exceptions.py +0 -0
  68. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/frames/messages.py +0 -0
  69. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/frames/requests.py +0 -0
  70. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/frames/responses.py +0 -0
  71. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/__init__.py +0 -0
  72. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/event_manager.py +0 -0
  73. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/factory.py +0 -0
  74. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/task_manager.py +0 -0
  75. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/timeout.py +0 -0
  76. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/helpers/uid.py +0 -0
  77. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/protocol.py +0 -0
  78. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/py.typed +0 -0
  79. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/__init__.py +0 -0
  80. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/alerts.py +0 -0
  81. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/boiler_load.py +0 -0
  82. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/boiler_power.py +0 -0
  83. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/fan_power.py +0 -0
  84. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/frame_versions.py +0 -0
  85. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/fuel_consumption.py +0 -0
  86. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/fuel_level.py +0 -0
  87. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/lambda_sensor.py +0 -0
  88. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/mixer_sensors.py +0 -0
  89. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/modules.py +0 -0
  90. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/network_info.py +0 -0
  91. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/output_flags.py +0 -0
  92. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/outputs.py +0 -0
  93. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/pending_alerts.py +0 -0
  94. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/product_info.py +0 -0
  95. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/program_version.py +0 -0
  96. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/regulator_data_schema.py +0 -0
  97. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/statuses.py +0 -0
  98. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/temperatures.py +0 -0
  99. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/structures/thermostat_sensors.py +0 -0
  100. {pyplumio-0.5.28 → pyplumio-0.5.30}/pyplumio/utils.py +0 -0
  101. {pyplumio-0.5.28 → pyplumio-0.5.30}/requirements.txt +0 -0
  102. {pyplumio-0.5.28 → pyplumio-0.5.30}/setup.cfg +0 -0
  103. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/__init__.py +0 -0
  104. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/conftest.py +0 -0
  105. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/frames/test_init.py +0 -0
  106. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/frames/test_requests.py +0 -0
  107. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/__init__.py +0 -0
  108. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_data_types.py +0 -0
  109. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_event_manager.py +0 -0
  110. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_factory.py +0 -0
  111. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_schedule.py +0 -0
  112. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_task_manager.py +0 -0
  113. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_timeout.py +0 -0
  114. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/helpers/test_uid.py +0 -0
  115. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/ruff.toml +0 -0
  116. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_connection.py +0 -0
  117. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_filters.py +0 -0
  118. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_init.py +0 -0
  119. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_main.py +0 -0
  120. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_protocol.py +0 -0
  121. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/test_utils.py +0 -0
  122. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/messages/regulator_data.json +0 -0
  123. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/messages/sensor_data.json +0 -0
  124. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/alerts.json +0 -0
  125. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/ecomax_control.json +0 -0
  126. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/ecomax_parameters.json +0 -0
  127. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/mixer_parameters.json +0 -0
  128. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  129. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  130. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/set_schedule.json +0 -0
  131. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  132. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/requests/thermostat_parameters.json +0 -0
  133. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/alerts.json +0 -0
  134. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/device_available.json +0 -0
  135. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/mixer_parameters.json +0 -0
  136. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/password.json +0 -0
  137. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/program_version.json +0 -0
  138. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/regulator_data_schema.json +0 -0
  139. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/schedules.json +0 -0
  140. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/thermostat_parameters.json +0 -0
  141. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/responses/uid.json +0 -0
  142. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  143. {pyplumio-0.5.28 → pyplumio-0.5.30}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -2,7 +2,7 @@
2
2
  # See https://pre-commit.com/hooks.html for more hooks
3
3
  repos:
4
4
  - repo: https://github.com/astral-sh/ruff-pre-commit
5
- rev: v0.6.3
5
+ rev: v0.8.1
6
6
  hooks:
7
7
  - id: ruff
8
8
  args:
@@ -12,6 +12,6 @@ repos:
12
12
  hooks:
13
13
  - id: codespell
14
14
  - repo: https://github.com/pre-commit/mirrors-mypy
15
- rev: v1.11.2
15
+ rev: v1.13.0
16
16
  hooks:
17
17
  - id: mypy
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyPlumIO
3
- Version: 0.5.28
3
+ Version: 0.5.30
4
4
  Summary: PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
5
5
  Author-email: Denis Paavilainen <denpa@denpa.pro>
6
6
  License: MIT License
@@ -27,22 +27,22 @@ Requires-Dist: pyserial-asyncio==0.6
27
27
  Requires-Dist: typing-extensions==4.12.2
28
28
  Provides-Extra: test
29
29
  Requires-Dist: codespell==2.3.0; extra == "test"
30
- Requires-Dist: coverage==7.6.1; extra == "test"
31
- Requires-Dist: mypy==1.11.2; extra == "test"
30
+ Requires-Dist: coverage==7.6.8; extra == "test"
31
+ Requires-Dist: mypy==1.13.0; extra == "test"
32
32
  Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
33
- Requires-Dist: pytest==8.3.2; extra == "test"
33
+ Requires-Dist: pytest==8.3.4; extra == "test"
34
34
  Requires-Dist: pytest-asyncio==0.24.0; extra == "test"
35
- Requires-Dist: ruff==0.6.3; extra == "test"
36
- Requires-Dist: tox==4.18.0; extra == "test"
35
+ Requires-Dist: ruff==0.8.1; extra == "test"
36
+ Requires-Dist: tox==4.23.2; extra == "test"
37
37
  Requires-Dist: types-pyserial==3.5.0.20240826; extra == "test"
38
38
  Provides-Extra: docs
39
- Requires-Dist: sphinx==7.4.7; extra == "docs"
40
- Requires-Dist: sphinx_rtd_theme==2.0.0; extra == "docs"
39
+ Requires-Dist: sphinx==8.1.3; extra == "docs"
40
+ Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
41
41
  Requires-Dist: readthedocs-sphinx-search==0.3.2; extra == "docs"
42
42
  Provides-Extra: dev
43
43
  Requires-Dist: pyplumio[docs,test]; extra == "dev"
44
- Requires-Dist: pre-commit==3.8.0; extra == "dev"
45
- Requires-Dist: tomli==2.0.1; extra == "dev"
44
+ Requires-Dist: pre-commit==4.0.1; extra == "dev"
45
+ Requires-Dist: tomli==2.2.1; extra == "dev"
46
46
 
47
47
  # PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
48
48
  [![PyPI version](https://badge.fury.io/py/PyPlumIO.svg)](https://badge.fury.io/py/PyPlumIO)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyPlumIO
3
- Version: 0.5.28
3
+ Version: 0.5.30
4
4
  Summary: PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
5
5
  Author-email: Denis Paavilainen <denpa@denpa.pro>
6
6
  License: MIT License
@@ -27,22 +27,22 @@ Requires-Dist: pyserial-asyncio==0.6
27
27
  Requires-Dist: typing-extensions==4.12.2
28
28
  Provides-Extra: test
29
29
  Requires-Dist: codespell==2.3.0; extra == "test"
30
- Requires-Dist: coverage==7.6.1; extra == "test"
31
- Requires-Dist: mypy==1.11.2; extra == "test"
30
+ Requires-Dist: coverage==7.6.8; extra == "test"
31
+ Requires-Dist: mypy==1.13.0; extra == "test"
32
32
  Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
33
- Requires-Dist: pytest==8.3.2; extra == "test"
33
+ Requires-Dist: pytest==8.3.4; extra == "test"
34
34
  Requires-Dist: pytest-asyncio==0.24.0; extra == "test"
35
- Requires-Dist: ruff==0.6.3; extra == "test"
36
- Requires-Dist: tox==4.18.0; extra == "test"
35
+ Requires-Dist: ruff==0.8.1; extra == "test"
36
+ Requires-Dist: tox==4.23.2; extra == "test"
37
37
  Requires-Dist: types-pyserial==3.5.0.20240826; extra == "test"
38
38
  Provides-Extra: docs
39
- Requires-Dist: sphinx==7.4.7; extra == "docs"
40
- Requires-Dist: sphinx_rtd_theme==2.0.0; extra == "docs"
39
+ Requires-Dist: sphinx==8.1.3; extra == "docs"
40
+ Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
41
41
  Requires-Dist: readthedocs-sphinx-search==0.3.2; extra == "docs"
42
42
  Provides-Extra: dev
43
43
  Requires-Dist: pyplumio[docs,test]; extra == "dev"
44
- Requires-Dist: pre-commit==3.8.0; extra == "dev"
45
- Requires-Dist: tomli==2.0.1; extra == "dev"
44
+ Requires-Dist: pre-commit==4.0.1; extra == "dev"
45
+ Requires-Dist: tomli==2.2.1; extra == "dev"
46
46
 
47
47
  # PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
48
48
  [![PyPI version](https://badge.fury.io/py/PyPlumIO.svg)](https://badge.fury.io/py/PyPlumIO)
@@ -4,21 +4,21 @@ typing-extensions==4.12.2
4
4
 
5
5
  [dev]
6
6
  pyplumio[docs,test]
7
- pre-commit==3.8.0
8
- tomli==2.0.1
7
+ pre-commit==4.0.1
8
+ tomli==2.2.1
9
9
 
10
10
  [docs]
11
- sphinx==7.4.7
12
- sphinx_rtd_theme==2.0.0
11
+ sphinx==8.1.3
12
+ sphinx_rtd_theme==3.0.2
13
13
  readthedocs-sphinx-search==0.3.2
14
14
 
15
15
  [test]
16
16
  codespell==2.3.0
17
- coverage==7.6.1
18
- mypy==1.11.2
17
+ coverage==7.6.8
18
+ mypy==1.13.0
19
19
  pyserial-asyncio-fast==0.14
20
- pytest==8.3.2
20
+ pytest==8.3.4
21
21
  pytest-asyncio==0.24.0
22
- ruff==0.6.3
23
- tox==4.18.0
22
+ ruff==0.8.1
23
+ tox==4.23.2
24
24
  types-pyserial==3.5.0.20240826
@@ -76,11 +76,15 @@ working with device classes and queues.
76
76
  )
77
77
 
78
78
  while connection.connected:
79
- if isinstance(
80
- (frame := await connection.reader.read()), responses.AlertsResponse
81
- ):
82
- print(frame.data)
83
- break
79
+ try:
80
+ if isinstance(
81
+ (frame := await connection.reader.read()), responses.AlertsResponse
82
+ ):
83
+ print(frame.data)
84
+ break
85
+ except pyplumio.ProtocolError:
86
+ # Skip protocol errors and read the next frame.
87
+ pass
84
88
 
85
89
 
86
90
  asyncio.run(main())
@@ -49,8 +49,8 @@ get it's current_temp property and set it's target temperature to
49
49
  # Set mixer target temperature to 50 degrees Celsius.
50
50
  await mixer.set("mixer_target_temp", 50)
51
51
 
52
- Thermosat Examples
53
- ------------------
52
+ Thermostat Examples
53
+ -------------------
54
54
 
55
55
  In the following example, we'll get single thermostat by it's index,
56
56
  get current room temperature and set daytime target temperature to 20
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.5.28'
16
- __version_tuple__ = version_tuple = (0, 5, 28)
15
+ __version__ = version = '0.5.30'
16
+ __version_tuple__ = version_tuple = (0, 5, 30)
@@ -5,17 +5,21 @@ from __future__ import annotations
5
5
  from abc import ABC
6
6
  import asyncio
7
7
  from functools import cache
8
+ import logging
8
9
  from typing import Any, ClassVar
9
10
 
10
11
  from pyplumio.const import ATTR_FRAME_ERRORS, ATTR_LOADED, DeviceType, FrameType
11
12
  from pyplumio.exceptions import UnknownDeviceError
12
- from pyplumio.frames import DataFrameDescription, Frame, Request
13
+ from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
13
14
  from pyplumio.helpers.event_manager import EventManager
14
15
  from pyplumio.helpers.factory import create_instance
15
16
  from pyplumio.helpers.parameter import Parameter, ParameterValue
17
+ from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
16
18
  from pyplumio.structures.network_info import NetworkInfo
17
19
  from pyplumio.utils import to_camelcase
18
20
 
21
+ _LOGGER = logging.getLogger(__name__)
22
+
19
23
 
20
24
  @cache
21
25
  def is_known_device_type(device_type: int) -> bool:
@@ -45,7 +49,7 @@ class Device(ABC, EventManager):
45
49
 
46
50
  queue: asyncio.Queue[Frame]
47
51
 
48
- def __init__(self, queue: asyncio.Queue[Frame]):
52
+ def __init__(self, queue: asyncio.Queue[Frame]) -> None:
49
53
  """Initialize a new device."""
50
54
  super().__init__()
51
55
  self.queue = queue
@@ -125,15 +129,48 @@ class PhysicalDevice(Device, ABC):
125
129
  address: ClassVar[int]
126
130
  _network: NetworkInfo
127
131
  _setup_frames: tuple[DataFrameDescription, ...]
132
+ _frame_versions: dict[int, int]
128
133
 
129
- def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo):
134
+ def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
130
135
  """Initialize a new physical device."""
131
136
  super().__init__(queue)
132
137
  self._network = network
138
+ self._frame_versions = {}
139
+
140
+ async def update_frame_versions(versions: dict[int, int]) -> None:
141
+ """Check frame versions and update outdated frames."""
142
+ for frame_type, version in versions.items():
143
+ if (
144
+ is_known_frame_type(frame_type)
145
+ and self.supports_frame_type(frame_type)
146
+ and not self.has_frame_version(frame_type, version)
147
+ ):
148
+ _LOGGER.debug(
149
+ "Updating frame %s to version %i", repr(frame_type), version
150
+ )
151
+ request = await Request.create(frame_type, recipient=self.address)
152
+ self.queue.put_nowait(request)
153
+ self._frame_versions[frame_type] = version
154
+
155
+ self.subscribe(ATTR_FRAME_VERSIONS, update_frame_versions)
156
+
157
+ def has_frame_version(self, frame_type: int, version: int | None = None) -> bool:
158
+ """Return True if frame data is up to date, False otherwise."""
159
+ if frame_type not in self._frame_versions:
160
+ return False
161
+
162
+ if version is None or self._frame_versions[frame_type] == version:
163
+ return True
164
+
165
+ return False
166
+
167
+ def supports_frame_type(self, frame_type: int) -> bool:
168
+ """Check if frame type is supported by the device."""
169
+ return frame_type not in self.data.get(ATTR_FRAME_ERRORS, [])
133
170
 
134
171
  def handle_frame(self, frame: Frame) -> None:
135
172
  """Handle frame received from the device."""
136
- frame.sender_device = self
173
+ frame.assign_to(self)
137
174
  if frame.data is not None:
138
175
  for name, value in frame.data.items():
139
176
  self.dispatch_nowait(name, value)
@@ -188,7 +225,7 @@ class VirtualDevice(Device, ABC):
188
225
 
189
226
  def __init__(
190
227
  self, queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
191
- ):
228
+ ) -> None:
192
229
  """Initialize a new sub-device."""
193
230
  super().__init__(queue)
194
231
  self.parent = parent
@@ -9,7 +9,6 @@ import time
9
9
  from typing import Any, Final
10
10
 
11
11
  from pyplumio.const import (
12
- ATTR_FRAME_ERRORS,
13
12
  ATTR_PASSWORD,
14
13
  ATTR_SENSORS,
15
14
  ATTR_STATE,
@@ -21,7 +20,7 @@ from pyplumio.devices import PhysicalDevice
21
20
  from pyplumio.devices.mixer import Mixer
22
21
  from pyplumio.devices.thermostat import Thermostat
23
22
  from pyplumio.filters import on_change
24
- from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
23
+ from pyplumio.frames import DataFrameDescription, Frame, Request
25
24
  from pyplumio.helpers.parameter import ParameterValues
26
25
  from pyplumio.helpers.schedule import Schedule, ScheduleDay
27
26
  from pyplumio.structures.alerts import ATTR_TOTAL_ALERTS
@@ -35,7 +34,6 @@ from pyplumio.structures.ecomax_parameters import (
35
34
  EcomaxSwitch,
36
35
  EcomaxSwitchDescription,
37
36
  )
38
- from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
39
37
  from pyplumio.structures.fuel_consumption import ATTR_FUEL_CONSUMPTION
40
38
  from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
41
39
  from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
@@ -106,17 +104,14 @@ class EcoMAX(PhysicalDevice):
106
104
 
107
105
  address = DeviceType.ECOMAX
108
106
 
109
- _frame_versions: dict[int, int]
110
107
  _fuel_burned_timestamp_ns: int
111
108
  _setup_frames = SETUP_FRAME_TYPES
112
109
 
113
- def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo):
110
+ def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
114
111
  """Initialize a new ecoMAX controller."""
115
112
  super().__init__(queue, network)
116
- self._frame_versions = {}
117
113
  self._fuel_burned_timestamp_ns = time.perf_counter_ns()
118
114
  self.subscribe(ATTR_ECOMAX_PARAMETERS, self._handle_ecomax_parameters)
119
- self.subscribe(ATTR_FRAME_VERSIONS, self._update_frame_versions)
120
115
  self.subscribe(ATTR_FUEL_CONSUMPTION, self._add_burned_fuel_counter)
121
116
  self.subscribe(ATTR_MIXER_PARAMETERS, self._handle_mixer_parameters)
122
117
  self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
@@ -142,17 +137,6 @@ class EcoMAX(PhysicalDevice):
142
137
 
143
138
  super().handle_frame(frame)
144
139
 
145
- def _has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
146
- """Check if ecoMAX controller has this version of the frame."""
147
- return (
148
- frame_type in self._frame_versions
149
- and self._frame_versions[frame_type] == version
150
- )
151
-
152
- def _frame_is_supported(self, frame_type: FrameType | int) -> bool:
153
- """Check if frame is supported by the device."""
154
- return frame_type not in self.data.get(ATTR_FRAME_ERRORS, [])
155
-
156
140
  def _mixers(self, indexes: Iterable[int]) -> Generator[Mixer, None, None]:
157
141
  """Iterate through the mixer indexes.
158
142
 
@@ -224,19 +208,6 @@ class EcoMAX(PhysicalDevice):
224
208
  await asyncio.gather(*_ecomax_parameter_events())
225
209
  return True
226
210
 
227
- async def _update_frame_versions(self, versions: dict[int, int]) -> None:
228
- """Check frame versions and update outdated frames."""
229
- for frame_type, version in versions.items():
230
- if (
231
- is_known_frame_type(frame_type)
232
- and self._frame_is_supported(frame_type)
233
- and not self._has_frame_version(frame_type, version)
234
- ):
235
- # We don't have this frame or it's version has changed.
236
- request = await Request.create(frame_type, recipient=self.address)
237
- self.queue.put_nowait(request)
238
- self._frame_versions[frame_type] = version
239
-
240
211
  async def _add_burned_fuel_counter(self, fuel_consumption: float) -> None:
241
212
  """Calculate fuel burned since last sensor's data message."""
242
213
  current_timestamp_ns = time.perf_counter_ns()
@@ -30,7 +30,7 @@ class Mixer(VirtualDevice):
30
30
 
31
31
  def __init__(
32
32
  self, queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
33
- ):
33
+ ) -> None:
34
34
  """Initialize a new mixer."""
35
35
  super().__init__(queue, parent, index)
36
36
  self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
@@ -26,7 +26,7 @@ class Thermostat(VirtualDevice):
26
26
 
27
27
  def __init__(
28
28
  self, queue: asyncio.Queue[Frame], parent: PhysicalDevice, index: int = 0
29
- ):
29
+ ) -> None:
30
30
  """Initialize a new thermostat."""
31
31
  super().__init__(queue, parent, index)
32
32
  self.subscribe(ATTR_THERMOSTAT_SENSORS, self._handle_thermostat_sensors)
@@ -66,13 +66,12 @@ def _significantly_changed(
66
66
  def _significantly_changed(old: Comparable, new: Comparable) -> bool:
67
67
  """Check if value is significantly changed."""
68
68
  if isinstance(old, Parameter) and isinstance(new, Parameter):
69
- result = new.pending_update or old.values != new.values
70
- elif isinstance(old, SupportsFloat) and isinstance(new, SupportsFloat):
71
- result = not math.isclose(old, new, abs_tol=TOLERANCE)
72
- else:
73
- result = old != new
69
+ return new.pending_update or old.values.__ne__(new.values)
74
70
 
75
- return result
71
+ if isinstance(old, SupportsFloat) and isinstance(new, SupportsFloat):
72
+ return not math.isclose(old, new, abs_tol=TOLERANCE)
73
+
74
+ return old.__ne__(new)
76
75
 
77
76
 
78
77
  @overload
@@ -91,10 +90,11 @@ def _diffence_between(
91
90
  """Return a difference between values."""
92
91
  if isinstance(old, list) and isinstance(new, list):
93
92
  return [x for x in new if x not in old]
94
- elif isinstance(old, SupportsSubtraction) and isinstance(new, SupportsSubtraction):
95
- return new - old
96
- else:
97
- return None
93
+
94
+ if isinstance(old, SupportsSubtraction) and isinstance(new, SupportsSubtraction):
95
+ return new.__sub__(old)
96
+
97
+ return None
98
98
 
99
99
 
100
100
  class Filter(ABC):
@@ -24,6 +24,7 @@ ECONET_VERSION: Final = 5
24
24
 
25
25
  # Frame header structure.
26
26
  struct_header = struct.Struct("<BH4B")
27
+ HEADER_SIZE = struct_header.size
27
28
 
28
29
  if TYPE_CHECKING:
29
30
  from pyplumio.devices import PhysicalDevice
@@ -73,22 +74,20 @@ class Frame(ABC):
73
74
 
74
75
  __slots__ = (
75
76
  "recipient",
76
- "recipient_device",
77
77
  "sender",
78
- "sender_device",
79
78
  "econet_type",
80
79
  "econet_version",
80
+ "_handler",
81
81
  "_message",
82
82
  "_data",
83
83
  )
84
84
 
85
85
  recipient: DeviceType
86
- recipient_device: PhysicalDevice | None
87
86
  sender: DeviceType
88
- sender_device: PhysicalDevice | None
89
87
  econet_type: int
90
88
  econet_version: int
91
89
  frame_type: ClassVar[FrameType]
90
+ _handler: PhysicalDevice | None
92
91
  _message: bytearray | None
93
92
  _data: dict[str, Any] | None
94
93
 
@@ -104,11 +103,10 @@ class Frame(ABC):
104
103
  ) -> None:
105
104
  """Process a frame data and message."""
106
105
  self.recipient = recipient
107
- self.recipient_device = None
108
106
  self.sender = sender
109
- self.sender_device = None
110
107
  self.econet_type = econet_type
111
108
  self.econet_version = econet_version
109
+ self._handler = None
112
110
  self._data = data if not kwargs else ensure_dict(data, kwargs)
113
111
  self._message = message
114
112
 
@@ -153,6 +151,15 @@ class Frame(ABC):
153
151
  """Return a frame message represented as hex string."""
154
152
  return self.bytes.hex(*args, **kwargs)
155
153
 
154
+ def assign_to(self, device: PhysicalDevice) -> None:
155
+ """Assign device to the frame."""
156
+ self._handler = device
157
+
158
+ @property
159
+ def handler(self) -> PhysicalDevice | None:
160
+ """Return the device associated to the frame."""
161
+ return self._handler
162
+
156
163
  @property
157
164
  def data(self) -> dict[str, Any]:
158
165
  """Return the frame data."""
@@ -19,7 +19,7 @@ class DataType(ABC, Generic[T]):
19
19
  _value: T
20
20
  _size: int
21
21
 
22
- def __init__(self, value: T | None = None):
22
+ def __init__(self, value: T | None = None) -> None:
23
23
  """Initialize a new data type."""
24
24
  if value is not None:
25
25
  self._value = value
@@ -112,7 +112,7 @@ class BitArray(DataType[int]):
112
112
 
113
113
  _index: int
114
114
 
115
- def __init__(self, value: bool | None = None, index: int = 0):
115
+ def __init__(self, value: bool | None = None, index: int = 0) -> None:
116
116
  """Initialize a new bit array."""
117
117
  super().__init__(value)
118
118
  self._index = index
@@ -199,7 +199,7 @@ class String(DataType[str]):
199
199
 
200
200
  __slots__ = ()
201
201
 
202
- def __init__(self, value: str = ""):
202
+ def __init__(self, value: str = "") -> None:
203
203
  """Initialize a new null-terminated string data type."""
204
204
  super().__init__(value)
205
205
  self._size = len(self.value) + 1
@@ -219,7 +219,7 @@ class VarBytes(DataType[bytes]):
219
219
 
220
220
  __slots__ = ()
221
221
 
222
- def __init__(self, value: bytes = b""):
222
+ def __init__(self, value: bytes = b"") -> None:
223
223
  """Initialize a new variable-length bytes data type."""
224
224
  super().__init__(value)
225
225
  self._size = len(value) + 1
@@ -239,7 +239,7 @@ class VarString(DataType[str]):
239
239
 
240
240
  __slots__ = ()
241
241
 
242
- def __init__(self, value: str = ""):
242
+ def __init__(self, value: str = "") -> None:
243
243
  """Initialize a new variable length bytes data type."""
244
244
  super().__init__(value)
245
245
  self._size = len(value) + 1
@@ -326,7 +326,7 @@ class UnsignedInt(BuiltInDataType[int]):
326
326
  _struct = struct.Struct("<I")
327
327
 
328
328
 
329
- class Float(BuiltInDataType[int]):
329
+ class Float(BuiltInDataType[float]):
330
330
  """Represents a float."""
331
331
 
332
332
  __slots__ = ()
@@ -334,7 +334,7 @@ class Float(BuiltInDataType[int]):
334
334
  _struct = struct.Struct("<f")
335
335
 
336
336
 
337
- class Double(BuiltInDataType[int]):
337
+ class Double(BuiltInDataType[float]):
338
338
  """Represents a double."""
339
339
 
340
340
  __slots__ = ()
@@ -77,11 +77,19 @@ class ParameterDescription:
77
77
  class Parameter(ABC):
78
78
  """Represents a base parameter."""
79
79
 
80
- __slots__ = ("device", "description", "_pending_update", "_index", "_values")
80
+ __slots__ = (
81
+ "device",
82
+ "description",
83
+ "_pending_update",
84
+ "_previous_value",
85
+ "_index",
86
+ "_values",
87
+ )
81
88
 
82
89
  device: Device
83
90
  description: ParameterDescription
84
91
  _pending_update: bool
92
+ _previous_value: int
85
93
  _index: int
86
94
  _values: ParameterValues
87
95
 
@@ -96,6 +104,7 @@ class Parameter(ABC):
96
104
  self.device = device
97
105
  self.description = description
98
106
  self._pending_update = False
107
+ self._previous_value = 0
99
108
  self._index = index
100
109
  self._values = values if values else ParameterValues(0, 0, 0)
101
110
 
@@ -185,6 +194,7 @@ class Parameter(ABC):
185
194
  f"Value must be between '{self.min_value}' and '{self.max_value}'"
186
195
  )
187
196
 
197
+ self._previous_value = self._values.value
188
198
  self._values.value = value
189
199
  self._pending_update = True
190
200
  while self.pending_update:
@@ -196,6 +206,9 @@ class Parameter(ABC):
196
206
  return False
197
207
 
198
208
  await self.device.queue.put(await self.create_request())
209
+ if not self.is_tracking_changes:
210
+ await self.force_refresh()
211
+
199
212
  await asyncio.sleep(timeout)
200
213
  retries -= 1
201
214
 
@@ -203,8 +216,19 @@ class Parameter(ABC):
203
216
 
204
217
  def update(self, values: ParameterValues) -> None:
205
218
  """Update the parameter values."""
219
+ if self.pending_update and self._previous_value != values.value:
220
+ self._pending_update = False
221
+
206
222
  self._values = values
207
- self._pending_update = False
223
+
224
+ async def force_refresh(self) -> None:
225
+ """Refresh the parameter from remote."""
226
+ await self.device.queue.put(await self.create_refresh_request())
227
+
228
+ @property
229
+ def is_tracking_changes(self) -> bool:
230
+ """Return True if remote's tracking changes, False otherwise."""
231
+ return False
208
232
 
209
233
  @property
210
234
  def pending_update(self) -> bool:
@@ -254,6 +278,10 @@ class Parameter(ABC):
254
278
  async def create_request(self) -> Request:
255
279
  """Create a request to change the parameter."""
256
280
 
281
+ @abstractmethod
282
+ async def create_refresh_request(self) -> Request:
283
+ """Create a request to refresh the parameter."""
284
+
257
285
 
258
286
  @dataslots
259
287
  @dataclass
@@ -286,6 +314,10 @@ class Number(Parameter):
286
314
  """Create a request to change the number."""
287
315
  return Request()
288
316
 
317
+ async def create_refresh_request(self) -> Request:
318
+ """Create a request to refresh the number."""
319
+ return Request()
320
+
289
321
  @property
290
322
  def value(self) -> int | float:
291
323
  """Return the value."""
@@ -362,6 +394,10 @@ class Switch(Parameter):
362
394
  """Create a request to change the switch."""
363
395
  return Request()
364
396
 
397
+ async def create_refresh_request(self) -> Request:
398
+ """Create a request to refresh the switch."""
399
+ return Request()
400
+
365
401
  @property
366
402
  def value(self) -> Literal["off", "on"]:
367
403
  """Return the value."""
@@ -70,7 +70,7 @@ class ScheduleDay(MutableMapping):
70
70
 
71
71
  _intervals: list[bool]
72
72
 
73
- def __init__(self, intervals: list[bool]):
73
+ def __init__(self, intervals: list[bool]) -> None:
74
74
  """Initialize a new schedule day."""
75
75
  self._intervals = intervals
76
76