PyPlumIO 0.5.46__tar.gz → 0.5.47__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 (163) hide show
  1. {pyplumio-0.5.46 → pyplumio-0.5.47}/PKG-INFO +2 -2
  2. {pyplumio-0.5.46 → pyplumio-0.5.47}/PyPlumIO.egg-info/PKG-INFO +2 -2
  3. {pyplumio-0.5.46 → pyplumio-0.5.47}/PyPlumIO.egg-info/requires.txt +1 -1
  4. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/_version.py +2 -2
  5. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/devices/__init__.py +5 -1
  6. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/helpers/factory.py +3 -0
  7. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/helpers/schedule.py +4 -3
  8. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/helpers/timeout.py +1 -0
  9. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyproject.toml +1 -1
  10. {pyplumio-0.5.46 → pyplumio-0.5.47}/requirements_test.txt +1 -1
  11. pyplumio-0.5.47/tests/__init__.py +1 -0
  12. pyplumio-0.5.47/tests/conftest.py +153 -0
  13. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/frames/test_init.py +18 -8
  14. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/frames/test_messages.py +10 -16
  15. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/frames/test_requests.py +1 -25
  16. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/frames/test_responses.py +1 -19
  17. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/helpers/test_event_manager.py +2 -1
  18. pyplumio-0.5.47/tests/helpers/test_factory.py +41 -0
  19. pyplumio-0.5.47/tests/helpers/test_schedule.py +191 -0
  20. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/helpers/test_task_manager.py +7 -7
  21. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/helpers/test_timeout.py +8 -10
  22. pyplumio-0.5.47/tests/helpers/test_uid.py +17 -0
  23. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/parameters/test_init.py +16 -8
  24. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/parameters/test_thermostats.py +1 -1
  25. pyplumio-0.5.47/tests/test_connection.py +224 -0
  26. pyplumio-0.5.47/tests/test_data_types.py +87 -0
  27. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/test_devices.py +296 -225
  28. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/test_filters.py +26 -25
  29. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/test_main.py +3 -1
  30. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/test_protocol.py +4 -4
  31. pyplumio-0.5.47/tests/test_stream.py +253 -0
  32. pyplumio-0.5.47/tests/test_utils.py +58 -0
  33. pyplumio-0.5.47/tests/testdata/unknown/unknown_ecomax_parameter.json +20 -0
  34. pyplumio-0.5.47/tests/testdata/unknown/unknown_mixer_parameter.json +11 -0
  35. pyplumio-0.5.46/tests/__init__.py +0 -56
  36. pyplumio-0.5.46/tests/conftest.py +0 -41
  37. pyplumio-0.5.46/tests/helpers/test_factory.py +0 -35
  38. pyplumio-0.5.46/tests/helpers/test_schedule.py +0 -150
  39. pyplumio-0.5.46/tests/helpers/test_uid.py +0 -17
  40. pyplumio-0.5.46/tests/test_connection.py +0 -250
  41. pyplumio-0.5.46/tests/test_data_types.py +0 -299
  42. pyplumio-0.5.46/tests/test_stream.py +0 -186
  43. pyplumio-0.5.46/tests/test_utils.py +0 -34
  44. pyplumio-0.5.46/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -18
  45. pyplumio-0.5.46/tests/testdata/unknown/unknown_mixer_parameter.json +0 -9
  46. {pyplumio-0.5.46 → pyplumio-0.5.47}/.gitattributes +0 -0
  47. {pyplumio-0.5.46 → pyplumio-0.5.47}/.github/CODE_OF_CONDUCT.md +0 -0
  48. {pyplumio-0.5.46 → pyplumio-0.5.47}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  49. {pyplumio-0.5.46 → pyplumio-0.5.47}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  50. {pyplumio-0.5.46 → pyplumio-0.5.47}/.github/dependabot.yml +0 -0
  51. {pyplumio-0.5.46 → pyplumio-0.5.47}/.github/workflows/ci.yml +0 -0
  52. {pyplumio-0.5.46 → pyplumio-0.5.47}/.github/workflows/codeql.yml +0 -0
  53. {pyplumio-0.5.46 → pyplumio-0.5.47}/.github/workflows/deploy.yml +0 -0
  54. {pyplumio-0.5.46 → pyplumio-0.5.47}/.github/workflows/documentation.yml +0 -0
  55. {pyplumio-0.5.46 → pyplumio-0.5.47}/.gitignore +0 -0
  56. {pyplumio-0.5.46 → pyplumio-0.5.47}/.pre-commit-config.yaml +0 -0
  57. {pyplumio-0.5.46 → pyplumio-0.5.47}/.qlty/qlty.toml +0 -0
  58. {pyplumio-0.5.46 → pyplumio-0.5.47}/.vscode/settings.json +0 -0
  59. {pyplumio-0.5.46 → pyplumio-0.5.47}/LICENSE +0 -0
  60. {pyplumio-0.5.46 → pyplumio-0.5.47}/MANIFEST.in +0 -0
  61. {pyplumio-0.5.46 → pyplumio-0.5.47}/PyPlumIO.egg-info/SOURCES.txt +0 -0
  62. {pyplumio-0.5.46 → pyplumio-0.5.47}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  63. {pyplumio-0.5.46 → pyplumio-0.5.47}/PyPlumIO.egg-info/top_level.txt +0 -0
  64. {pyplumio-0.5.46 → pyplumio-0.5.47}/README.md +0 -0
  65. {pyplumio-0.5.46 → pyplumio-0.5.47}/docs/Makefile +0 -0
  66. {pyplumio-0.5.46 → pyplumio-0.5.47}/docs/make.bat +0 -0
  67. {pyplumio-0.5.46 → pyplumio-0.5.47}/docs/source/callbacks.rst +0 -0
  68. {pyplumio-0.5.46 → pyplumio-0.5.47}/docs/source/conf.py +0 -0
  69. {pyplumio-0.5.46 → pyplumio-0.5.47}/docs/source/connecting.rst +0 -0
  70. {pyplumio-0.5.46 → pyplumio-0.5.47}/docs/source/frames.rst +0 -0
  71. {pyplumio-0.5.46 → pyplumio-0.5.47}/docs/source/index.rst +0 -0
  72. {pyplumio-0.5.46 → pyplumio-0.5.47}/docs/source/mixers_thermostats.rst +0 -0
  73. {pyplumio-0.5.46 → pyplumio-0.5.47}/docs/source/protocol.rst +0 -0
  74. {pyplumio-0.5.46 → pyplumio-0.5.47}/docs/source/reading.rst +0 -0
  75. {pyplumio-0.5.46 → pyplumio-0.5.47}/docs/source/schedules.rst +0 -0
  76. {pyplumio-0.5.46 → pyplumio-0.5.47}/docs/source/writing.rst +0 -0
  77. {pyplumio-0.5.46 → pyplumio-0.5.47}/images/ecomax.png +0 -0
  78. {pyplumio-0.5.46 → pyplumio-0.5.47}/images/rs485.png +0 -0
  79. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/__init__.py +0 -0
  80. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/__main__.py +0 -0
  81. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/connection.py +0 -0
  82. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/const.py +0 -0
  83. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/data_types.py +0 -0
  84. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/devices/ecomax.py +0 -0
  85. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/devices/ecoster.py +0 -0
  86. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/devices/mixer.py +0 -0
  87. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/devices/thermostat.py +0 -0
  88. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/exceptions.py +0 -0
  89. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/filters.py +0 -0
  90. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/frames/__init__.py +0 -0
  91. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/frames/messages.py +0 -0
  92. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/frames/requests.py +0 -0
  93. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/frames/responses.py +0 -0
  94. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/helpers/__init__.py +0 -0
  95. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/helpers/async_cache.py +0 -0
  96. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/helpers/event_manager.py +0 -0
  97. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/helpers/task_manager.py +0 -0
  98. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/helpers/uid.py +0 -0
  99. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/parameters/__init__.py +0 -0
  100. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/parameters/ecomax.py +0 -0
  101. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/parameters/mixer.py +0 -0
  102. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/parameters/thermostat.py +0 -0
  103. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/protocol.py +0 -0
  104. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/py.typed +0 -0
  105. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/stream.py +0 -0
  106. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/__init__.py +0 -0
  107. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/alerts.py +0 -0
  108. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/boiler_load.py +0 -0
  109. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/boiler_power.py +0 -0
  110. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/ecomax_parameters.py +0 -0
  111. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/fan_power.py +0 -0
  112. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/frame_versions.py +0 -0
  113. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/fuel_consumption.py +0 -0
  114. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/fuel_level.py +0 -0
  115. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/lambda_sensor.py +0 -0
  116. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/mixer_parameters.py +0 -0
  117. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/mixer_sensors.py +0 -0
  118. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/modules.py +0 -0
  119. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/network_info.py +0 -0
  120. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/output_flags.py +0 -0
  121. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/outputs.py +0 -0
  122. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/pending_alerts.py +0 -0
  123. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/product_info.py +0 -0
  124. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/program_version.py +0 -0
  125. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/regulator_data.py +0 -0
  126. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/regulator_data_schema.py +0 -0
  127. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/schedules.py +0 -0
  128. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/statuses.py +0 -0
  129. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/temperatures.py +0 -0
  130. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/thermostat_parameters.py +0 -0
  131. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/structures/thermostat_sensors.py +0 -0
  132. {pyplumio-0.5.46 → pyplumio-0.5.47}/pyplumio/utils.py +0 -0
  133. {pyplumio-0.5.46 → pyplumio-0.5.47}/requirements.txt +0 -0
  134. {pyplumio-0.5.46 → pyplumio-0.5.47}/requirements_docs.txt +0 -0
  135. {pyplumio-0.5.46 → pyplumio-0.5.47}/setup.cfg +0 -0
  136. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/helpers/__init__.py +0 -0
  137. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/helpers/test_async_cache.py +0 -0
  138. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/parameters/__init__.py +0 -0
  139. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/parameters/test_ecomax.py +0 -0
  140. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/parameters/test_mixers.py +0 -0
  141. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/ruff.toml +0 -0
  142. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/test_init.py +0 -0
  143. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/messages/regulator_data.json +0 -0
  144. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/messages/sensor_data.json +0 -0
  145. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/requests/alerts.json +0 -0
  146. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/requests/ecomax_control.json +0 -0
  147. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/requests/ecomax_parameters.json +0 -0
  148. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/requests/mixer_parameters.json +0 -0
  149. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  150. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  151. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/requests/set_schedule.json +0 -0
  152. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  153. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/requests/thermostat_parameters.json +0 -0
  154. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/responses/alerts.json +0 -0
  155. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/responses/device_available.json +0 -0
  156. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/responses/ecomax_parameters.json +0 -0
  157. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/responses/mixer_parameters.json +0 -0
  158. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/responses/password.json +0 -0
  159. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/responses/program_version.json +0 -0
  160. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/responses/regulator_data_schema.json +0 -0
  161. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/responses/schedules.json +0 -0
  162. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/responses/thermostat_parameters.json +0 -0
  163. {pyplumio-0.5.46 → pyplumio-0.5.47}/tests/testdata/responses/uid.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.5.46
