PyPlumIO 0.5.43__tar.gz → 0.5.44__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 (152) hide show
  1. {pyplumio-0.5.43 → pyplumio-0.5.44}/PKG-INFO +3 -1
  2. {pyplumio-0.5.43 → pyplumio-0.5.44}/PyPlumIO.egg-info/PKG-INFO +3 -1
  3. {pyplumio-0.5.43 → pyplumio-0.5.44}/PyPlumIO.egg-info/SOURCES.txt +4 -2
  4. {pyplumio-0.5.43 → pyplumio-0.5.44}/PyPlumIO.egg-info/requires.txt +2 -0
  5. {pyplumio-0.5.43 → pyplumio-0.5.44}/docs/source/callbacks.rst +6 -3
  6. {pyplumio-0.5.43 → pyplumio-0.5.44}/docs/source/frames.rst +1 -1
  7. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/_version.py +2 -2
  8. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/const.py +1 -3
  9. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/devices/__init__.py +16 -28
  10. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/devices/ecomax.py +103 -59
  11. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/devices/mixer.py +6 -6
  12. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/devices/thermostat.py +9 -6
  13. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/filters.py +44 -22
  14. pyplumio-0.5.44/pyplumio/helpers/async_cache.py +48 -0
  15. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/helpers/event_manager.py +20 -2
  16. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/helpers/timeout.py +6 -5
  17. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/parameters/__init__.py +9 -3
  18. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/protocol.py +15 -7
  19. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/alerts.py +1 -1
  20. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/boiler_power.py +1 -1
  21. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/fan_power.py +1 -1
  22. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/frame_versions.py +1 -1
  23. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/fuel_consumption.py +1 -1
  24. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/lambda_sensor.py +1 -1
  25. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/mixer_sensors.py +1 -1
  26. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/network_info.py +1 -1
  27. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/output_flags.py +1 -1
  28. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/outputs.py +1 -1
  29. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/product_info.py +1 -1
  30. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/regulator_data.py +1 -1
  31. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/regulator_data_schema.py +1 -1
  32. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/temperatures.py +1 -1
  33. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/thermostat_parameters.py +5 -7
  34. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/thermostat_sensors.py +1 -1
  35. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyproject.toml +2 -0
  36. {pyplumio-0.5.43 → pyplumio-0.5.44}/requirements_test.txt +2 -0
  37. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/conftest.py +8 -0
  38. pyplumio-0.5.44/tests/helpers/test_async_cache.py +49 -0
  39. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/helpers/test_event_manager.py +17 -2
  40. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/parameters/test_init.py +9 -2
  41. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/test_connection.py +5 -1
  42. {pyplumio-0.5.43/tests/helpers → pyplumio-0.5.44/tests}/test_data_types.py +1 -1
  43. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/test_devices.py +23 -17
  44. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/test_filters.py +63 -11
  45. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/test_protocol.py +9 -2
  46. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/responses/regulator_data_schema.json +44 -44
  47. {pyplumio-0.5.43 → pyplumio-0.5.44}/.gitattributes +0 -0
  48. {pyplumio-0.5.43 → pyplumio-0.5.44}/.github/CODE_OF_CONDUCT.md +0 -0
  49. {pyplumio-0.5.43 → pyplumio-0.5.44}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  50. {pyplumio-0.5.43 → pyplumio-0.5.44}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  51. {pyplumio-0.5.43 → pyplumio-0.5.44}/.github/dependabot.yml +0 -0
  52. {pyplumio-0.5.43 → pyplumio-0.5.44}/.github/workflows/ci.yml +0 -0
  53. {pyplumio-0.5.43 → pyplumio-0.5.44}/.github/workflows/codeql.yml +0 -0
  54. {pyplumio-0.5.43 → pyplumio-0.5.44}/.github/workflows/deploy.yml +0 -0
  55. {pyplumio-0.5.43 → pyplumio-0.5.44}/.github/workflows/documentation.yml +0 -0
  56. {pyplumio-0.5.43 → pyplumio-0.5.44}/.gitignore +0 -0
  57. {pyplumio-0.5.43 → pyplumio-0.5.44}/.pre-commit-config.yaml +0 -0
  58. {pyplumio-0.5.43 → pyplumio-0.5.44}/.qlty/qlty.toml +0 -0
  59. {pyplumio-0.5.43 → pyplumio-0.5.44}/.vscode/settings.json +0 -0
  60. {pyplumio-0.5.43 → pyplumio-0.5.44}/LICENSE +0 -0
  61. {pyplumio-0.5.43 → pyplumio-0.5.44}/MANIFEST.in +0 -0
  62. {pyplumio-0.5.43 → pyplumio-0.5.44}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  63. {pyplumio-0.5.43 → pyplumio-0.5.44}/PyPlumIO.egg-info/top_level.txt +0 -0
  64. {pyplumio-0.5.43 → pyplumio-0.5.44}/README.md +0 -0
  65. {pyplumio-0.5.43 → pyplumio-0.5.44}/docs/Makefile +0 -0
  66. {pyplumio-0.5.43 → pyplumio-0.5.44}/docs/make.bat +0 -0
  67. {pyplumio-0.5.43 → pyplumio-0.5.44}/docs/source/conf.py +0 -0
  68. {pyplumio-0.5.43 → pyplumio-0.5.44}/docs/source/connecting.rst +0 -0
  69. {pyplumio-0.5.43 → pyplumio-0.5.44}/docs/source/index.rst +0 -0
  70. {pyplumio-0.5.43 → pyplumio-0.5.44}/docs/source/mixers_thermostats.rst +0 -0
  71. {pyplumio-0.5.43 → pyplumio-0.5.44}/docs/source/protocol.rst +0 -0
  72. {pyplumio-0.5.43 → pyplumio-0.5.44}/docs/source/reading.rst +0 -0
  73. {pyplumio-0.5.43 → pyplumio-0.5.44}/docs/source/schedules.rst +0 -0
  74. {pyplumio-0.5.43 → pyplumio-0.5.44}/docs/source/writing.rst +0 -0
  75. {pyplumio-0.5.43 → pyplumio-0.5.44}/images/ecomax.png +0 -0
  76. {pyplumio-0.5.43 → pyplumio-0.5.44}/images/rs485.png +0 -0
  77. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/__init__.py +0 -0
  78. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/__main__.py +0 -0
  79. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/connection.py +0 -0
  80. {pyplumio-0.5.43/pyplumio/helpers → pyplumio-0.5.44/pyplumio}/data_types.py +0 -0
  81. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/devices/ecoster.py +0 -0
  82. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/exceptions.py +0 -0
  83. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/frames/__init__.py +0 -0
  84. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/frames/messages.py +0 -0
  85. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/frames/requests.py +0 -0
  86. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/frames/responses.py +0 -0
  87. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/helpers/__init__.py +0 -0
  88. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/helpers/factory.py +0 -0
  89. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/helpers/schedule.py +0 -0
  90. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/helpers/task_manager.py +0 -0
  91. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/helpers/uid.py +0 -0
  92. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/parameters/ecomax.py +0 -0
  93. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/parameters/mixer.py +0 -0
  94. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/parameters/thermostat.py +0 -0
  95. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/py.typed +0 -0
  96. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/stream.py +0 -0
  97. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/__init__.py +0 -0
  98. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/boiler_load.py +0 -0
  99. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/ecomax_parameters.py +0 -0
  100. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/fuel_level.py +0 -0
  101. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/mixer_parameters.py +0 -0
  102. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/modules.py +0 -0
  103. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/pending_alerts.py +0 -0
  104. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/program_version.py +0 -0
  105. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/schedules.py +0 -0
  106. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/structures/statuses.py +0 -0
  107. {pyplumio-0.5.43 → pyplumio-0.5.44}/pyplumio/utils.py +0 -0
  108. {pyplumio-0.5.43 → pyplumio-0.5.44}/requirements.txt +0 -0
  109. {pyplumio-0.5.43 → pyplumio-0.5.44}/requirements_docs.txt +0 -0
  110. {pyplumio-0.5.43 → pyplumio-0.5.44}/setup.cfg +0 -0
  111. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/__init__.py +0 -0
  112. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/frames/test_init.py +0 -0
  113. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/frames/test_messages.py +0 -0
  114. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/frames/test_requests.py +0 -0
  115. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/frames/test_responses.py +0 -0
  116. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/helpers/__init__.py +0 -0
  117. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/helpers/test_factory.py +0 -0
  118. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/helpers/test_schedule.py +0 -0
  119. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/helpers/test_task_manager.py +0 -0
  120. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/helpers/test_timeout.py +0 -0
  121. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/helpers/test_uid.py +0 -0
  122. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/parameters/__init__.py +0 -0
  123. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/parameters/test_ecomax.py +0 -0
  124. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/parameters/test_mixers.py +0 -0
  125. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/parameters/test_thermostats.py +0 -0
  126. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/ruff.toml +0 -0
  127. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/test_init.py +0 -0
  128. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/test_main.py +0 -0
  129. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/test_stream.py +0 -0
  130. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/test_utils.py +0 -0
  131. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/messages/regulator_data.json +0 -0
  132. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/messages/sensor_data.json +0 -0
  133. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/requests/alerts.json +0 -0
  134. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/requests/ecomax_control.json +0 -0
  135. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/requests/ecomax_parameters.json +0 -0
  136. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/requests/mixer_parameters.json +0 -0
  137. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  138. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  139. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/requests/set_schedule.json +0 -0
  140. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  141. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/requests/thermostat_parameters.json +0 -0
  142. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/responses/alerts.json +0 -0
  143. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/responses/device_available.json +0 -0
  144. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/responses/ecomax_parameters.json +0 -0
  145. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/responses/mixer_parameters.json +0 -0
  146. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/responses/password.json +0 -0
  147. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/responses/program_version.json +0 -0
  148. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/responses/schedules.json +0 -0
  149. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/responses/thermostat_parameters.json +0 -0
  150. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/responses/uid.json +0 -0
  151. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  152. {pyplumio-0.5.43 → pyplumio-0.5.44}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.5.43
