PyPlumIO 0.5.36__tar.gz → 0.5.38__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 (144) hide show
  1. {pyplumio-0.5.36 → pyplumio-0.5.38}/.pre-commit-config.yaml +2 -2
  2. {pyplumio-0.5.36 → pyplumio-0.5.38}/PKG-INFO +9 -9
  3. {pyplumio-0.5.36 → pyplumio-0.5.38}/PyPlumIO.egg-info/PKG-INFO +9 -9
  4. {pyplumio-0.5.36 → pyplumio-0.5.38}/PyPlumIO.egg-info/requires.txt +6 -6
  5. {pyplumio-0.5.36 → pyplumio-0.5.38}/README.md +2 -2
  6. {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/index.rst +3 -2
  7. {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/schedules.rst +16 -12
  8. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/_version.py +9 -4
  9. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/const.py +8 -4
  10. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/devices/ecomax.py +20 -22
  11. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/factory.py +1 -1
  12. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/parameter.py +17 -8
  13. pyplumio-0.5.38/pyplumio/helpers/schedule.py +176 -0
  14. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/uid.py +2 -2
  15. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/alerts.py +1 -1
  16. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/ecomax_parameters.py +3 -1
  17. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/outputs.py +1 -2
  18. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/product_info.py +2 -2
  19. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyproject.toml +9 -8
  20. pyplumio-0.5.38/requirements_test.txt +11 -0
  21. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_parameter.py +55 -11
  22. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_schedule.py +50 -26
  23. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_uid.py +2 -2
  24. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_devices.py +7 -4
  25. pyplumio-0.5.36/pyplumio/helpers/schedule.py +0 -178
  26. pyplumio-0.5.36/requirements_test.txt +0 -11
  27. {pyplumio-0.5.36 → pyplumio-0.5.38}/.gitattributes +0 -0
  28. {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/CODE_OF_CONDUCT.md +0 -0
  29. {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  30. {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  31. {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/dependabot.yml +0 -0
  32. {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/workflows/ci.yml +0 -0
  33. {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/workflows/codeql-analysis.yml +0 -0
  34. {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/workflows/deploy.yml +0 -0
  35. {pyplumio-0.5.36 → pyplumio-0.5.38}/.github/workflows/documentation.yml +0 -0
  36. {pyplumio-0.5.36 → pyplumio-0.5.38}/.gitignore +0 -0
  37. {pyplumio-0.5.36 → pyplumio-0.5.38}/.vscode/settings.json +0 -0
  38. {pyplumio-0.5.36 → pyplumio-0.5.38}/LICENSE +0 -0
  39. {pyplumio-0.5.36 → pyplumio-0.5.38}/MANIFEST.in +0 -0
  40. {pyplumio-0.5.36 → pyplumio-0.5.38}/PyPlumIO.egg-info/SOURCES.txt +0 -0
  41. {pyplumio-0.5.36 → pyplumio-0.5.38}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  42. {pyplumio-0.5.36 → pyplumio-0.5.38}/PyPlumIO.egg-info/top_level.txt +0 -0
  43. {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/Makefile +0 -0
  44. {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/make.bat +0 -0
  45. {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/callbacks.rst +0 -0
  46. {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/conf.py +0 -0
  47. {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/connecting.rst +0 -0
  48. {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/frames.rst +0 -0
  49. {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/mixers_thermostats.rst +0 -0
  50. {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/protocol.rst +0 -0
  51. {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/reading.rst +0 -0
  52. {pyplumio-0.5.36 → pyplumio-0.5.38}/docs/source/writing.rst +0 -0
  53. {pyplumio-0.5.36 → pyplumio-0.5.38}/images/ecomax.png +0 -0
  54. {pyplumio-0.5.36 → pyplumio-0.5.38}/images/rs485.png +0 -0
  55. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/__init__.py +0 -0
  56. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/__main__.py +0 -0
  57. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/connection.py +0 -0
  58. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/devices/__init__.py +0 -0
  59. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/devices/ecoster.py +0 -0
  60. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/devices/mixer.py +0 -0
  61. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/devices/thermostat.py +0 -0
  62. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/exceptions.py +0 -0
  63. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/filters.py +0 -0
  64. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/frames/__init__.py +0 -0
  65. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/frames/messages.py +0 -0
  66. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/frames/requests.py +0 -0
  67. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/frames/responses.py +0 -0
  68. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/__init__.py +0 -0
  69. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/data_types.py +0 -0
  70. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/event_manager.py +0 -0
  71. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/task_manager.py +0 -0
  72. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/helpers/timeout.py +0 -0
  73. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/protocol.py +0 -0
  74. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/py.typed +0 -0
  75. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/stream.py +0 -0
  76. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/__init__.py +0 -0
  77. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/boiler_load.py +0 -0
  78. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/boiler_power.py +0 -0
  79. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/fan_power.py +0 -0
  80. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/frame_versions.py +0 -0
  81. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/fuel_consumption.py +0 -0
  82. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/fuel_level.py +0 -0
  83. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/lambda_sensor.py +0 -0
  84. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/mixer_parameters.py +0 -0
  85. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/mixer_sensors.py +0 -0
  86. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/modules.py +0 -0
  87. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/network_info.py +0 -0
  88. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/output_flags.py +0 -0
  89. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/pending_alerts.py +0 -0
  90. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/program_version.py +0 -0
  91. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/regulator_data.py +0 -0
  92. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/regulator_data_schema.py +0 -0
  93. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/schedules.py +0 -0
  94. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/statuses.py +0 -0
  95. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/temperatures.py +0 -0
  96. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/thermostat_parameters.py +0 -0
  97. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/structures/thermostat_sensors.py +0 -0
  98. {pyplumio-0.5.36 → pyplumio-0.5.38}/pyplumio/utils.py +0 -0
  99. {pyplumio-0.5.36 → pyplumio-0.5.38}/requirements.txt +0 -0
  100. {pyplumio-0.5.36 → pyplumio-0.5.38}/requirements_docs.txt +0 -0
  101. {pyplumio-0.5.36 → pyplumio-0.5.38}/setup.cfg +0 -0
  102. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/__init__.py +0 -0
  103. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/conftest.py +0 -0
  104. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/frames/test_init.py +0 -0
  105. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/frames/test_messages.py +0 -0
  106. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/frames/test_requests.py +0 -0
  107. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/frames/test_responses.py +0 -0
  108. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/__init__.py +0 -0
  109. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_data_types.py +0 -0
  110. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_event_manager.py +0 -0
  111. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_factory.py +0 -0
  112. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_task_manager.py +0 -0
  113. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/helpers/test_timeout.py +0 -0
  114. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/ruff.toml +0 -0
  115. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_connection.py +0 -0
  116. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_filters.py +0 -0
  117. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_init.py +0 -0
  118. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_main.py +0 -0
  119. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_protocol.py +0 -0
  120. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_stream.py +0 -0
  121. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/test_utils.py +0 -0
  122. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/messages/regulator_data.json +0 -0
  123. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/messages/sensor_data.json +0 -0
  124. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/alerts.json +0 -0
  125. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/ecomax_control.json +0 -0
  126. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/ecomax_parameters.json +0 -0
  127. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/mixer_parameters.json +0 -0
  128. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  129. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  130. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/set_schedule.json +0 -0
  131. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  132. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/requests/thermostat_parameters.json +0 -0
  133. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/alerts.json +0 -0
  134. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/device_available.json +0 -0
  135. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/ecomax_parameters.json +0 -0
  136. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/mixer_parameters.json +0 -0
  137. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/password.json +0 -0
  138. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/program_version.json +0 -0
  139. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/regulator_data_schema.json +0 -0
  140. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/schedules.json +0 -0
  141. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/thermostat_parameters.json +0 -0
  142. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/responses/uid.json +0 -0
  143. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  144. {pyplumio-0.5.36 → pyplumio-0.5.38}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -2,13 +2,13 @@
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.9.3
5
+ rev: v0.9.4
6
6
  hooks:
7
7
  - id: ruff
8
8
  args:
9
9
  - --fix
10
10
  - repo: https://github.com/codespell-project/codespell
11
- rev: v2.4.0
11
+ rev: v2.4.1
12
12
  hooks:
13
13
  - id: codespell
14
14
  - repo: https://github.com/pre-commit/mirrors-mypy
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyPlumIO
3
- Version: 0.5.36
3
+ Version: 0.5.38
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,15 +27,15 @@ Requires-Dist: dataslots==1.2.0
27
27
  Requires-Dist: pyserial-asyncio==0.6
28
28
  Requires-Dist: typing-extensions==4.12.2
29
29
  Provides-Extra: test
30
- Requires-Dist: codespell==2.4.0; extra == "test"
31
- Requires-Dist: coverage==7.6.10; extra == "test"
32
- Requires-Dist: mypy==1.14.1; extra == "test"
30
+ Requires-Dist: codespell==2.4.1; extra == "test"
31
+ Requires-Dist: coverage==7.6.12; extra == "test"
32
+ Requires-Dist: mypy==1.15.0; extra == "test"
33
33
  Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
34
34
  Requires-Dist: pytest==8.3.4; extra == "test"
35
- Requires-Dist: pytest-asyncio==0.25.2; extra == "test"
36
- Requires-Dist: ruff==0.9.3; extra == "test"
35
+ Requires-Dist: pytest-asyncio==0.25.3; extra == "test"
36
+ Requires-Dist: ruff==0.9.6; extra == "test"
37
37
  Requires-Dist: tox==4.24.1; extra == "test"
38
- Requires-Dist: types-pyserial==3.5.0.20250124; extra == "test"
38
+ Requires-Dist: types-pyserial==3.5.0.20250130; extra == "test"
39
39
  Provides-Extra: docs
40
40
  Requires-Dist: sphinx==8.1.3; extra == "docs"
41
41
  Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
@@ -49,8 +49,8 @@ Requires-Dist: tomli==2.2.1; extra == "dev"
49
49
  [![PyPI version](https://badge.fury.io/py/PyPlumIO.svg)](https://badge.fury.io/py/PyPlumIO)
50
50
  [![PyPI Supported Python Versions](https://img.shields.io/pypi/pyversions/pyplumio.svg)](https://pypi.python.org/pypi/pyplumio/)
51
51
  [![PyPlumIO CI](https://github.com/denpamusic/PyPlumIO/actions/workflows/ci.yml/badge.svg)](https://github.com/denpamusic/PyPlumIO/actions/workflows/ci.yml)
52
- [![Maintainability](https://api.codeclimate.com/v1/badges/9f275fbc50fe9082a909/maintainability)](https://codeclimate.com/github/denpamusic/PyPlumIO/maintainability)
53
- [![Test Coverage](https://api.codeclimate.com/v1/badges/9f275fbc50fe9082a909/test_coverage)](https://codeclimate.com/github/denpamusic/PyPlumIO/test_coverage)
52
+ [![Maintainability](https://api.codeclimate.com/v1/badges/e802127770476b7ba6fd/maintainability)](https://codeclimate.com/github/denpamusic/PyPlumIO/maintainability)
53
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/e802127770476b7ba6fd/test_coverage)](https://codeclimate.com/github/denpamusic/PyPlumIO/test_coverage)
54
54
  [![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg)](https://guidelines.denpa.pro/stability#release-candidate)
55
55
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
56
56
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyPlumIO
3
- Version: 0.5.36
3
+ Version: 0.5.38
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,15 +27,15 @@ Requires-Dist: dataslots==1.2.0
27
27
  Requires-Dist: pyserial-asyncio==0.6
28
28
  Requires-Dist: typing-extensions==4.12.2
29
29
  Provides-Extra: test
30
- Requires-Dist: codespell==2.4.0; extra == "test"
31
- Requires-Dist: coverage==7.6.10; extra == "test"
32
- Requires-Dist: mypy==1.14.1; extra == "test"
30
+ Requires-Dist: codespell==2.4.1; extra == "test"
31
+ Requires-Dist: coverage==7.6.12; extra == "test"
32
+ Requires-Dist: mypy==1.15.0; extra == "test"
33
33
  Requires-Dist: pyserial-asyncio-fast==0.14; extra == "test"
34
34
  Requires-Dist: pytest==8.3.4; extra == "test"
35
- Requires-Dist: pytest-asyncio==0.25.2; extra == "test"
36
- Requires-Dist: ruff==0.9.3; extra == "test"
35
+ Requires-Dist: pytest-asyncio==0.25.3; extra == "test"
36
+ Requires-Dist: ruff==0.9.6; extra == "test"
37
37
  Requires-Dist: tox==4.24.1; extra == "test"
38
- Requires-Dist: types-pyserial==3.5.0.20250124; extra == "test"
38
+ Requires-Dist: types-pyserial==3.5.0.20250130; extra == "test"
39
39
  Provides-Extra: docs
40
40
  Requires-Dist: sphinx==8.1.3; extra == "docs"
41
41
  Requires-Dist: sphinx_rtd_theme==3.0.2; extra == "docs"
@@ -49,8 +49,8 @@ Requires-Dist: tomli==2.2.1; extra == "dev"
49
49
  [![PyPI version](https://badge.fury.io/py/PyPlumIO.svg)](https://badge.fury.io/py/PyPlumIO)
50
50
  [![PyPI Supported Python Versions](https://img.shields.io/pypi/pyversions/pyplumio.svg)](https://pypi.python.org/pypi/pyplumio/)
51
51
  [![PyPlumIO CI](https://github.com/denpamusic/PyPlumIO/actions/workflows/ci.yml/badge.svg)](https://github.com/denpamusic/PyPlumIO/actions/workflows/ci.yml)
52
- [![Maintainability](https://api.codeclimate.com/v1/badges/9f275fbc50fe9082a909/maintainability)](https://codeclimate.com/github/denpamusic/PyPlumIO/maintainability)
53
- [![Test Coverage](https://api.codeclimate.com/v1/badges/9f275fbc50fe9082a909/test_coverage)](https://codeclimate.com/github/denpamusic/PyPlumIO/test_coverage)
52
+ [![Maintainability](https://api.codeclimate.com/v1/badges/e802127770476b7ba6fd/maintainability)](https://codeclimate.com/github/denpamusic/PyPlumIO/maintainability)
53
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/e802127770476b7ba6fd/test_coverage)](https://codeclimate.com/github/denpamusic/PyPlumIO/test_coverage)
54
54
  [![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg)](https://guidelines.denpa.pro/stability#release-candidate)
55
55
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
56
56
 
@@ -13,12 +13,12 @@ sphinx_rtd_theme==3.0.2
13
13
  readthedocs-sphinx-search==0.3.2
14
14
 
15
15
  [test]
16
- codespell==2.4.0
17
- coverage==7.6.10
18
- mypy==1.14.1
16
+ codespell==2.4.1
17
+ coverage==7.6.12
18
+ mypy==1.15.0
19
19
  pyserial-asyncio-fast==0.14
20
20
  pytest==8.3.4
21
- pytest-asyncio==0.25.2
22
- ruff==0.9.3
21
+ pytest-asyncio==0.25.3
22
+ ruff==0.9.6
23
23
  tox==4.24.1
24
- types-pyserial==3.5.0.20250124
24
+ types-pyserial==3.5.0.20250130
@@ -2,8 +2,8 @@
2
2
  [![PyPI version](https://badge.fury.io/py/PyPlumIO.svg)](https://badge.fury.io/py/PyPlumIO)
3
3
  [![PyPI Supported Python Versions](https://img.shields.io/pypi/pyversions/pyplumio.svg)](https://pypi.python.org/pypi/pyplumio/)
4
4
  [![PyPlumIO CI](https://github.com/denpamusic/PyPlumIO/actions/workflows/ci.yml/badge.svg)](https://github.com/denpamusic/PyPlumIO/actions/workflows/ci.yml)
5
- [![Maintainability](https://api.codeclimate.com/v1/badges/9f275fbc50fe9082a909/maintainability)](https://codeclimate.com/github/denpamusic/PyPlumIO/maintainability)
6
- [![Test Coverage](https://api.codeclimate.com/v1/badges/9f275fbc50fe9082a909/test_coverage)](https://codeclimate.com/github/denpamusic/PyPlumIO/test_coverage)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/e802127770476b7ba6fd/maintainability)](https://codeclimate.com/github/denpamusic/PyPlumIO/maintainability)
6
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/e802127770476b7ba6fd/test_coverage)](https://codeclimate.com/github/denpamusic/PyPlumIO/test_coverage)
7
7
  [![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg)](https://guidelines.denpa.pro/stability#release-candidate)
8
8
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
9
9
 
@@ -6,8 +6,9 @@
6
6
  Welcome to PyPlumIO's documentation!
7
7
  ====================================
8
8
 
9
- This project aims to provide complete and easy to use solution for communicating with
10
- climate devices by `Plum Sp. z o.o. <https://www.plum.pl/>`_
9
+ The `PyPlumIO <https://github.com/denpamusic/PyPlumIO/>`_ projects aims to
10
+ provide complete and easy to use solution for communicating with
11
+ climate devices manufactured by `Plum Sp. z o.o. <https://www.plum.pl/>`_
11
12
 
12
13
  Currently it supports reading and writing parameters of ecoMAX controllers by
13
14
  Plum Sp. z o.o., getting service password and sending network information to
@@ -42,9 +42,11 @@ temperature by 10 degrees Celsius.
42
42
  Setting Schedule
43
43
  ----------------
44
44
 
45
- To set the schedule, you can use ``set_state(state)``, ``set_on()`` or
46
- ``set_off()`` methods and call ``commit()`` to send changes to the
47
- device.
45
+ To set the schedule, you can either directly set the state via key or
46
+ by using ``set_state(state)``, ``set_on()`` or ``set_off()``.
47
+
48
+ After updating the state you must call ``commit()`` method to save
49
+ changes on the device.
48
50
 
49
51
  This example sets nighttime mode for Monday from 00:00 to 07:00 and
50
52
  switches back to daytime mode from 07:00 to 00:00.
@@ -57,14 +59,15 @@ switches back to daytime mode from 07:00 to 00:00.
57
59
  heating_schedule.monday.set_on(start="07:00", end="00:00")
58
60
  await heating_schedule.commit()
59
61
 
60
- For clarity sake, you might want to use ``STATE_NIGHT`` and
61
- ``STATE_DAY`` constants from ``pyplumio.helpers.schedule`` module.
62
+ For clarity sake, you might want to use ``STATE_OFF`` and
63
+ ``STATE_ON`` constants from ``pyplumio.helpers.schedule`` module.
62
64
 
63
65
  .. code-block:: python
64
66
 
65
- from pyplumio.helpers.schedule import STATE_NIGHT
67
+ from pyplumio.helpers.schedule import STATE_OFF
66
68
 
67
- heating_schedule.monday.set_state(STATE_NIGHT, "00:00", "07:00")
69
+ heating_schedule.monday["18:00"] = STATE_OFF
70
+ heating_schedule.monday.set_state(STATE_OFF, "00:00", "07:00")
68
71
 
69
72
  You may also omit one of the boundaries.
70
73
  The other boundary is then set to the end or start of the day.
@@ -107,7 +110,7 @@ Schedule Examples
107
110
  .. code-block:: python
108
111
 
109
112
  import pyplumio
110
- from pyplumio.helpers.schedule import STATE_DAY, STATE_NIGHT
113
+ from pyplumio.helpers.schedule import STATE_ON, STATE_OFF
111
114
 
112
115
 
113
116
  async def main():
@@ -124,12 +127,13 @@ Schedule Examples
124
127
  await ecomax.set("schedule_heating_parameter", 10)
125
128
 
126
129
  for weekday in heating_schedule:
127
- weekday.set_state(STATE_DAY, "00:00", "00:30")
128
- weekday.set_state(STATE_NIGHT, "00:30", "09:00")
129
- weekday.set_state(STATE_DAY, "09:00", "00:00")
130
+ weekday.set_state(STATE_ON, "00:00", "00:30")
131
+ weekday.set_state(STATE_OFF, "00:30", "09:00")
132
+ weekday.set_state(STATE_ON, "09:00", "00:00")
133
+ weekday["19:00"] = STATE_OFF
130
134
 
131
135
  # There will be no nighttime mode on sunday.
132
- heating_schedule.sunday.set_state(STATE_DAY)
136
+ heating_schedule.sunday.set_state(STATE_ON)
133
137
 
134
138
  await heating_schedule.commit()
135
139
 
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '0.5.36'
16
- __version_tuple__ = version_tuple = (0, 5, 36)
20
+ __version__ = version = '0.5.38'
21
+ __version_tuple__ = version_tuple = (0, 5, 38)
@@ -3,11 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from enum import Enum, IntEnum, unique
6
- from typing import Any, Final
6
+ from typing import Any, Final, Literal
7
7
 
8
- # Binary states.
9
- STATE_ON: Final = "on"
10
- STATE_OFF: Final = "off"
8
+ from typing_extensions import TypeAlias
11
9
 
12
10
  # General attributes.
13
11
  ATTR_CONNECTED: Final = "connected"
@@ -221,3 +219,9 @@ class UnitOfMeasurement(Enum):
221
219
 
222
220
 
223
221
  PERCENTAGE: Final = "%"
222
+
223
+ STATE_ON: Final = "on"
224
+ STATE_OFF: Final = "off"
225
+
226
+
227
+ State: TypeAlias = Literal["on", "off"]
@@ -21,7 +21,7 @@ from pyplumio.devices.mixer import Mixer
21
21
  from pyplumio.devices.thermostat import Thermostat
22
22
  from pyplumio.filters import on_change
23
23
  from pyplumio.frames import DataFrameDescription, Frame, Request
24
- from pyplumio.helpers.parameter import ParameterValues
24
+ from pyplumio.helpers.parameter import STATE_OFF, STATE_ON, ParameterValues, State
25
25
  from pyplumio.helpers.schedule import Schedule, ScheduleDay
26
26
  from pyplumio.structures.alerts import ATTR_TOTAL_ALERTS
27
27
  from pyplumio.structures.ecomax_parameters import (
@@ -99,8 +99,6 @@ SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
99
99
 
100
100
  _LOGGER = logging.getLogger(__name__)
101
101
 
102
- ecomax_control_error = "ecoMAX control is not available. Please try again later."
103
-
104
102
 
105
103
  class EcoMAX(PhysicalDevice):
106
104
  """Represents an ecoMAX controller."""
@@ -282,13 +280,13 @@ class EcoMAX(PhysicalDevice):
282
280
  SCHEDULES[index]: Schedule(
283
281
  name=SCHEDULES[index],
284
282
  device=self,
285
- monday=ScheduleDay(schedule[1]),
286
- tuesday=ScheduleDay(schedule[2]),
287
- wednesday=ScheduleDay(schedule[3]),
288
- thursday=ScheduleDay(schedule[4]),
289
- friday=ScheduleDay(schedule[5]),
290
- saturday=ScheduleDay(schedule[6]),
291
- sunday=ScheduleDay(schedule[0]),
283
+ monday=ScheduleDay.from_iterable(schedule[1]),
284
+ tuesday=ScheduleDay.from_iterable(schedule[2]),
285
+ wednesday=ScheduleDay.from_iterable(schedule[3]),
286
+ thursday=ScheduleDay.from_iterable(schedule[4]),
287
+ friday=ScheduleDay.from_iterable(schedule[5]),
288
+ saturday=ScheduleDay.from_iterable(schedule[6]),
289
+ sunday=ScheduleDay.from_iterable(schedule[0]),
292
290
  )
293
291
  for index, schedule in schedules
294
292
  }
@@ -398,23 +396,23 @@ class EcoMAX(PhysicalDevice):
398
396
 
399
397
  return False
400
398
 
401
- async def turn_on(self) -> bool:
402
- """Turn on the ecoMAX controller."""
399
+ async def _set_ecomax_state(self, state: State) -> bool:
400
+ """Try to set the ecoMAX control state."""
403
401
  try:
404
- ecomax_control: EcomaxSwitch = self.data[ATTR_ECOMAX_CONTROL]
405
- return await ecomax_control.turn_on()
402
+ switch: EcomaxSwitch = self.data[ATTR_ECOMAX_CONTROL]
403
+ return await switch.set(state)
406
404
  except KeyError:
407
- _LOGGER.error(ecomax_control_error)
408
- return False
405
+ _LOGGER.error("ecoMAX control is not available. Please try again later.")
406
+
407
+ return False
408
+
409
+ async def turn_on(self) -> bool:
410
+ """Turn on the ecoMAX controller."""
411
+ return await self._set_ecomax_state(STATE_ON)
409
412
 
410
413
  async def turn_off(self) -> bool:
411
414
  """Turn off the ecoMAX controller."""
412
- try:
413
- ecomax_control: EcomaxSwitch = self.data[ATTR_ECOMAX_CONTROL]
414
- return await ecomax_control.turn_off()
415
- except KeyError:
416
- _LOGGER.error(ecomax_control_error)
417
- return False
415
+ return await self._set_ecomax_state(STATE_OFF)
418
416
 
419
417
  def turn_on_nowait(self) -> None:
420
418
  """Turn on the ecoMAX controller without waiting."""
@@ -19,7 +19,7 @@ async def _import_module(name: str) -> ModuleType:
19
19
  return await loop.run_in_executor(None, importlib.import_module, f"pyplumio.{name}")
20
20
 
21
21
 
22
- async def create_instance(class_path: str, cls: type[T], **kwargs: Any) -> T:
22
+ async def create_instance(class_path: str, /, cls: type[T], **kwargs: Any) -> T:
23
23
  """Return a class instance from the class path."""
24
24
  module_name, class_name = class_path.rsplit(".", 1)
25
25
  try:
@@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, get_args
11
11
  from dataslots import dataslots
12
12
  from typing_extensions import TypeAlias
13
13
 
14
- from pyplumio.const import BYTE_UNDEFINED, STATE_OFF, STATE_ON, UnitOfMeasurement
14
+ from pyplumio.const import BYTE_UNDEFINED, STATE_OFF, STATE_ON, State, UnitOfMeasurement
15
15
  from pyplumio.frames import Request
16
16
 
17
17
  if TYPE_CHECKING:
@@ -19,8 +19,8 @@ if TYPE_CHECKING:
19
19
 
20
20
  _LOGGER = logging.getLogger(__name__)
21
21
 
22
+
22
23
  NumericType: TypeAlias = Union[int, float]
23
- State: TypeAlias = Literal["on", "off"]
24
24
  ParameterT = TypeVar("ParameterT", bound="Parameter")
25
25
 
26
26
 
@@ -28,7 +28,7 @@ def unpack_parameter(
28
28
  data: bytearray, offset: int = 0, size: int = 1
29
29
  ) -> ParameterValues | None:
30
30
  """Unpack a device parameter."""
31
- if not validate_parameter(data[offset : offset + size * 3]):
31
+ if not is_valid_parameter(data[offset : offset + size * 3]):
32
32
  return None
33
33
 
34
34
  value = data[offset : offset + size]
@@ -42,7 +42,7 @@ def unpack_parameter(
42
42
  )
43
43
 
44
44
 
45
- def validate_parameter(data: bytearray) -> bool:
45
+ def is_valid_parameter(data: bytearray) -> bool:
46
46
  """Check if parameter contains any bytes besides 0xFF."""
47
47
  return any(x for x in data if x != BYTE_UNDEFINED)
48
48
 
@@ -75,6 +75,7 @@ class ParameterDescription:
75
75
  """Represents a parameter description."""
76
76
 
77
77
  name: str
78
+ optimistic: bool = False
78
79
 
79
80
 
80
81
  class Parameter(ABC):
@@ -107,7 +108,7 @@ class Parameter(ABC):
107
108
  """Return a serializable string representation."""
108
109
  return (
109
110
  f"{self.__class__.__name__}("
110
- f"device={self.device.__class__.__name__}, "
111
+ f"device={self.device}, "
111
112
  f"description={self.description}, "
112
113
  f"values={self.values}, "
113
114
  f"index={self._index})"
@@ -204,13 +205,21 @@ class Parameter(ABC):
204
205
  self, value: int, retries: int = 5, timeout: float = 5.0
205
206
  ) -> bool:
206
207
  """Attempt to update a parameter value on the remote device."""
208
+ _LOGGER.debug(
209
+ "Attempting to update '%s' parameter to %d", self.description.name, value
210
+ )
207
211
  if value == self.values.value:
208
212
  # Value is unchanged
209
213
  return True
210
214
 
211
- self._pending_update = True
212
215
  self._values.value = value
213
- initial_retries = retries
216
+ request = await self.create_request()
217
+ if self.description.optimistic or not (initial_retries := retries):
218
+ # No retries
219
+ await self.device.queue.put(request)
220
+ return True
221
+
222
+ self._pending_update = True
214
223
  while self.pending_update:
215
224
  if retries <= 0:
216
225
  _LOGGER.warning(
@@ -220,7 +229,7 @@ class Parameter(ABC):
220
229
  )
221
230
  return False
222
231
 
223
- await self.device.queue.put(await self.create_request())
232
+ await self.device.queue.put(request)
224
233
  await asyncio.sleep(timeout)
225
234
  retries -= 1
226
235
 
@@ -0,0 +1,176 @@
1
+ """Contains a schedule helper classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Iterator, MutableMapping
6
+ from dataclasses import dataclass
7
+ import datetime as dt
8
+ from functools import lru_cache
9
+ from typing import Annotated, Final, get_args
10
+
11
+ from pyplumio.const import STATE_OFF, STATE_ON, FrameType, State
12
+ from pyplumio.devices import PhysicalDevice
13
+ from pyplumio.frames import Request
14
+ from pyplumio.structures.schedules import collect_schedule_data
15
+
16
+ TIME_FORMAT: Final = "%H:%M"
17
+
18
+ MIDNIGHT: Final = "00:00"
19
+ MIDNIGHT_DT = dt.datetime.strptime(MIDNIGHT, TIME_FORMAT)
20
+
21
+ STEP = dt.timedelta(minutes=30)
22
+
23
+ Time = Annotated[str, "Time string in %H:%M format"]
24
+
25
+
26
+ def _get_time(
27
+ index: int, start: dt.datetime = MIDNIGHT_DT, step: dt.timedelta = STEP
28
+ ) -> Time:
29
+ """Return time for a specific index."""
30
+ time_dt = start + (step * index)
31
+ return time_dt.strftime(TIME_FORMAT)
32
+
33
+
34
+ @lru_cache(maxsize=10)
35
+ def _get_time_range(start: Time, end: Time, step: dt.timedelta = STEP) -> list[Time]:
36
+ """Get a time range.
37
+
38
+ Start and end boundaries should be specified in %H:%M format.
39
+ Both are inclusive.
40
+ """
41
+ start_dt = dt.datetime.strptime(start, TIME_FORMAT)
42
+ end_dt = dt.datetime.strptime(end, TIME_FORMAT)
43
+
44
+ if end_dt == MIDNIGHT_DT:
45
+ # Upper boundary of the interval is midnight.
46
+ end_dt += dt.timedelta(hours=24) - step
47
+
48
+ if end_dt <= start_dt:
49
+ raise ValueError(
50
+ f"Invalid time range: start time ({start}) must be earlier "
51
+ f"than end time ({end})."
52
+ )
53
+
54
+ seconds = (end_dt - start_dt).total_seconds()
55
+ steps = seconds // step.total_seconds() + 1
56
+
57
+ return [_get_time(index, start=start_dt, step=step) for index in range(int(steps))]
58
+
59
+
60
+ class ScheduleDay(MutableMapping):
61
+ """Represents a single day of schedule."""
62
+
63
+ __slots__ = ("_schedule",)
64
+
65
+ _schedule: dict[Time, bool]
66
+
67
+ def __init__(self, schedule: dict[Time, bool]) -> None:
68
+ """Initialize a new schedule day."""
69
+ self._schedule = schedule
70
+
71
+ def __repr__(self) -> str:
72
+ """Return serializable representation of the class."""
73
+ return f"ScheduleDay({self._schedule})"
74
+
75
+ def __len__(self) -> int:
76
+ """Return a schedule length."""
77
+ return self._schedule.__len__()
78
+
79
+ def __iter__(self) -> Iterator[Time]:
80
+ """Return an iterator."""
81
+ return self._schedule.__iter__()
82
+
83
+ def __getitem__(self, time: Time) -> State:
84
+ """Return a schedule item."""
85
+ state = self._schedule.__getitem__(time)
86
+ return STATE_ON if state else STATE_OFF
87
+
88
+ def __delitem__(self, time: Time) -> None:
89
+ """Delete a schedule item."""
90
+ self._schedule.__delitem__(time)
91
+
92
+ def __setitem__(self, time: Time, state: State | bool) -> None:
93
+ """Set a schedule item."""
94
+ if state in get_args(State):
95
+ state = True if state == STATE_ON else False
96
+ if isinstance(state, bool):
97
+ self._schedule.__setitem__(time, state)
98
+ else:
99
+ raise TypeError(
100
+ f"Expected boolean value or one of: {', '.join(get_args(State))}."
101
+ )
102
+
103
+ def set_state(
104
+ self, state: State | bool, start: Time = MIDNIGHT, end: Time = MIDNIGHT
105
+ ) -> None:
106
+ """Set a schedule interval state."""
107
+ for time in _get_time_range(start, end):
108
+ self.__setitem__(time, state)
109
+
110
+ def set_on(self, start: Time = MIDNIGHT, end: Time = MIDNIGHT) -> None:
111
+ """Set a schedule interval state to 'on'."""
112
+ self.set_state(STATE_ON, start, end)
113
+
114
+ def set_off(self, start: Time = MIDNIGHT, end: Time = MIDNIGHT) -> None:
115
+ """Set a schedule interval state to 'off'."""
116
+ self.set_state(STATE_OFF, start, end)
117
+
118
+ @property
119
+ def schedule(self) -> dict[Time, bool]:
120
+ """Return the schedule."""
121
+ return self._schedule
122
+
123
+ @classmethod
124
+ def from_iterable(cls: type[ScheduleDay], intervals: Iterable[bool]) -> ScheduleDay:
125
+ """Make schedule day from iterable."""
126
+ return cls({_get_time(index): state for index, state in enumerate(intervals)})
127
+
128
+
129
+ @dataclass
130
+ class Schedule(Iterable):
131
+ """Represents a weekly schedule."""
132
+
133
+ __slots__ = (
134
+ "name",
135
+ "device",
136
+ "sunday",
137
+ "monday",
138
+ "tuesday",
139
+ "wednesday",
140
+ "thursday",
141
+ "friday",
142
+ "saturday",
143
+ )
144
+
145
+ name: str
146
+ device: PhysicalDevice
147
+
148
+ sunday: ScheduleDay
149
+ monday: ScheduleDay
150
+ tuesday: ScheduleDay
151
+ wednesday: ScheduleDay
152
+ thursday: ScheduleDay
153
+ friday: ScheduleDay
154
+ saturday: ScheduleDay
155
+
156
+ def __iter__(self) -> Iterator[ScheduleDay]:
157
+ """Return list of days."""
158
+ return (
159
+ self.sunday,
160
+ self.monday,
161
+ self.tuesday,
162
+ self.wednesday,
163
+ self.thursday,
164
+ self.friday,
165
+ self.saturday,
166
+ ).__iter__()
167
+
168
+ async def commit(self) -> None:
169
+ """Commit a weekly schedule to the device."""
170
+ await self.device.queue.put(
171
+ await Request.create(
172
+ FrameType.REQUEST_SET_SCHEDULE,
173
+ recipient=self.device.address,
174
+ data=collect_schedule_data(self.name, self.device),
175
+ )
176
+ )
@@ -10,8 +10,8 @@ POLYNOMIAL: Final = 0xA001
10
10
  BASE5_KEY: Final = "0123456789ABCDEFGHIJKLMNZPQRSTUV"
11
11
 
12
12
 
13
- def decode_uid(buffer: bytes) -> str:
14
- """Decode an UID string."""
13
+ def unpack_uid(buffer: bytes) -> str:
14
+ """Unpack UID from bytes."""
15
15
  return _base5(buffer + _crc16(buffer))
16
16
 
17
17
 
@@ -17,7 +17,7 @@ from pyplumio.utils import ensure_dict
17
17
  ATTR_ALERTS: Final = "alerts"
18
18
  ATTR_TOTAL_ALERTS: Final = "total_alerts"
19
19
 
20
- MAX_UINT32: Final = 4294967295
20
+ MAX_UINT32: Final = 0xFFFFFFFF
21
21
 
22
22
 
23
23
  class DateTimeInterval(NamedTuple):
@@ -825,7 +825,9 @@ ECOMAX_PARAMETERS: dict[ProductType, tuple[EcomaxParameterDescription, ...]] = {
825
825
  ),
826
826
  }
827
827
 
828
- ECOMAX_CONTROL_PARAMETER = EcomaxSwitchDescription(name=ATTR_ECOMAX_CONTROL)
828
+ ECOMAX_CONTROL_PARAMETER = EcomaxSwitchDescription(
829
+ name=ATTR_ECOMAX_CONTROL, optimistic=True
830
+ )
829
831
  THERMOSTAT_PROFILE_PARAMETER = EcomaxNumberDescription(name=ATTR_THERMOSTAT_PROFILE)
830
832
 
831
833
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import math
6
5
  from typing import Any, Final
7
6
 
8
7
  from pyplumio.helpers.data_types import UnsignedInt
@@ -60,7 +59,7 @@ class OutputsStructure(StructureDecoder):
60
59
  ensure_dict(
61
60
  data,
62
61
  {
63
- output: bool(outputs.value & int(math.pow(2, index)))
62
+ output: bool(outputs.value & 2**index)
64
63
  for index, output in enumerate(OUTPUTS)
65
64
  },
66
65
  ),