3
+ Version: 0.5.47
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
@@ -35,7 +35,7 @@ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
35
35
  Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
36
36
  Requires-Dist: pytest==8.3.5; extra == "test"
37
37
  Requires-Dist: pytest-asyncio==0.26.0; extra == "test"
38
- Requires-Dist: ruff==0.11.7; extra == "test"
38
+ Requires-Dist: ruff==0.11.8; extra == "test"
39
39
  Requires-Dist: tox==4.25.0; extra == "test"
40
40
  Requires-Dist: types-pyserial==3.5.0.20250326; extra == "test"
41
41
  Provides-Extra: docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.5.46
3
+ Version: 0.5.47
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
@@ -35,7 +35,7 @@ Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
35
35
  Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
36
36
  Requires-Dist: pytest==8.3.5; extra == "test"
37
37
  Requires-Dist: pytest-asyncio==0.26.0; extra == "test"
38
- Requires-Dist: ruff==0.11.7; extra == "test"
38
+ Requires-Dist: ruff==0.11.8; extra == "test"
39
39
  Requires-Dist: tox==4.25.0; extra == "test"
40
40
  Requires-Dist: types-pyserial==3.5.0.20250326; extra == "test"
41
41
  Provides-Extra: docs
@@ -21,6 +21,6 @@ numpy<3.0.0,>=2.0.0
21
21
  pyserial-asyncio-fast==0.16
