PyPlumIO 0.5.45__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.
- {pyplumio-0.5.45 → pyplumio-0.5.47}/PKG-INFO +2 -2
- {pyplumio-0.5.45 → pyplumio-0.5.47}/PyPlumIO.egg-info/PKG-INFO +2 -2
- {pyplumio-0.5.45 → pyplumio-0.5.47}/PyPlumIO.egg-info/requires.txt +1 -1
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/_version.py +2 -2
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/devices/__init__.py +5 -1
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/devices/ecomax.py +3 -10
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/helpers/factory.py +3 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/helpers/schedule.py +4 -3
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/helpers/timeout.py +1 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyproject.toml +1 -1
- {pyplumio-0.5.45 → pyplumio-0.5.47}/requirements_test.txt +1 -1
- pyplumio-0.5.47/tests/__init__.py +1 -0
- pyplumio-0.5.47/tests/conftest.py +153 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/frames/test_init.py +18 -8
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/frames/test_messages.py +10 -16
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/frames/test_requests.py +1 -25
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/frames/test_responses.py +1 -19
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/helpers/test_event_manager.py +2 -1
- pyplumio-0.5.47/tests/helpers/test_factory.py +41 -0
- pyplumio-0.5.47/tests/helpers/test_schedule.py +191 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/helpers/test_task_manager.py +7 -7
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/helpers/test_timeout.py +8 -10
- pyplumio-0.5.47/tests/helpers/test_uid.py +17 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/parameters/test_init.py +16 -8
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/parameters/test_thermostats.py +1 -1
- pyplumio-0.5.47/tests/test_connection.py +224 -0
- pyplumio-0.5.47/tests/test_data_types.py +87 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/test_devices.py +297 -225
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/test_filters.py +26 -25
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/test_main.py +3 -1
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/test_protocol.py +4 -4
- pyplumio-0.5.47/tests/test_stream.py +253 -0
- pyplumio-0.5.47/tests/test_utils.py +58 -0
- pyplumio-0.5.47/tests/testdata/unknown/unknown_ecomax_parameter.json +20 -0
- pyplumio-0.5.47/tests/testdata/unknown/unknown_mixer_parameter.json +11 -0
- pyplumio-0.5.45/tests/__init__.py +0 -56
- pyplumio-0.5.45/tests/conftest.py +0 -41
- pyplumio-0.5.45/tests/helpers/test_factory.py +0 -35
- pyplumio-0.5.45/tests/helpers/test_schedule.py +0 -150
- pyplumio-0.5.45/tests/helpers/test_uid.py +0 -17
- pyplumio-0.5.45/tests/test_connection.py +0 -250
- pyplumio-0.5.45/tests/test_data_types.py +0 -299
- pyplumio-0.5.45/tests/test_stream.py +0 -186
- pyplumio-0.5.45/tests/test_utils.py +0 -34
- pyplumio-0.5.45/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -18
- pyplumio-0.5.45/tests/testdata/unknown/unknown_mixer_parameter.json +0 -9
- {pyplumio-0.5.45 → pyplumio-0.5.47}/.gitattributes +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/.github/CODE_OF_CONDUCT.md +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/.github/dependabot.yml +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/.github/workflows/ci.yml +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/.github/workflows/codeql.yml +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/.github/workflows/deploy.yml +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/.github/workflows/documentation.yml +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/.gitignore +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/.pre-commit-config.yaml +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/.qlty/qlty.toml +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/.vscode/settings.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/LICENSE +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/MANIFEST.in +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/PyPlumIO.egg-info/SOURCES.txt +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/PyPlumIO.egg-info/dependency_links.txt +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/PyPlumIO.egg-info/top_level.txt +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/README.md +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/docs/Makefile +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/docs/make.bat +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/docs/source/callbacks.rst +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/docs/source/conf.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/docs/source/connecting.rst +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/docs/source/frames.rst +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/docs/source/index.rst +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/docs/source/mixers_thermostats.rst +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/docs/source/protocol.rst +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/docs/source/reading.rst +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/docs/source/schedules.rst +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/docs/source/writing.rst +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/images/ecomax.png +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/images/rs485.png +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/__init__.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/__main__.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/connection.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/const.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/data_types.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/devices/ecoster.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/devices/mixer.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/devices/thermostat.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/exceptions.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/filters.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/frames/__init__.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/frames/messages.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/frames/requests.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/frames/responses.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/helpers/__init__.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/helpers/async_cache.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/helpers/event_manager.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/helpers/task_manager.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/helpers/uid.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/parameters/__init__.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/parameters/ecomax.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/parameters/mixer.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/parameters/thermostat.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/protocol.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/py.typed +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/stream.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/__init__.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/alerts.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/boiler_load.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/boiler_power.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/ecomax_parameters.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/fan_power.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/frame_versions.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/fuel_consumption.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/fuel_level.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/lambda_sensor.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/mixer_parameters.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/mixer_sensors.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/modules.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/network_info.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/output_flags.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/outputs.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/pending_alerts.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/product_info.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/program_version.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/regulator_data.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/regulator_data_schema.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/schedules.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/statuses.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/temperatures.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/thermostat_parameters.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/structures/thermostat_sensors.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/pyplumio/utils.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/requirements.txt +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/requirements_docs.txt +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/setup.cfg +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/helpers/__init__.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/helpers/test_async_cache.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/parameters/__init__.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/parameters/test_ecomax.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/parameters/test_mixers.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/ruff.toml +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/test_init.py +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/messages/regulator_data.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/messages/sensor_data.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/requests/alerts.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/requests/ecomax_control.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/requests/ecomax_parameters.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/requests/mixer_parameters.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/requests/set_mixer_parameter.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/requests/set_schedule.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/requests/thermostat_parameters.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/responses/alerts.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/responses/device_available.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/responses/ecomax_parameters.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/responses/mixer_parameters.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/responses/password.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/responses/program_version.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/responses/regulator_data_schema.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/responses/schedules.json +0 -0
- {pyplumio-0.5.45 → pyplumio-0.5.47}/tests/testdata/responses/thermostat_parameters.json +0 -0
- {pyplumio-0.5.45 → 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.
|
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.
|
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.
|
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.
|
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
|
@@ -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={
|
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
|
|
@@ -12,6 +12,7 @@ from pyplumio.const import (
|
|
12
12
|
ATTR_FRAME_ERRORS,
|
13
13
|
ATTR_PASSWORD,
|
14
14
|
ATTR_SENSORS,
|
15
|
+
ATTR_SETUP,
|
15
16
|
STATE_OFF,
|
16
17
|
STATE_ON,
|
17
18
|
DeviceState,
|
@@ -192,16 +193,8 @@ class EcoMAX(PhysicalDevice):
|
|
192
193
|
self, frame_type: FrameType | int, version: int
|
193
194
|
) -> None:
|
194
195
|
"""Request frame version from the device."""
|
195
|
-
|
196
|
-
if frame_type not in REQUIRED_TYPES:
|
197
|
-
# Frame not in required, so we'll request it right away.
|
198
|
-
request_frame = True
|
199
|
-
|
200
|
-
if frame_type in self._frame_versions:
|
201
|
-
# Frame in required and has been requested once by setup.
|
202
|
-
request_frame = True
|
203
|
-
|
204
|
-
if request_frame:
|
196
|
+
setup_done = self.get_nowait(ATTR_SETUP, False)
|
197
|
+
if setup_done or frame_type not in REQUIRED_TYPES:
|
205
198
|
await super()._request_frame_version(frame_type, version)
|
206
199
|
|
207
200
|
async def _set_ecomax_state(self, state: State) -> bool:
|
@@ -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
|
-
|
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
|
@@ -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
|
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
|
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
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
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
|
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
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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)
|