3
+ Version: 0.5.44
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
@@ -29,7 +29,9 @@ Requires-Dist: typing-extensions==4.13.2
29
29
  Provides-Extra: test
30
30
  Requires-Dist: codespell==2.4.1; extra == "test"
31
31
  Requires-Dist: coverage==7.8.0; extra == "test"
32
+ Requires-Dist: freezegun==1.5.1; extra == "test"
32
33
  Requires-Dist: mypy==1.15.0; extra == "test"
34
+ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
33
35
  Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
34
36
  Requires-Dist: pytest==8.3.5; extra == "test"
35
37
  Requires-Dist: pytest-asyncio==0.26.0; extra == "test"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.5.43
3
+ Version: 0.5.44
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
@@ -29,7 +29,9 @@ Requires-Dist: typing-extensions==4.13.2
29
29
  Provides-Extra: test
30
30
  Requires-Dist: codespell==2.4.1; extra == "test"
31
31
  Requires-Dist: coverage==7.8.0; extra == "test"
32
+ Requires-Dist: freezegun==1.5.1; extra == "test"
32
33
  Requires-Dist: mypy==1.15.0; extra == "test"
34
+ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
33
35
  Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
34
36
  Requires-Dist: pytest==8.3.5; extra == "test"
35
37
  Requires-Dist: pytest-asyncio==0.26.0; extra == "test"