22
22
  pytest==8.3.5
23
23
  pytest-asyncio==0.26.0
24
- ruff==0.11.7
24
+ ruff==0.11.8
25
25
  tox==4.25.0
26
26
  types-pyserial==3.5.0.20250326
@@ -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.46'
21
- __version_tuple__ = version_tuple = (0, 5, 46)
20
+ __version__ = version = '0.5.47'
21
+ __version_tuple__ = version_tuple = (0, 5, 47)
@@ -39,7 +39,11 @@ def get_device_handler(device_type: int) -> str:
39
39
 
40
40
  type_name = to_camelcase(
41
41
  DeviceType(device_type).name,
42
- overrides={"ecomax": "EcoMAX", "ecoster": "EcoSTER"},
42
+ overrides={
43
+ "ecomax": "EcoMAX",
44
+ "ecoster": "EcoSTER",
45
+ "econet": "EcoNET",
46
+ },
43
47
  )
44
48
  return f"devices.{type_name.lower()}.{type_name}"
45
49
 
@@ -8,11 +8,14 @@ import logging
8
8
  from types import ModuleType
9
9
  from typing import Any, TypeVar
10
10
 
11
+ from pyplumio.helpers.async_cache import acache
12
+
11
13
  _LOGGER = logging.getLogger(__name__)
12
14
 
13
15
  T = TypeVar("T")
14
16
 
15
17
 
18
+ @acache
16
19
  async def import_module(name: str) -> ModuleType:
17
20
  """Import module by name."""
18
21
  loop = asyncio.get_running_loop()
@@ -15,13 +15,14 @@ from pyplumio.structures.schedules import collect_schedule_data
15
15
 
16
16
  TIME_FORMAT: Final = "%H:%M"
17
17
 
18
- MIDNIGHT: Final = "00:00"
18
+
19
+ Time = Annotated[str, "Time string in %H:%M format"]
20
+
21
+ MIDNIGHT: Final = Time("00:00")
19
22
  MIDNIGHT_DT = dt.datetime.strptime(MIDNIGHT, TIME_FORMAT)
20
23
 
21
24
  STEP = dt.timedelta(minutes=30)
22
25
 
23
- Time = Annotated[str, "Time string in %H:%M format"]
24
-
25
26
 
26
27
  def get_time(
27
28
  index: int, start: dt.datetime = MIDNIGHT_DT, step: dt.timedelta = STEP
@@ -23,6 +23,7 @@ def timeout(
23
23
  async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
24
24
  return await asyncio.wait_for(func(*args, **kwargs), timeout=seconds)
25
25
 
26
+ setattr(wrapper, "_has_timeout_seconds", seconds)
26
27
  return wrapper
27
28
 
28
29
  return decorator
@@ -45,7 +45,7 @@ test = [
45
45
  "pyserial-asyncio-fast==0.16",
46
46
  "pytest==8.3.5",
47
47
  "pytest-asyncio==0.26.0",
48
- "ruff==0.11.7",
48
+ "ruff==0.11.8",
49
49
  "tox==4.25.0",
50
50
  "types-pyserial==3.5.0.20250326"
51
51
  ]
@@ -7,7 +7,7 @@ pre-commit==4.2.0
7
7
  pyserial-asyncio-fast==0.16
8
8
  pytest-asyncio==0.26.0
9
9
  pytest==8.3.5
10
- ruff==0.11.7
10
+ ruff==0.11.8
11
11
  tomli==2.2.1
12
12
  tox==4.25.0
13
13
  types-pyserial==3.5.0.20250326
@@ -0,0 +1 @@
1
+ """Contains a test suite."""
@@ -0,0 +1,153 @@
1
+ """Contains fixtures for the test suite."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Sequence
7
+ import functools
8
+ import importlib
9
+ import inspect
10
+ import json
11
+ import os
12
+ import pathlib
13
+ from typing import Any, Final, TypeVar
14
+ from unittest.mock import patch
15
+
16
+ from freezegun import freeze_time
17
+ import pytest
18
+
19
+ from pyplumio.const import ProductType
20
+ from pyplumio.devices.ecomax import EcoMAX
21
+ from pyplumio.structures.network_info import NetworkInfo
22
+ from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
23
+
24
+ TESTDATA_DIR: Final = "testdata"
25
+ UNDEFINED: Final = "undefined"
26
+ RAISES: Final = "raises"
27
+ DEFAULT_TOLERANCE: Final = 1e-6
28
+
29
+ T = TypeVar("T")
30
+
31
+
32
+ def _create_class_instance(module_name: str, class_name: str, **kwargs):
33
+ """Create class instance and cache it."""
34
+ return getattr(importlib.import_module(module_name), class_name)(**kwargs)
35
+
36
+
37
+ def try_int(key: Any) -> Any:
38
+ """Try to convert key to integer or return key unchanged on error."""
39
+ try:
40
+ return int(key)
41
+ except ValueError:
42
+ return key
43
+
44
+
45
+ def _decode_hinted_objects(d: Any) -> Any:
46
+ """Decode a hinted JSON objects."""
47
+ if "__module__" in d and "__class__" in d:
48
+ module_name = d.pop("__module__")
49
+ class_name = d.pop("__class__")
50
+ return _create_class_instance(module_name, class_name, **d)
51
+
52
+ if "__bytearray__" in d:
53
+ return bytearray.fromhex("".join(d["items"]))
54
+
55
+ if "__tuple__" in d:
56
+ return tuple(d["items"])
57
+
58
+ return {try_int(k): v for k, v in d.items()}
59
+
60
+
61
+ def load_json_test_data(path: str) -> Any:
62
+ """Load test data from JSON file."""
63
+ abs_path = "/".join([os.path.dirname(__file__), TESTDATA_DIR, path])
64
+ file = pathlib.Path(abs_path)
65
+ with open(file, encoding="utf-8") as fp:
66
+ return json.load(fp, object_hook=_decode_hinted_objects)
67
+
68
+
69
+ def load_json_parameters(path: str):
70
+ """Prepare JSON test data for parametrization."""
71
+ test_data = load_json_test_data(path)
72
+ return [pytest.param(x["message"], x["data"], id=x["id"]) for x in test_data]
73
+
74
+
75
+ def _bypass_pytest_argument_inspection(
76
+ wrapper: T, signature: inspect.Signature, argument: str
77
+ ) -> T:
78
+ """Remove argument from pytest's parameter inspection."""
79
+ replacement = signature.replace(
80
+ parameters=[p for p in signature.parameters.values() if p.name != argument]
81
+ )
82
+ setattr(wrapper, "__signature__", replacement)
83
+ return wrapper
84
+
85
+
86
+ def json_test_data(json_path: str, selector: str | None = None, dataset: int = 0):
87
+ """Pytest decorator to inject JSON test data as a test argument."""
88
+ name = json_path.split("/")[-1].split("\\")[-1].rsplit(".", 1)[0]
89
+ if selector:
90
+ name += f"_{selector}"
91
+
92
+ def decorator(test_func):
93
+ @functools.wraps(test_func)
94
+ async def wrapper(*args, **kwargs):
95
+ json_test_data = load_json_test_data(json_path)[dataset]
96
+ kwargs[name] = json_test_data[selector] if selector else json_test_data
97
+ return await test_func(*args, **kwargs)
98
+
99
+ return _bypass_pytest_argument_inspection(
100
+ wrapper, inspect.signature(test_func), name
101
+ )
102
+
103
+ return decorator
104
+
105
+
106
+ def class_from_json(
107
+ cls: type, json_path: str, /, dataset: int = 0, arguments: Sequence | None = None
108
+ ):
109
+ """Pytest decorator to inject JSON test data as a test argument."""
110
+ name = json_path.split("/")[-1].split("\\")[-1].rsplit(".", 1)[0]
111
+ init_args = arguments if arguments else ()
112
+
113
+ def decorator(test_func):
114
+ @functools.wraps(test_func)
115
+ async def wrapper(*args, **kwargs):
116
+ json_test_data = load_json_test_data(json_path)[dataset]
117
+ kwargs[name] = cls(**{k: json_test_data[k] for k in init_args})
118
+ return await test_func(*args, **kwargs)
119
+
120
+ return _bypass_pytest_argument_inspection(
121
+ wrapper, inspect.signature(test_func), name
122
+ )
123
+
124
+ return decorator
125
+
126
+
127
+ @pytest.fixture(autouse=True)
128
+ def bypass_asyncio_sleep():
129
+ """Bypass an asyncio sleep."""
130
+ with patch("asyncio.sleep"):
131
+ yield
132
+
133
+
134
+ @pytest.fixture(name="ecomax")
135
+ def fixture_ecomax() -> EcoMAX:
136
+ """Return an ecoMAX object."""
137
+ ecomax = EcoMAX(asyncio.Queue(), network=NetworkInfo())
138
+ ecomax.data[ATTR_PRODUCT] = ProductInfo(
139
+ type=ProductType.ECOMAX_P,
140
+ id=90,
141
+ uid="TEST",
142
+ logo=23040,
143
+ image=2816,
144
+ model="ecoMAX 350P2-ZF",
145
+ )
146
+ return ecomax
147
+
148
+
149
+ @pytest.fixture(name="frozen_time")
150
+ def fixture_frozen_time():
151
+ """Get frozen time."""
152
+ with freeze_time("2012-12-12 12:00:00") as frozen_time:
153
+ yield frozen_time
@@ -2,9 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import ClassVar
6
-
7
5
  import pytest
6
+ from tests.conftest import RAISES
8
7
 
9
8
  from pyplumio.const import DeviceType, FrameType
10
9
  from pyplumio.exceptions import UnknownFrameError
@@ -23,13 +22,13 @@ from pyplumio.structures.program_version import ATTR_VERSION, VersionInfo
23
22
  class RequestFrame(Request):
24
23
  """Representation of a request frame."""
25
24
 
26
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_PROGRAM_VERSION
25
+ frame_type = FrameType.REQUEST_PROGRAM_VERSION
27
26
 
28
27
 
29
28
  class ResponseFrame(Response):
30
29
  """Representation of a response frame."""
31
30
 
32
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_PROGRAM_VERSION
31
+ frame_type = FrameType.RESPONSE_PROGRAM_VERSION
33
32
 
34
33
 
35
34
  @pytest.fixture(name="request_frame")
@@ -65,11 +64,22 @@ def test_decode_create_message(frames: tuple[Request, Response]) -> None:
65
64
  assert frame.decode_message(message=bytearray()) == {}
66
65
 
67
66
 
68
- def test_get_frame_handler() -> None:
67
+ @pytest.mark.parametrize(
68
+ ("frame_type", "handler"),
69
+ [
70
+ (FrameType.REQUEST_STOP_MASTER, "frames.requests.StopMasterRequest"),
71
+ (FrameType.RESPONSE_ECOMAX_CONTROL, "frames.responses.EcomaxControlResponse"),
72
+ (FrameType.MESSAGE_REGULATOR_DATA, "frames.messages.RegulatorDataMessage"),
73
+ (99, RAISES),
74
+ ],
75
+ )
76
+ def test_get_frame_handler(frame_type: FrameType | int, handler: str) -> None:
69
77
  """Test getting a frame handler."""
70
- assert get_frame_handler(0x18) == "frames.requests.StopMasterRequest"
71
- with pytest.raises(UnknownFrameError):
72
- get_frame_handler(0x0)
78
+ if handler == RAISES:
79
+ with pytest.raises(UnknownFrameError):
80
+ get_frame_handler(frame_type)
81
+ else:
82
+ assert get_frame_handler(frame_type) == handler
73
83
 
74
84
 
75
85
  def test_passing_frame_type(
@@ -3,23 +3,14 @@
3
3
  from typing import Final
4
4
 
5
5
  import pytest
6
- from tests import load_json_parameters, load_json_test_data
6
+ from tests.conftest import json_test_data, load_json_parameters, load_json_test_data
7
7
 
8
- from pyplumio.const import ATTR_SENSORS, ATTR_STATE, DeviceType
8
+ from pyplumio.const import ATTR_SENSORS, ATTR_STATE
9
9
  from pyplumio.devices.ecomax import EcoMAX
10
10
  from pyplumio.frames.messages import RegulatorDataMessage, SensorDataMessage
11
11
  from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS
12
12
  from pyplumio.structures.regulator_data import ATTR_REGDATA
13
13
 
14
- INDEX_STATE: Final = 22
15
-
16
-
17
- def test_messages_type() -> None:
18
- """Test if response is an instance of abstract frame class."""
19
- for response in (RegulatorDataMessage, SensorDataMessage):
20
- frame = response(recipient=DeviceType.ALL, sender=DeviceType.ECONET)
21
- assert isinstance(frame, response)
22
-
23
14
 
24
15
  @pytest.mark.parametrize(
25
16
  ("schema", "regdata"),
@@ -57,9 +48,12 @@ def test_sensor_data_message(message, data) -> None:
57
48
  assert SensorDataMessage(message=message).data == data
58
49
 
59
50
 
60
- def test_sensor_data_message_with_unknown_state() -> None:
51
+ INDEX_STATE: Final = 22
52
+
53
+
54
+ @json_test_data("messages/sensor_data.json", selector="message")
55
+ async def test_sensor_data_message_with_unknown_state(sensor_data_message) -> None:
61
56
  """Test a sensor data message with an unknown device state."""
62
- test_data = load_json_test_data("messages/sensor_data.json")[0]
63
- message = test_data["message"]
64
- message[INDEX_STATE] = 99
65
- assert SensorDataMessage(message=message).data[ATTR_SENSORS][ATTR_STATE] == 99
57
+ sensor_data_message[INDEX_STATE] = 99
58
+ sensor_data = SensorDataMessage(message=sensor_data_message)
59
+ assert sensor_data.data[ATTR_SENSORS][ATTR_STATE] == 99
@@ -1,7 +1,7 @@
1
1
  """Contains a tests for the request frame classes."""
2
2
 
3
3
  import pytest
4
- from tests import load_json_parameters
4
+ from tests.conftest import load_json_parameters
5
5
 
6
6
  from pyplumio.const import DeviceType
7
7
  from pyplumio.exceptions import FrameDataError
@@ -12,17 +12,12 @@ from pyplumio.frames.requests import (
12
12
  EcomaxControlRequest,
13
13
  EcomaxParametersRequest,
14
14
  MixerParametersRequest,
15
- PasswordRequest,
16
15
  ProgramVersionRequest,
17
- RegulatorDataSchemaRequest,
18
16
  SetEcomaxParameterRequest,
19
17
  SetMixerParameterRequest,
20
18
  SetScheduleRequest,
21
19
  SetThermostatParameterRequest,
22
- StartMasterRequest,
23
- StopMasterRequest,
24
20
  ThermostatParametersRequest,
25
- UIDRequest,
26
21
  )
27
22
  from pyplumio.frames.responses import DeviceAvailableResponse, ProgramVersionResponse
28
23
 
@@ -32,25 +27,6 @@ def test_request_class_response_property() -> None:
32
27
  assert Request().response() is None
33
28
 
34
29
 
35
- def test_request_type() -> None:
36
- """Test if request is an instance of frame class."""
37
- for request in (
38
- ProgramVersionRequest,
39
- CheckDeviceRequest,
40
- UIDRequest,
41
- PasswordRequest,
42
- EcomaxParametersRequest,
43
- MixerParametersRequest,
44
- RegulatorDataSchemaRequest,
45
- StartMasterRequest,
46
- StopMasterRequest,
47
- AlertsRequest,
48
- ThermostatParametersRequest,
49
- ):
50
- frame = request(recipient=DeviceType.ALL, sender=DeviceType.ECONET)
51
- assert isinstance(frame, request)
52
-
53
-
54
30
  def test_program_version_response_recipient_and_type() -> None:
55
31
  """Test if program version response recipient and type is set."""
56
32
  frame = ProgramVersionRequest(recipient=DeviceType.ALL, sender=DeviceType.ECONET)
@@ -1,9 +1,8 @@
1
1
  """Contains tests for the response frame classes."""
2
2
 
3
3
  import pytest
4
- from tests import load_json_parameters
4
+ from tests.conftest import load_json_parameters
5
5
 
6
- from pyplumio.const import DeviceType
7
6
  from pyplumio.devices.ecomax import EcoMAX
8
7
  from pyplumio.frames.responses import (
9
8
  AlertsResponse,
@@ -20,22 +19,6 @@ from pyplumio.frames.responses import (
20
19
  from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTATS_AVAILABLE
21
20
 
22
21
 
23
- def test_responses_type() -> None:
24
- """Test if response is an instance of frame class."""
25
- for response in (
26
- ProgramVersionResponse,
27
- DeviceAvailableResponse,
28
- UIDResponse,
29
- PasswordResponse,
30
- EcomaxParametersResponse,
31
- MixerParametersResponse,
32
- RegulatorDataSchemaResponse,
33
- AlertsResponse,
34
- ):
35
- frame = response(recipient=DeviceType.ALL, sender=DeviceType.ECONET)
36
- assert isinstance(frame, response)
37
-
38
-
39
22
  @pytest.mark.parametrize(
40
23
  ("message", "data"),
41
24
  load_json_parameters("responses/alerts.json"),
@@ -126,7 +109,6 @@ async def test_thermostat_parameters_response(ecomax: EcoMAX, message, data) ->
126
109
  frame.assign_to(ecomax)
127
110
  ecomax.load_nowait({ATTR_THERMOSTATS_AVAILABLE: 3})
128
111
  await ecomax.wait_until_done()
129
-
130
112
  assert frame.data == data
131
113
 
132
114
 
@@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, Mock, call, patch
5
5
 
6
6
  import pytest
7
7
 
8
+ from pyplumio.filters import Filter
8
9
  from pyplumio.helpers.event_manager import EventManager, event_listener
9
10
 
10
11
 
@@ -26,7 +27,7 @@ def test_register_event_listeners() -> None:
26
27
  # Create event listener with filter.
27
28
  mock_on_event_test2 = AsyncMock()
28
29
  setattr(mock_on_event_test2, "_on_event", "test2")
29
- mock_filter = Mock()
30
+ mock_filter = Mock(spec=Filter)
30
31
  mock_wrapper = Mock(return_value=mock_filter)
31
32
  setattr(mock_on_event_test2, "_on_event_filter", mock_wrapper)
32
33
 
@@ -0,0 +1,41 @@
1
+ """Contains tests for the object factory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ import pytest
8
+
9
+ from pyplumio.frames import Frame, Request, Response
10
+ from pyplumio.frames.messages import RegulatorDataMessage
11
+ from pyplumio.frames.requests import StopMasterRequest
12
+ from pyplumio.frames.responses import UIDResponse
13
+ from pyplumio.helpers.factory import create_instance
14
+ from tests.conftest import RAISES
15
+
16
+
17
+ @pytest.mark.parametrize(
18
+ ("path", "base", "expected", "error_pattern"),
19
+ [
20
+ ("frames.requests.StopMasterRequest", Request, StopMasterRequest, None),
21
+ ("frames.responses.UIDResponse", Response, UIDResponse, None),
22
+ ("frames.messages.RegulatorDataMessage", Frame, RegulatorDataMessage, None),
23
+ ("frames.responses.UIDResponse", Request, RAISES, "Expected instance"),
24
+ ("frames.requests.NonExistent", Request, RAISES, "no attribute"),
25
+ ("frames.request.StopMasterRequest", Request, RAISES, "No module"),
26
+ ],
27
+ )
28
+ async def test_create_instance(
29
+ path: str,
30
+ base: type,
31
+ expected: type | Literal["raises"],
32
+ error_pattern: str | None,
33
+ ) -> None:
34
+ """Test creating an instance of class."""
35
+ if expected == RAISES:
36
+ with pytest.raises(
37
+ (TypeError, AttributeError, ModuleNotFoundError), match=error_pattern
38
+ ):
39
+ await create_instance(path, cls=base)
40
+ else:
41
+ assert isinstance(await create_instance(path, cls=base), expected)