@@ -42,6 +42,7 @@ pyplumio/__main__.py
42
42
  pyplumio/_version.py
43
43
  pyplumio/connection.py
44
44
  pyplumio/const.py
45
+ pyplumio/data_types.py
45
46
  pyplumio/exceptions.py
46
47
  pyplumio/filters.py
47
48
  pyplumio/protocol.py
@@ -58,7 +59,7 @@ pyplumio/frames/messages.py
58
59
  pyplumio/frames/requests.py
59
60
  pyplumio/frames/responses.py
60
61
  pyplumio/helpers/__init__.py
61
- pyplumio/helpers/data_types.py
62
+ pyplumio/helpers/async_cache.py
62
63
  pyplumio/helpers/event_manager.py
63
64
  pyplumio/helpers/factory.py
64
65
  pyplumio/helpers/schedule.py
@@ -99,6 +100,7 @@ tests/__init__.py
99
100
  tests/conftest.py
100
101
  tests/ruff.toml
101
102
  tests/test_connection.py
103
+ tests/test_data_types.py
102
104
  tests/test_devices.py
103
105
  tests/test_filters.py
104
106
  tests/test_init.py
@@ -111,7 +113,7 @@ tests/frames/test_messages.py
111
113
  tests/frames/test_requests.py
112
114
  tests/frames/test_responses.py
113
115
  tests/helpers/__init__.py
114
- tests/helpers/test_data_types.py
116
+ tests/helpers/test_async_cache.py
115
117
  tests/helpers/test_event_manager.py
116
118
  tests/helpers/test_factory.py
117
119
  tests/helpers/test_schedule.py
@@ -15,7 +15,9 @@ readthedocs-sphinx-search==0.3.2
15
15
  [test]
16
16
  codespell==2.4.1
17
17
  coverage==7.8.0
18
+ freezegun==1.5.1
18
19
  mypy==1.15.0
20
+ numpy<3.0.0,>=2.0.0
19
21
  pyserial-asyncio-fast==0.16
20
22
  pytest==8.3.5
21
23
  pytest-asyncio==0.26.0
@@ -38,15 +38,18 @@ All built-in filters are described below.
38
38
 
39
39
  .. autofunction:: pyplumio.filters.aggregate
40
40
 
41
- This filter aggregates value for specified amount of seconds and
42
- then calls the callback with the sum of values collected.
41
+ This filter aggregates values for specified amount of seconds or until
42
+ certain sample size is reached and then calls the callback with the
43
+ sum of values collected.
43
44
 
44
45
  .. code-block:: python
45
46
 
46
47
  from pyplumio import filters
47
48
 
48
49
  # Await the callback with the fuel burned during 30 seconds.
49
- ecomax.subscribe("fuel_burned", filters.aggregate(my_callback, seconds=30))
50
+ ecomax.subscribe(
51
+ "fuel_burned", filters.aggregate(my_callback, seconds=30, sample_size=100)
52
+ )
50
53
 
51
54
  .. autofunction:: pyplumio.filters.clamp
52
55
 
@@ -436,7 +436,7 @@ Get the regulator data schema, that describes the data type of :ref:`regulatorda
436
436
  It's represented by dictionary, that's indexed by regulator data field id and
437
437
  a member of the following DataType class, that defines the regulator data field type.
438
438
 
439
- .. autoclass:: pyplumio.helpers.data_types.DataType
439
+ .. autoclass:: pyplumio.data_types.DataType
440
440
 
441
441
  Request
442
442
  ^^^^^^^
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.5.43'
21
- __version_tuple__ = version_tuple = (0, 5, 43)
20
+ __version__ = version = '0.5.44'
21
+ __version_tuple__ = version_tuple = (0, 5, 44)
@@ -14,11 +14,11 @@ ATTR_CURRENT_TEMP: Final = "current_temp"
14
14
  ATTR_DEVICE_INDEX: Final = "device_index"
15
15
  ATTR_FRAME_ERRORS: Final = "frame_errors"
16
16
  ATTR_INDEX: Final = "index"
17
- ATTR_LOADED: Final = "loaded"
18
17
  ATTR_OFFSET: Final = "offset"
19
18
  ATTR_PARAMETER: Final = "parameter"
20
19
  ATTR_PASSWORD: Final = "password"
21
20
  ATTR_SCHEDULE: Final = "schedule"
21
+ ATTR_SETUP: Final = "setup"
22
22
  ATTR_SENSORS: Final = "sensors"
23
23
  ATTR_START: Final = "start"
24
24
  ATTR_STATE: Final = "state"
@@ -229,6 +229,4 @@ PERCENTAGE: Final = "%"
229
229
 
230
230
  STATE_ON: Final = "on"
231
231
  STATE_OFF: Final = "off"
232
-
233
-
234
232
  State: TypeAlias = Literal["on", "off"]
@@ -8,14 +8,13 @@ from functools import cache
8
8
  import logging
9
9
  from typing import Any, ClassVar
10
10
 
11
- from pyplumio.const import ATTR_FRAME_ERRORS, ATTR_LOADED, DeviceType, FrameType, State
11
+ from pyplumio.const import ATTR_FRAME_ERRORS, DeviceType, FrameType, State
12
12
  from pyplumio.exceptions import RequestError, UnknownDeviceError
13
13
  from pyplumio.filters import on_change
14
- from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
14
+ from pyplumio.frames import Frame, Request, is_known_frame_type
15
15
  from pyplumio.helpers.event_manager import EventManager, event_listener
16
16
  from pyplumio.helpers.factory import create_instance
17
17
  from pyplumio.parameters import NumericType, Parameter
18
- from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
19
18
  from pyplumio.structures.network_info import NetworkInfo
20
19
  from pyplumio.utils import to_camelcase
21
20
 
@@ -126,11 +125,11 @@ class PhysicalDevice(Device, ABC):
126
125
  virtual devices associated with them via parent property.
127
126
  """
128
127
 
129
- __slots__ = ("address", "_network", "_setup_frames", "_frame_versions")
128
+ __slots__ = ("address", "_network", "_frame_versions")
130
129
 
131
130
  address: ClassVar[int]
131
+
132
132
  _network: NetworkInfo
133
- _setup_frames: tuple[DataFrameDescription, ...]
134
133
  _frame_versions: dict[int, int]
135
134
 
136
135
  def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
@@ -139,20 +138,27 @@ class PhysicalDevice(Device, ABC):
139
138
  self._network = network
140
139
  self._frame_versions = {}
141
140
 
142
- @event_listener(ATTR_FRAME_VERSIONS, on_change)
141
+ @event_listener(filter=on_change)
143
142
  async def on_event_frame_versions(self, versions: dict[int, int]) -> None:
144
143
  """Check frame versions and update outdated frames."""
144
+ _LOGGER.info("Received version table")
145
145
  for frame_type, version in versions.items():
146
146
  if (
147
147
  is_known_frame_type(frame_type)
148
148
  and self.supports_frame_type(frame_type)
149
149
  and not self.has_frame_version(frame_type, version)
150
150
  ):
151
- _LOGGER.debug("Updating frame %s to version %i", frame_type, version)
152
- request = await Request.create(frame_type, recipient=self.address)
153
- self.queue.put_nowait(request)
151
+ await self._request_frame_version(frame_type, version)
154
152
  self._frame_versions[frame_type] = version
155
153
 
154
+ async def _request_frame_version(
155
+ self, frame_type: FrameType | int, version: int
156
+ ) -> None:
157
+ """Request frame version from the device."""
158
+ _LOGGER.info("Updating frame %s to version %i", repr(frame_type), version)
159
+ request = await Request.create(frame_type, recipient=self.address)
160
+ self.queue.put_nowait(request)
161
+
156
162
  def has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
157
163
  """Return True if frame data is up to date, False otherwise."""
158
164
  return (
@@ -171,25 +177,6 @@ class PhysicalDevice(Device, ABC):
171
177
  for name, value in frame.data.items():
172
178
  self.dispatch_nowait(name, value)
173
179
 
174
- async def async_setup(self) -> bool:
175
- """Set up addressable device."""
176
- results = await asyncio.gather(
177
- *(
178
- self.request(description.provides, description.frame_type)
179
- for description in self._setup_frames
180
- ),
181
- return_exceptions=True,
182
- )
183
-
184
- errors = [
185
- result.frame_type for result in results if isinstance(result, RequestError)
186
- ]
187
-
188
- await asyncio.gather(
189
- self.dispatch(ATTR_FRAME_ERRORS, errors), self.dispatch(ATTR_LOADED, True)
190
- )
191
- return True
192
-
193
180
  async def request(
194
181
  self, name: str, frame_type: FrameType, retries: int = 3, timeout: float = 3.0
195
182
  ) -> Any:
@@ -197,6 +184,7 @@ class PhysicalDevice(Device, ABC):
197
184
 
198
185
  If value is not available before timeout, retry request.
199
186
  """
187
+ _LOGGER.info("Requesting '%s' with %s", name, repr(frame_type))
200
188
  request = await Request.create(frame_type, recipient=self.address)
201
189
  while retries > 0:
202
190
  try:
@@ -3,15 +3,15 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Coroutine, Generator, Iterable, Sequence
6
+ from collections.abc import Coroutine, Generator, Iterable
7
7
  import logging
8
8
  import time
9
9
  from typing import Any, Final
10
10
 
11
11
  from pyplumio.const import (
12
+ ATTR_FRAME_ERRORS,
12
13
  ATTR_PASSWORD,
13
14
  ATTR_SENSORS,
14
- ATTR_STATE,
15
15
  STATE_OFF,
16
16
  STATE_ON,
17
17
  DeviceState,
@@ -22,6 +22,7 @@ from pyplumio.const import (
22
22
  from pyplumio.devices import PhysicalDevice
23
23
  from pyplumio.devices.mixer import Mixer
24
24
  from pyplumio.devices.thermostat import Thermostat
25
+ from pyplumio.exceptions import RequestError
25
26
  from pyplumio.filters import on_change
26
27
  from pyplumio.frames import DataFrameDescription, Frame, Request
27
28
  from pyplumio.helpers.event_manager import event_listener
@@ -40,14 +41,12 @@ from pyplumio.structures.ecomax_parameters import (
40
41
  ATTR_ECOMAX_CONTROL,
41
42
  ATTR_ECOMAX_PARAMETERS,
42
43
  )
43
- from pyplumio.structures.fuel_consumption import ATTR_FUEL_CONSUMPTION
44
44
  from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
45
45
  from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
46
46
  from pyplumio.structures.network_info import ATTR_NETWORK, NetworkInfo
47
47
  from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
48
48
  from pyplumio.structures.regulator_data_schema import ATTR_REGDATA_SCHEMA
49
49
  from pyplumio.structures.schedules import (
50
- ATTR_SCHEDULE_PARAMETERS,
51
50
  ATTR_SCHEDULES,
52
51
  SCHEDULE_PARAMETERS,
53
52
  SCHEDULES,
@@ -55,20 +54,53 @@ from pyplumio.structures.schedules import (
55
54
  ScheduleSwitch,
56
55
  ScheduleSwitchDescription,
57
56
  )
58
- from pyplumio.structures.thermostat_parameters import (
59
- ATTR_THERMOSTAT_PARAMETERS,
60
- ATTR_THERMOSTAT_PROFILE,
61
- )
57
+ from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PARAMETERS
62
58
  from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_SENSORS
63
59
 
60
+ _LOGGER = logging.getLogger(__name__)
61
+
62
+
64
63
  ATTR_MIXERS: Final = "mixers"
65
64
  ATTR_THERMOSTATS: Final = "thermostats"
66
65
  ATTR_FUEL_BURNED: Final = "fuel_burned"
67
66
 
68
- NANOSECONDS_IN_SECOND: Final = 1_000_000_000
69
- MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS: Final = 300 * NANOSECONDS_IN_SECOND
67
+ MAX_TIME_SINCE_LAST_FUEL_UPDATE: Final = 5 * 60
68
+
69
+
70
+ class FuelMeter:
71
+ """Represents a fuel meter.
72
+
73
+ Calculates the fuel burned based on the time
74
+ elapsed since the last sensor message, which contains fuel
75
+ consumption data. If the elapsed time is within the acceptable
76
+ range, it returns the fuel burned data. Otherwise, it logs a
77
+ warning and returns None.
78
+ """
79
+
80
+ __slots__ = ("_last_update_time",)
81
+
82
+ _last_update_time: float
70
83
 
71
- SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
84
+ def __init__(self) -> None:
85
+ """Initialize a new fuel meter."""
86
+ self._last_update_time = time.monotonic()
87
+
88
+ def calculate(self, fuel_consumption: float) -> float | None:
89
+ """Calculate the amount of burned fuel since last update."""
90
+ current_time = time.monotonic()
91
+ time_since_update = current_time - self._last_update_time
92
+ self._last_update_time = current_time
93
+ if time_since_update < MAX_TIME_SINCE_LAST_FUEL_UPDATE:
94
+ return fuel_consumption * (time_since_update / 3600)
95
+
96
+ _LOGGER.warning(
97
+ "Skipping outdated fuel consumption data (was %f seconds old)",
98
+ time_since_update,
99
+ )
100
+ return None
101
+
102
+
103
+ REQUIRED: tuple[DataFrameDescription, ...] = (
72
104
  DataFrameDescription(
73
105
  frame_type=FrameType.REQUEST_UID,
74
106
  provides=ATTR_PRODUCT,
@@ -103,28 +135,22 @@ SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
103
135
  ),
104
136
  )
105
137
 
106
- _LOGGER = logging.getLogger(__name__)
138
+ REQUIRED_TYPES = [description.frame_type for description in REQUIRED]
107
139
 
108
140
 
109
141
  class EcoMAX(PhysicalDevice):
110
142
  """Represents an ecoMAX controller."""
111
143
 
112
- __slots__ = ("_fuel_burned_time_ns",)
144
+ __slots__ = ("_fuel_meter",)
113
145
 
114
- address = DeviceType.ECOMAX
115
- _setup_frames = SETUP_FRAME_TYPES
146
+ _fuel_meter: FuelMeter
116
147
 
117
- _fuel_burned_time_ns: int
148
+ address = DeviceType.ECOMAX
118
149
 
119
150
  def __init__(self, queue: asyncio.Queue[Frame], network: NetworkInfo) -> None:
120
151
  """Initialize a new ecoMAX controller."""
121
152
  super().__init__(queue, network)
122
- self._fuel_burned_time_ns = time.perf_counter_ns()
123
-
124
- async def async_setup(self) -> bool:
125
- """Set up an ecoMAX controller."""
126
- await self.wait_for(ATTR_SENSORS)
127
- return await super().async_setup()
153
+ self._fuel_meter = FuelMeter()
128
154
 
129
155
  def handle_frame(self, frame: Frame) -> None:
130
156
  """Handle frame received from the ecoMAX device."""
@@ -162,6 +188,13 @@ class EcoMAX(PhysicalDevice):
162
188
 
163
189
  return self.dispatch_nowait(ATTR_THERMOSTATS, thermostats)
164
190
 
191
+ async def _request_frame_version(
192
+ self, frame_type: FrameType | int, version: int
193
+ ) -> None:
194
+ """Request frame version from the device."""
195
+ if frame_type not in REQUIRED_TYPES:
196
+ await super()._request_frame_version(frame_type, version)
197
+
165
198
  async def _set_ecomax_state(self, state: State) -> bool:
166
199
  """Try to set the ecoMAX control state."""
167
200
  try:
@@ -196,11 +229,34 @@ class EcoMAX(PhysicalDevice):
196
229
  await asyncio.gather(*(device.shutdown() for device in devices))
197
230
  await super().shutdown()
198
231
 
199
- @event_listener(ATTR_ECOMAX_PARAMETERS)
232
+ @event_listener
233
+ async def on_event_setup(self, setup: bool) -> None:
234
+ """Request frames required to set up an ecoMAX entry."""
235
+ _LOGGER.info("Setting up device entry")
236
+ await self.wait_for(ATTR_SENSORS)
237
+ results = await asyncio.gather(
238
+ *(
239
+ self.request(description.provides, description.frame_type)
240
+ for description in REQUIRED
241
+ ),
242
+ return_exceptions=True,
243
+ )
244
+
245
+ errors = [
246
+ result.frame_type for result in results if isinstance(result, RequestError)
247
+ ]
248
+
249
+ if errors:
250
+ self.dispatch_nowait(ATTR_FRAME_ERRORS, errors)
251
+
252
+ _LOGGER.info("Device entry setup done")
253
+
254
+ @event_listener
200
255
  async def on_event_ecomax_parameters(
201
- self, parameters: Sequence[tuple[int, ParameterValues]]
256
+ self, parameters: list[tuple[int, ParameterValues]]
202
257
  ) -> bool:
203
258
  """Update ecoMAX parameters and dispatch the events."""
259
+ _LOGGER.info("Received device parameters")
204
260
  product_info: ProductInfo = await self.get(ATTR_PRODUCT)
205
261
 
206
262
  def _ecomax_parameter_events() -> Generator[Coroutine, Any, None]:
@@ -236,37 +292,20 @@ class EcoMAX(PhysicalDevice):
236
292
  await asyncio.gather(*_ecomax_parameter_events())
237
293
  return True
238
294
 
239
- @event_listener(ATTR_FUEL_CONSUMPTION)
295
+ @event_listener
240
296
  async def on_event_fuel_consumption(self, fuel_consumption: float) -> None:
241
- """Update the amount of burned fuel.
242
-
243
- This method calculates the fuel burned based on the time
244
- elapsed since the last sensor message, which contains fuel
245
- consumption data. If the elapsed time is within the acceptable
246
- range, it dispatches the fuel burned data. Otherwise, it logs a
247
- warning and skips the outdated data.
248
- """
249
- time_ns = time.perf_counter_ns()
250
- nanoseconds_passed = time_ns - self._fuel_burned_time_ns
251
- self._fuel_burned_time_ns = time_ns
252
- if nanoseconds_passed < MAX_TIME_SINCE_LAST_FUEL_UPDATE_NS:
253
- return self.dispatch_nowait(
254
- ATTR_FUEL_BURNED,
255
- fuel_consumption * nanoseconds_passed / (3600 * NANOSECONDS_IN_SECOND),
256
- )
257
-
258
- _LOGGER.warning(
259
- "Skipping outdated fuel consumption data: %f (was %i seconds old)",
260
- fuel_consumption,
261
- nanoseconds_passed / NANOSECONDS_IN_SECOND,
262
- )
297
+ """Update the amount of burned fuel."""
298
+ fuel_burned = self._fuel_meter.calculate(fuel_consumption)
299
+ if fuel_burned is not None:
300
+ self.dispatch_nowait(ATTR_FUEL_BURNED, fuel_burned)
263
301
 
264
- @event_listener(ATTR_MIXER_PARAMETERS)
302
+ @event_listener
265
303
  async def on_event_mixer_parameters(
266
304
  self,
267
- parameters: dict[int, Sequence[tuple[int, ParameterValues]]] | None,
305
+ parameters: dict[int, list[tuple[int, ParameterValues]]] | None,
268
306
  ) -> bool:
269
307
  """Handle mixer parameters and dispatch the events."""
308
+ _LOGGER.info("Received mixer parameters")
270
309
  if parameters:
271
310
  await asyncio.gather(
272
311
  *(
@@ -278,11 +317,12 @@ class EcoMAX(PhysicalDevice):
278
317
 
279
318
  return False
280
319
 
281
- @event_listener(ATTR_MIXER_SENSORS)
320
+ @event_listener
282
321
  async def on_event_mixer_sensors(
283
322
  self, sensors: dict[int, dict[str, Any]] | None
284
323
  ) -> bool:
285
324
  """Update mixer sensors and dispatch the events."""
325
+ _LOGGER.info("Received mixer sensors")
286
326
  if sensors:
287
327
  await asyncio.gather(
288
328
  *(
@@ -294,9 +334,9 @@ class EcoMAX(PhysicalDevice):
294
334
 
295
335
  return False
296
336
 
297
- @event_listener(ATTR_SCHEDULE_PARAMETERS)
337
+ @event_listener
298
338
  async def on_event_schedule_parameters(
299
- self, parameters: Sequence[tuple[int, ParameterValues]]
339
+ self, parameters: list[tuple[int, ParameterValues]]
300
340
  ) -> bool:
301
341
  """Update schedule parameters and dispatch the events."""
302
342
 
@@ -319,20 +359,22 @@ class EcoMAX(PhysicalDevice):
319
359
  await asyncio.gather(*_schedule_parameter_events())
320
360
  return True
321
361
 
322
- @event_listener(ATTR_SENSORS)
362
+ @event_listener
323
363
  async def on_event_sensors(self, sensors: dict[str, Any]) -> bool:
324
364
  """Update ecoMAX sensors and dispatch the events."""
365
+ _LOGGER.info("Received device sensors")
325
366
  await asyncio.gather(
326
367
  *(self.dispatch(name, value) for name, value in sensors.items())
327
368
  )
328
369
  return True
329
370
 
330
- @event_listener(ATTR_THERMOSTAT_PARAMETERS)
371
+ @event_listener
331
372
  async def on_event_thermostat_parameters(
332
373
  self,
333
- parameters: dict[int, Sequence[tuple[int, ParameterValues]]] | None,
374
+ parameters: dict[int, list[tuple[int, ParameterValues]]] | None,
334
375
  ) -> bool:
335
376
  """Handle thermostat parameters and dispatch the events."""
377
+ _LOGGER.info("Received thermostat parameters")
336
378
  if parameters:
337
379
  await asyncio.gather(
338
380
  *(
@@ -346,7 +388,7 @@ class EcoMAX(PhysicalDevice):
346
388
 
347
389
  return False
348
390
 
349
- @event_listener(ATTR_THERMOSTAT_PROFILE)
391
+ @event_listener
350
392
  async def on_event_thermostat_profile(
351
393
  self, values: ParameterValues | None
352
394
  ) -> EcomaxNumber | None:
@@ -358,11 +400,12 @@ class EcoMAX(PhysicalDevice):
358
400
 
359
401
  return None
360
402
 
361
- @event_listener(ATTR_THERMOSTAT_SENSORS)
403
+ @event_listener
362
404
  async def on_event_thermostat_sensors(
363
405
  self, sensors: dict[int, dict[str, Any]] | None
364
406
  ) -> bool:
365
407
  """Update thermostat sensors and dispatch the events."""
408
+ _LOGGER.info("Received thermostat sensors")
366
409
  if sensors:
367
410
  await asyncio.gather(
368
411
  *(
@@ -377,11 +420,12 @@ class EcoMAX(PhysicalDevice):
377
420
 
378
421
  return False
379
422
 
380
- @event_listener(ATTR_SCHEDULES)
423
+ @event_listener
381
424
  async def on_event_schedules(
382
425
  self, schedules: list[tuple[int, list[list[bool]]]]
383
426
  ) -> dict[str, Schedule]:
384
427
  """Update schedules."""
428
+ _LOGGER.info("Received device schedules")
385
429
  return {
386
430
  SCHEDULES[index]: Schedule(
387
431
  name=SCHEDULES[index],
@@ -397,7 +441,7 @@ class EcoMAX(PhysicalDevice):
397
441
  for index, schedule in schedules
398
442
  }
399
443
 
400
- @event_listener(ATTR_STATE, on_change)
444
+ @event_listener(filter=on_change)
401
445
  async def on_event_state(self, state: DeviceState) -> None:
402
446
  """Update the ecoMAX control parameter."""
403
447
  await self.dispatch(
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Coroutine, Generator, Sequence
6
+ from collections.abc import Coroutine, Generator
7
7
  import logging
8
8
  from typing import Any
9
9
 
@@ -16,8 +16,6 @@ from pyplumio.parameters.mixer import (
16
16
  MixerSwitchDescription,
17
17
  get_mixer_parameter_types,
18
18
  )
19
- from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS
20
- from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
21
19
  from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
22
20
 
23
21
  _LOGGER = logging.getLogger(__name__)
@@ -28,19 +26,21 @@ class Mixer(VirtualDevice):
28
26
 
29
27
  __slots__ = ()
30
28
 
31
- @event_listener(ATTR_MIXER_SENSORS)
29
+ @event_listener
32
30
  async def on_event_mixer_sensors(self, sensors: dict[str, Any]) -> bool:
33
31
  """Update mixer sensors and dispatch the events."""
32
+ _LOGGER.info("Received mixer %i sensors", self.index)
34
33
  await asyncio.gather(
35
34
  *(self.dispatch(name, value) for name, value in sensors.items())
36
35
  )
37
36
  return True
38
37
 
39
- @event_listener(ATTR_MIXER_PARAMETERS)
38
+ @event_listener
40
39
  async def on_event_mixer_parameters(
41
- self, parameters: Sequence[tuple[int, ParameterValues]]
40
+ self, parameters: list[tuple[int, ParameterValues]]
42
41
  ) -> bool:
43
42
  """Update mixer parameters and dispatch the events."""
43
+ _LOGGER.info("Received mixer %i parameters", self.index)
44
44
  product_info: ProductInfo = await self.parent.get(ATTR_PRODUCT)
45
45
 
46
46
  def _mixer_parameter_events() -> Generator[Coroutine, Any, None]:
@@ -3,7 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Coroutine, Generator, Sequence
6
+ from collections.abc import Coroutine, Generator
7
+ import logging
7
8
  from typing import Any
8
9
 
9
10
  from pyplumio.devices import VirtualDevice
@@ -15,8 +16,8 @@ from pyplumio.parameters.thermostat import (
15
16
  ThermostatSwitchDescription,
16
17
  get_thermostat_parameter_types,
17
18
  )
18
- from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PARAMETERS
19
- from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_SENSORS
19
+
20
+ _LOGGER = logging.getLogger()
20
21
 
21
22
 
22
23
  class Thermostat(VirtualDevice):
@@ -24,19 +25,21 @@ class Thermostat(VirtualDevice):
24
25
 
25
26
  __slots__ = ()
26
27
 
27
- @event_listener(ATTR_THERMOSTAT_SENSORS)
28
+ @event_listener
28
29
  async def on_event_thermostat_sensors(self, sensors: dict[str, Any]) -> bool:
29
30
  """Update thermostat sensors and dispatch the events."""
31
+ _LOGGER.info("Received thermostat %i sensors", self.index)
30
32
  await asyncio.gather(
31
33
  *(self.dispatch(name, value) for name, value in sensors.items())
32
34
  )
33
35
  return True
34
36
 
35
- @event_listener(ATTR_THERMOSTAT_PARAMETERS)
37
+ @event_listener
36
38
  async def on_event_thermostat_parameters(
37
- self, parameters: Sequence[tuple[int, ParameterValues]]
39
+ self, parameters: list[tuple[int, ParameterValues]]
38
40
  ) -> bool:
39
41
  """Update thermostat parameters and dispatch the events."""
42
+ _LOGGER.info("Received thermostat %i parameters", self.index)
40
43
 
41
44
  def _thermostat_parameter_events() -> Generator[Coroutine, Any, None]:
42
45
  """Get dispatch calls for thermostat parameter events."""