From d75cf62239b67d7cc554eeff9f2b00a533279f00 Mon Sep 17 00:00:00 2001 From: denpamusic Date: Wed, 1 Nov 2023 05:32:30 +0300 Subject: [PATCH] Improve tests. - Move test data into separate JSON files. - Improve test data. - Improve annotations. --- tests/__init__.py | 58 +- tests/conftest.py | 465 +----------- tests/frames/test_init.py | 38 +- tests/frames/test_messages.py | 211 ++---- tests/frames/test_requests.py | 122 +-- tests/frames/test_responses.py | 262 +++---- tests/helpers/__init__.py | 2 +- tests/helpers/test_data_types.py | 60 +- tests/helpers/test_event_manager.py | 20 +- tests/helpers/test_factory.py | 12 +- tests/helpers/test_parameter.py | 40 +- tests/helpers/test_schedule.py | 44 +- tests/helpers/test_task_manager.py | 10 +- tests/helpers/test_timeout.py | 4 +- tests/helpers/test_uid.py | 20 +- tests/test_connection.py | 18 +- tests/test_devices.py | 154 ++-- tests/test_filters.py | 2 +- tests/test_init.py | 2 +- tests/test_main.py | 2 +- tests/test_protocol.py | 2 +- tests/test_stream.py | 2 +- tests/test_utils.py | 2 +- tests/testdata/messages/regulator_data.json | 97 +++ tests/testdata/messages/sensor_data.json | 175 +++++ tests/testdata/requests/ecomax_control.json | 26 + .../testdata/requests/ecomax_parameters.json | 41 + .../requests/set_ecomax_parameter.json | 15 + .../requests/set_mixer_parameter.json | 30 + tests/testdata/requests/set_schedule.json | 369 +++++++++ .../requests/set_thermostat_parameter.json | 32 + tests/testdata/responses/alerts.json | 68 ++ tests/testdata/responses/data_schema.json | 481 ++++++++++++ .../testdata/responses/device_available.json | 35 + .../testdata/responses/ecomax_parameters.json | 714 ++++++++++++++++++ .../testdata/responses/mixer_parameters.json | 107 +++ tests/testdata/responses/password.json | 26 + tests/testdata/responses/program_version.json | 18 + tests/testdata/responses/schedules.json | 402 ++++++++++ .../responses/thermostat_parameters.json | 199 +++++ tests/testdata/responses/uid.json | 23 + 41 files changed, 3357 insertions(+), 1053 deletions(-) create mode 100644 tests/testdata/messages/regulator_data.json create mode 100644 tests/testdata/messages/sensor_data.json create mode 100644 tests/testdata/requests/ecomax_control.json create mode 100644 tests/testdata/requests/ecomax_parameters.json create mode 100644 tests/testdata/requests/set_ecomax_parameter.json create mode 100644 tests/testdata/requests/set_mixer_parameter.json create mode 100644 tests/testdata/requests/set_schedule.json create mode 100644 tests/testdata/requests/set_thermostat_parameter.json create mode 100644 tests/testdata/responses/alerts.json create mode 100644 tests/testdata/responses/data_schema.json create mode 100644 tests/testdata/responses/device_available.json create mode 100644 tests/testdata/responses/ecomax_parameters.json create mode 100644 tests/testdata/responses/mixer_parameters.json create mode 100644 tests/testdata/responses/password.json create mode 100644 tests/testdata/responses/program_version.json create mode 100644 tests/testdata/responses/schedules.json create mode 100644 tests/testdata/responses/thermostat_parameters.json create mode 100644 tests/testdata/responses/uid.json diff --git a/tests/__init__.py b/tests/__init__.py index b4a5ed89..99bb51fc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,57 @@ -"""Contains test suite.""" +"""Contains a test suite.""" + + +import importlib +import json +import os +import pathlib +from typing import Final + +import pytest + +TESTDATA_DIR: Final = "testdata" + + +def load_and_create_class_instance(module_name: str, class_name: str, **kwargs): + """Load a class and creates it's instance.""" + return getattr(importlib.import_module(module_name), class_name)(**kwargs) + + +def try_int(key): + """Try to convert key to integer or return key unchanged + on error. + """ + try: + return int(key) + except ValueError: + return key + + +def decode_hinted_objects(d): + """Decode a hinted JSON objects.""" + if "__module__" in d and "__class__" in d: + module_name = d.pop("__module__") + class_name = d.pop("__class__") + return load_and_create_class_instance(module_name, class_name, **d) + + if "__bytearray__" in d: + return bytearray.fromhex("".join(d["items"])) + + if "__tuple__" in d: + return tuple(d["items"]) + + return {try_int(k): v for k, v in d.items()} + + +def load_json_test_data(path: str): + """Load test data from JSON file.""" + abs_path = "/".join([os.path.dirname(__file__), TESTDATA_DIR, path]) + file = pathlib.Path(abs_path) + with open(file, encoding="utf-8") as fp: + return json.load(fp, object_hook=decode_hinted_objects) + + +def load_json_parameters(path: str): + """Prepare JSON test data for parametrization.""" + test_data = load_json_test_data(path) + return [pytest.param(x["message"], x["data"], id=x["id"]) for x in test_data] diff --git a/tests/conftest.py b/tests/conftest.py index 4b877e3c..0f1004b8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,474 +1,19 @@ -"""Fixtures for PyPlumIO test suite.""" +"""Contains fixtures for the test suite.""" import asyncio -from datetime import datetime from unittest.mock import patch import pytest -from pyplumio.const import ( - ATTR_DEVICE_INDEX, - ATTR_INDEX, - ATTR_OFFSET, - ATTR_PARAMETER, - ATTR_PASSWORD, - ATTR_SCHEDULE, - ATTR_SIZE, - ATTR_SWITCH, - ATTR_TYPE, - ATTR_VALUE, - AlertType, - FrameType, - ProductType, -) +from pyplumio.const import ProductType from pyplumio.devices.ecomax import EcoMAX -from pyplumio.helpers.parameter import ParameterValues -from pyplumio.helpers.typing import EventDataType -from pyplumio.structures.alerts import ATTR_ALERTS, Alert -from pyplumio.structures.ecomax_parameters import ATTR_ECOMAX_PARAMETERS -from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS -from pyplumio.structures.modules import ConnectedModules -from pyplumio.structures.network_info import ( - ATTR_NETWORK, - EthernetParameters, - NetworkInfo, - WirelessParameters, -) +from pyplumio.structures.network_info import NetworkInfo from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo -from pyplumio.structures.program_version import ATTR_VERSION, VersionInfo -from pyplumio.structures.schedules import ATTR_SCHEDULE_PARAMETERS, ATTR_SCHEDULES -from pyplumio.structures.thermostat_parameters import ( - ATTR_THERMOSTAT_PARAMETERS, - ATTR_THERMOSTAT_PROFILE, -) -from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_COUNT - -TEST_SCHEDULE: list[bool] = [ - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - False, -] - - -@pytest.fixture(name="data") -def fixture_data() -> dict[FrameType, EventDataType]: - """Return response data keyed by frame type.""" - return { - FrameType.RESPONSE_PROGRAM_VERSION: { - ATTR_VERSION: VersionInfo(software="1.0.0") - }, - FrameType.RESPONSE_DEVICE_AVAILABLE: { - ATTR_NETWORK: NetworkInfo( - eth=EthernetParameters( - ip="192.168.1.2", - netmask="255.255.255.0", - gateway="192.168.1.1", - status=True, - ), - wlan=WirelessParameters( - ip="192.168.2.2", - netmask="255.255.255.0", - gateway="192.168.2.1", - status=True, - ssid="tests", - ), - ) - }, - FrameType.RESPONSE_UID: { - ATTR_PRODUCT: ProductInfo( - type=ProductType.ECOMAX_P, - id=90, - uid="D251PAKR3GCPZ1K8G05G0", - logo=23040, - image=2816, - model="EM350P2-ZF", - ) - }, - FrameType.RESPONSE_PASSWORD: {ATTR_PASSWORD: "0000"}, - FrameType.RESPONSE_ECOMAX_PARAMETERS: { - ATTR_ECOMAX_PARAMETERS: [ - (0, ParameterValues(61, 61, 100)), - (1, ParameterValues(60, 41, 60)), - (2, ParameterValues(40, 20, 59)), - (14, ParameterValues(20, 1, 250)), - (15, ParameterValues(3, 1, 30)), - (16, ParameterValues(1, 1, 30)), - (17, ParameterValues(5, 1, 30)), - (18, ParameterValues(1, 0, 1)), - (19, ParameterValues(0, 0, 60)), - (20, ParameterValues(60, 0, 100)), - (23, ParameterValues(20, 10, 100)), - (35, ParameterValues(30, 20, 100)), - (36, ParameterValues(4, 1, 30)), - (37, ParameterValues(0, 0, 100)), - (38, ParameterValues(8, 1, 250)), - (39, ParameterValues(50, 40, 85)), - (40, ParameterValues(10, 10, 30)), - (41, ParameterValues(30, 20, 50)), - (44, ParameterValues(10, 10, 240)), - (47, ParameterValues(15, 10, 20)), - (50, ParameterValues(50, 40, 150)), - (53, ParameterValues(2, 1, 15)), - (54, ParameterValues(3, 1, 10)), - (55, ParameterValues(40, 20, 60)), - (60, ParameterValues(60, 1, 250)), - (61, ParameterValues(30, 20, 50)), - (85, ParameterValues(125, 1, 250)), - (87, ParameterValues(2, 1, 100)), - (88, ParameterValues(47, 1, 250)), - (89, ParameterValues(10, 10, 30)), - (98, ParameterValues(65, 50, 80)), - (99, ParameterValues(50, 30, 80)), - (100, ParameterValues(80, 60, 90)), - (101, ParameterValues(50, 30, 80)), - (102, ParameterValues(0, 0, 99)), - (105, ParameterValues(5, 3, 15)), - (106, ParameterValues(0, 0, 1)), - (107, ParameterValues(13, 1, 40)), - (108, ParameterValues(20, 0, 40)), - (111, ParameterValues(0, 0, 1)), - (112, ParameterValues(5, 0, 30)), - (114, ParameterValues(90, 85, 95)), - (115, ParameterValues(60, 40, 90)), - (119, ParameterValues(51, 40, 70)), - (120, ParameterValues(40, 20, 55)), - (121, ParameterValues(70, 40, 80)), - (122, ParameterValues(2, 0, 2)), - (123, ParameterValues(10, 1, 30)), - (124, ParameterValues(0, 0, 1)), - (125, ParameterValues(0, 0, 2)), - (126, ParameterValues(16, 5, 30)), - (127, ParameterValues(10, 1, 15)), - (128, ParameterValues(3, 0, 99)), - ] - }, - FrameType.RESPONSE_MIXER_PARAMETERS: { - ATTR_MIXER_PARAMETERS: { - 0: [ - (0, ParameterValues(40, 30, 60)), - (1, ParameterValues(20, 30, 40)), - (2, ParameterValues(80, 70, 90)), - (3, ParameterValues(20, 10, 30)), - (4, ParameterValues(1, 0, 1)), - (5, ParameterValues(13, 10, 30)), - ], - } - }, - FrameType.RESPONSE_THERMOSTAT_PARAMETERS: { - ATTR_THERMOSTAT_COUNT: 3, - ATTR_THERMOSTAT_PROFILE: ParameterValues(0, 0, 5), - ATTR_THERMOSTAT_PARAMETERS: { - 0: [ - (0, ParameterValues(0, 0, 7)), - (1, ParameterValues(220, 100, 350)), - (2, ParameterValues(150, 100, 350)), - (3, ParameterValues(100, 60, 140)), - (4, ParameterValues(2, 0, 60)), - (5, ParameterValues(1, 0, 60)), - (6, ParameterValues(1, 0, 60)), - (7, ParameterValues(10, 0, 60)), - (8, ParameterValues(9, 0, 50)), - (9, ParameterValues(222, 100, 350)), - (10, ParameterValues(212, 100, 350)), - (11, ParameterValues(90, 50, 300)), - ] - }, - }, - FrameType.RESPONSE_ALERTS: { - ATTR_ALERTS: [ - Alert( - code=26, - from_dt=datetime(2022, 7, 23, 16, 27), - to_dt=datetime(2022, 7, 23, 16, 32, 27), - ), - Alert( - code=AlertType.POWER_LOSS, - from_dt=datetime(2022, 7, 22, 22, 33), - to_dt=None, - ), - ] - }, - FrameType.RESPONSE_SCHEDULES: { - ATTR_SCHEDULES: [(0, [TEST_SCHEDULE] * 7)], - ATTR_SCHEDULE_PARAMETERS: [ - (0, ParameterValues(0, 0, 1)), - (1, ParameterValues(5, 0, 30)), - ], - }, - FrameType.REQUEST_SET_ECOMAX_PARAMETER: { - ATTR_INDEX: 0, - ATTR_VALUE: 80, - }, - FrameType.REQUEST_SET_MIXER_PARAMETER: { - ATTR_INDEX: 0, - ATTR_VALUE: 40, - ATTR_DEVICE_INDEX: 0, - }, - FrameType.REQUEST_SET_THERMOSTAT_PARAMETER: { - ATTR_INDEX: 1, - ATTR_VALUE: 42, - ATTR_OFFSET: 12, - ATTR_SIZE: 2, - }, - FrameType.REQUEST_ECOMAX_CONTROL: {ATTR_VALUE: 1}, - FrameType.REQUEST_SET_SCHEDULE: { - ATTR_TYPE: "heating", - ATTR_SWITCH: 0, - ATTR_PARAMETER: 5, - ATTR_SCHEDULE: [TEST_SCHEDULE] * 7, - }, - FrameType.MESSAGE_SENSOR_DATA: { - "sensors": { - "frame_versions": { - 85: 45559, - 84: 48672, - 86: 64152, - 54: 1, - 56: 2, - 57: 1, - 61: 12568, - }, - "state": 0, - "fan": False, - "feeder": False, - "heating_pump": False, - "water_heater_pump": False, - "circulation_pump": False, - "lighter": False, - "alarm": False, - "outer_boiler": False, - "fan2_exhaust": False, - "feeder2": False, - "outer_feeder": False, - "solar_pump": False, - "fireplace_pump": False, - "gcz_contact": False, - "blow_fan1": False, - "blow_fan2": False, - "heating_pump_flag": True, - "water_heater_pump_flag": True, - "circulation_pump_flag": True, - "solar_pump_flag": False, - "heating_temp": 22.384185791015625, - "optical_temp": 0.0, - "heating_target": 41, - "heating_status": 0, - "water_heater_target": 45, - "water_heater_status": 128, - "pending_alerts": 0, - "fuel_level": 32, - "transmission": 0, - "fan_power": 0.0, - "load": 0, - "power": 0.0, - "fuel_consumption": 0.0, - "thermostat": 1, - "modules": ConnectedModules( - module_a="18.11.58.K1", - module_b=None, - module_c=None, - ecolambda=None, - ecoster=None, - panel="18.10.72", - ), - "lambda_sensor": {"state": 1, "target": 2, "level": 40}, - "thermostat_sensors": [ - ( - 0, - { - "state": 3, - "current_temp": 43.5, - "target_temp": 50.0, - "contacts": True, - "schedule": False, - }, - ) - ], - "thermostat_count": 1, - "mixer_sensors": [ - (4, {"current_temp": 20.0, "target_temp": 40, "pump": False}) - ], - "mixer_count": 5, - } - }, - } - - -@pytest.fixture(name="messages") -def fixture_messages() -> dict[FrameType, bytearray]: - """Return response messages keyed by frame type.""" - return { - FrameType.RESPONSE_PROGRAM_VERSION: bytearray.fromhex( - "FFFF057A0000000001000000000056" - ), - FrameType.RESPONSE_DEVICE_AVAILABLE: bytearray.fromhex( - """01C0A80102FFFFFF00C0A8010101C0A80202FFFFFF00C0A80201010164010000000005746 -5737473""".replace( - "\n", "" - ) - ), - FrameType.RESPONSE_UID: bytearray.fromhex( - "005A000B001600110D3833383655395A0000000A454D33353050322D5A46" - ), - FrameType.RESPONSE_PASSWORD: bytearray.fromhex("0430303030"), - FrameType.RESPONSE_ECOMAX_PARAMETERS: bytearray.fromhex( - """00008b3d3d643c293c28143bfffffffffffffffffffffffffffffffffffffffffffffffff -fffffffffffffffff1401fa03011e01011e05011e01000100003c3c0064ffffffffffff140a64fffffffffff -fffffffffffffffffffffffffffffffffffffffffffffffffffffff1e146404011e0000640801fa3228550a0 -a1e1e1432ffffffffffff0a0af0ffffffffffff0f0a14ffffffffffff322896ffffffffffff02010f03010a2 -8143cffffffffffffffffffffffff3c01fa1e1432fffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -fff7d01faffffff0201642f01fa0a0a1effffffffffffffffffffffffffffffffffffffffffffffff4132503 -21e50503c5a321e50000063ffffffffffff05030f0000010d0128140028ffffffffffff00000105001efffff -f5a555f3c285affffffffffffffffff3328462814374628500200020a011e00000100000210051e0a010f030 -063ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff""".replace( - "\n", "" - ) - ), - FrameType.RESPONSE_MIXER_PARAMETERS: bytearray.fromhex( - "00000601281E3C141E2850465A140A1E0100010D0A1E" - ), - FrameType.RESPONSE_THERMOSTAT_PARAMETERS: bytearray.fromhex( - """000025000005000007DC0064005E01960064005E01643C8C02003C01003C01003C0A003C0 -90032DE0064005E01D40064005E015A0032002C01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF""".replace( - "\n", "" - ) - ), - FrameType.RESPONSE_DATA_SCHEMA: bytearray.fromhex( - """01010400070A02060A00060A01060A02000A01000A0 -3060A07060A05060A06060A08060A09060A0A060A03000A04060A0B060A0C060A0D060A0E060A0F060A10060 -A04000A11060A11060A12060A13060A14060A15060A16060A0500050800050B00050C00050D00050E00050A0 -0050900050700050F00051200050600051000051100050600050600051400051500051600051700051800051 -900051A00070104070704070304071C00070604070204070004071B000704040705040706000709040708040 -70600070600071E00071F00070B04070A0407200007210007220004010504070504030504240004060504020 -5040005042300040405040505040600040905040805040600040600042600042700040B05040A05042800042 -900042A00042C00042F00043000043100043200042E00042D00042B000433000436000406000434000435000 -40600040600043800043900043A00043B00043C00043D00043E000A40000A43000A44000A45000A46000A420 -00A41000A3F000A47000A53000A53000A48000A49000A4A000A53000A4C000A4D000A4E000A4F000A50000A5 -1000A52000A53000A53000A53000A53000A53000A53000A53000A53000A55000A56000A57000A58000A59000 -A5A000A5B000A5C000A5D000A5E000A5F000A5F000A5F000A5F000A5F000A5F000461000462000400080A630 -00A64000401080A66000A67000A68000A69000A6A000A6B000A6C000A6D000A6E000A6F000A70000A71000A7 -2000A72000A72000A72000473000474000402070A75000A76000A06070A77000A53000A78000A79000A53000 -A7A000A7B000A7C000A7D000A7E000A7F0004800004830004840004910004920004930004940004950004960 -0049700049800049900049A000A9B000A9C000A9D000A9E000A9F0004A40004A50004A60004A00004A10007A -20007A30004030704600007010707650004AA0004AD0005B00005B10005B20005B30005B50005B60007AB000 -5AC0006AE0006AF000FB7000FB8000FB90004BA000FBB000FBC000FBD0004BE0004BF0004C00004C10004C20 -00CC300 - """.replace( - "\n", "" - ) - ), - FrameType.RESPONSE_ALERTS: bytearray.fromhex( - "6400021a5493382B9B94382B009C97372B00000000" - ), - FrameType.RESPONSE_SCHEDULES: bytearray.fromhex( - """100101000005001E0000FFFFFFFE0000FFFFFFFE0000FFFFFFFE0000FFFFFFFE0000FFFFF -FFE0000FFFFFFFE0000FFFFFFFE -""".replace( - "\n", "" - ) - ), - FrameType.MESSAGE_REGULATOR_DATA: bytearray.fromhex( - """626400010855F7B15420BE6101003D183136010064010040041C5698FA0000000000FF0FF -F0FFF0FFF0FFF0FFF0F9F04080FFF0FFF0F0000000000000000000000000000000000000000000000000000C -07F0000C07F0000C07F0000C07F0000C07F0000C07FD012B341000000000000C07F0000C07F0000C07F0000C -07F0000C07F0000C07F0000C07F0000C07F0000C07F0000C07F0000C07F0000C07F0000C07F0000C07F2D280 -000000029000000002828000000002828000000800000000000000000000000000000000000000000003FFF7 -F00000000200000000000404000403F124B01000000000000000000000002020100000000000000000000000 -000000000000000000000150009001A000D000C001D00000000000000000000000000000000000000FFFFFF0 -0000000000000000000FFFFFF0000000000010164000000 -""".replace( - "\n", "" - ) - ), - FrameType.MESSAGE_SENSOR_DATA: bytearray.fromhex( - """0755F7B15420BE5698FA3601003802003901003D18310000000000FF0300000900D012B34 -101FFFFFFFF02FFFFFFFF03FFFFFFFF04FFFFFFFF05FFFFFFFF060000000007FFFFFFFF08FFFFFFFF29002D8 -00020000000000000000000000000000001120B3A4B01FFFFFFFF120A480102280005010300002E420000484 -205FFFFFFFF28000800FFFFFFFF28000800FFFFFFFF28000800FFFFFFFF280008000000A04128000800 -""".replace( - "\n", "" - ) - ), - FrameType.REQUEST_ECOMAX_PARAMETERS: bytearray.fromhex("FF00"), - FrameType.REQUEST_SET_ECOMAX_PARAMETER: bytearray.fromhex("0050"), - FrameType.REQUEST_SET_MIXER_PARAMETER: bytearray.fromhex("000028"), - FrameType.REQUEST_SET_THERMOSTAT_PARAMETER: bytearray.fromhex("0d2a00"), - FrameType.REQUEST_ECOMAX_CONTROL: bytearray.fromhex("01"), - FrameType.REQUEST_SET_SCHEDULE: bytearray.fromhex( - """010000050000FFFFFFFE0000FFFFFFFE0000FFFFFFFE0000FFFFFFFE0000FFFFFFFE0000F -FFFFFFE0000FFFFFFFE -""".replace( - "\n", "" - ) - ), - } - - -@pytest.fixture(name="sensor_data_without_thermostats") -def fixture_sensor_data_without_thermostats() -> bytearray: - """Return device sensor data without thermostat data.""" - return bytearray.fromhex( - """0755F7B15420BE5698FA3601003802003901003D18310000000000FF0300000900D012B34101F -FFFFFFF02FFFFFFFF03FFFFFFFF04FFFFFFFF05FFFFFFFF060000000007FFFFFFFF08FFFFFFFF29002D80002 -0000000000000000000000000000001120B3A4B01FFFFFFFF120A4801022800FF05FFFFFFFF28000800FFFFF -FFF28000800FFFFFFFF28000800FFFFFFFF28000800FFFFFFFF28000800 - """.replace( - "\n", "" - ) - ) @pytest.fixture(name="ecomax") def fixture_ecomax() -> EcoMAX: - """Return instance of ecomax.""" + """Return an ecoMAX object.""" ecomax = EcoMAX(asyncio.Queue(), network=NetworkInfo()) ecomax.data[ATTR_PRODUCT] = ProductInfo( type=ProductType.ECOMAX_P, @@ -483,6 +28,6 @@ def fixture_ecomax() -> EcoMAX: @pytest.fixture(autouse=True) def bypass_asyncio_sleep(): - """Bypass asyncio sleep.""" + """Bypass an asyncio sleep.""" with patch("asyncio.sleep"): yield diff --git a/tests/frames/test_init.py b/tests/frames/test_init.py index 8afc8335..6a4e2b3c 100644 --- a/tests/frames/test_init.py +++ b/tests/frames/test_init.py @@ -1,4 +1,4 @@ -"""Test PyPlumIO base frame.""" +"""Contains tests for the abstract frame class.""" from typing import ClassVar, Final @@ -20,26 +20,26 @@ class RequestFrame(Request): - """Test request class.""" + """Representation of a request frame.""" frame_type: ClassVar[int] = FrameType.REQUEST_PROGRAM_VERSION class ResponseFrame(Response): - """Test response class.""" + """Representation of a response frame.""" frame_type: ClassVar[int] = FrameType.RESPONSE_PROGRAM_VERSION @pytest.fixture(name="request_frame") def fixture_request_frame() -> Request: - """Return program version request.""" + """Return a request frame object.""" return RequestFrame() @pytest.fixture(name="response_frame") def fixture_response_frame() -> Response: - """Return program version response.""" + """Return a response frame object.""" return ResponseFrame() @@ -47,7 +47,7 @@ def fixture_response_frame() -> Response: def fixture_frames( request_frame: Request, response_frame: Response ) -> tuple[Request, Response]: - """Return request and response frames as a tuple.""" + """Return both request and response frame objects as a tuple.""" return (request_frame, response_frame) @@ -58,21 +58,21 @@ def fixture_types() -> tuple[int, int]: def test_unknown_device_type() -> None: - """Test creating a frame with unknown device type.""" + """Test creating frame with an unknown device type.""" frame = RequestFrame(sender=UNKNOWN_DEVICE) assert frame.sender == UNKNOWN_DEVICE assert not isinstance(frame.sender, DeviceType) def test_decode_create_message(frames: tuple[Request, Response]) -> None: - """Test creating and decoding message.""" + """Test creating and decoding frame message.""" for frame in frames: assert frame.create_message(data={}) == bytearray() assert frame.decode_message(message=bytearray()) == {} def test_get_frame_handler() -> None: - """Test getting frame handler.""" + """Test getting a frame handler.""" assert get_frame_handler(0x18) == "frames.requests.StopMasterRequest" with pytest.raises(UnknownFrameError): get_frame_handler(0x0) @@ -81,13 +81,13 @@ def test_get_frame_handler() -> None: def test_passing_frame_type( frames: tuple[Request, Response], types: tuple[int, int] ) -> None: - """Test getting frame type.""" + """Test getting a frame type.""" for index, frame in enumerate(frames): assert frame.frame_type == types[index] -def test_default_params(frames: tuple[Request, Response]) -> None: - """Test frame attributes.""" +def test_frame_attributes(frames: tuple[Request, Response]) -> None: + """Test accessing frame attributes.""" for frame in frames: assert frame.recipient == DeviceType.ALL assert frame.message == b"" @@ -97,20 +97,20 @@ def test_default_params(frames: tuple[Request, Response]) -> None: def test_frame_length_without_data(frames: tuple[Request, Response]) -> None: - """Test frame length without any data.""" + """Test a frame length without any data.""" for frame in frames: assert frame.length == HEADER_SIZE + 3 assert len(frame) == HEADER_SIZE + 3 def test_get_header(frames: tuple[Request, Response]) -> None: - """Test getting frame header as bytes.""" + """Test getting a frame header as bytes.""" for frame in frames: assert frame.header == b"\x68\x0a\x00\x00\x56\x30\x05" def test_base_class_with_message() -> None: - """Test base request class with message.""" + """Test base request class with a message.""" frame = RequestFrame(message=bytearray.fromhex("B00B")) assert frame.message == b"\xB0\x0B" @@ -129,12 +129,12 @@ def test_to_hex() -> None: def test_equality() -> None: - """Test equality check.""" + """Test frame objects equality.""" assert ProgramVersionResponse() == ProgramVersionResponse() -def test_request_framerepr(request_frame: Request) -> None: - """Test serialiazible request representation.""" +def test_request_repr(request_frame: Request) -> None: + """Test a request class serialiazible representation.""" repr_string = ( "RequestFrame(recipient=, " + "sender=, sender_type=48, econet_version=5, " @@ -144,7 +144,7 @@ def test_request_framerepr(request_frame: Request) -> None: def test_response_repr(response_frame: Response) -> None: - """Test serialiazible response representation.""" + """Test a response class serialiazible representation.""" repr_string = ( "ResponseFrame(recipient=, " + "sender=, sender_type=48, econet_version=5, " diff --git a/tests/frames/test_messages.py b/tests/frames/test_messages.py index 4c651c68..20af9ca6 100644 --- a/tests/frames/test_messages.py +++ b/tests/frames/test_messages.py @@ -1,182 +1,63 @@ -"""Test PyPlumIO message frames.""" +"""Contains a tests for the message frame classes.""" + from typing import Final -from pyplumio.const import ( - ATTR_CURRENT_TEMP, - ATTR_SCHEDULE, - ATTR_SENSORS, - ATTR_STATE, - ATTR_TARGET_TEMP, - BYTE_UNDEFINED, - DeviceState, - DeviceType, - FrameType, -) +import pytest + +from pyplumio.const import ATTR_SENSORS, ATTR_STATE, DeviceType from pyplumio.frames.messages import RegulatorDataMessage, SensorDataMessage -from pyplumio.structures.fan_power import ATTR_FAN_POWER from pyplumio.structures.frame_versions import ATTR_FRAME_VERSIONS -from pyplumio.structures.fuel_consumption import ATTR_FUEL_CONSUMPTION -from pyplumio.structures.fuel_level import ATTR_FUEL_LEVEL -from pyplumio.structures.lambda_sensor import ( - ATTR_LAMBDA_LEVEL, - ATTR_LAMBDA_STATE, - ATTR_LAMBDA_TARGET, -) -from pyplumio.structures.load import ATTR_LOAD -from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS, ATTR_PUMP -from pyplumio.structures.modules import ATTR_MODULES -from pyplumio.structures.outputs import ATTR_HEATING_PUMP -from pyplumio.structures.pending_alerts import ATTR_PENDING_ALERTS -from pyplumio.structures.power import ATTR_POWER -from pyplumio.structures.regulator_data import ATTR_REGDATA_DECODER -from pyplumio.structures.statuses import ATTR_HEATING_STATUS, ATTR_HEATING_TARGET -from pyplumio.structures.temperatures import ATTR_HEATING_TEMP -from pyplumio.structures.thermostat_sensors import ( - ATTR_CONTACTS, - ATTR_THERMOSTAT_SENSORS, -) +from pyplumio.structures.regulator_data import ATTR_REGDATA, ATTR_REGDATA_DECODER +from tests import load_json_parameters, load_json_test_data -INDEX_VERSION_MINOR: Final = 3 -INDEX_VERSION_MAJOR: Final = 4 INDEX_STATE: Final = 22 -INDEX_FUEL_LEVEL: Final = 82 -INDEX_FAN_POWER: Final = 84 -INDEX_LOAD: Final = 88 -INDEX_POWER: Final = 89 -INDEX_FUEL_CONSUMPTION: Final = 93 -INDEX_LAMBDA_SENSOR: Final = 110 def test_messages_type() -> None: - """Test if response is instance of frame class.""" - for response in ( - RegulatorDataMessage, - SensorDataMessage, - ): + """Test if response is an instance of abstract frame class.""" + for response in (RegulatorDataMessage, SensorDataMessage): frame = response(recipient=DeviceType.ALL, sender=DeviceType.ECONET) assert isinstance(frame, response) -def test_regdata_decode_message(messages: dict[FrameType, bytearray]) -> None: - """Test parsing of regdata message.""" - frame = RegulatorDataMessage(message=messages[FrameType.MESSAGE_REGULATOR_DATA]) - decoder = frame.data[ATTR_REGDATA_DECODER] - data = decoder.decode(frame.message)[0] - assert ATTR_FRAME_VERSIONS in data - - -def test_regdata_decode_message_with_unknown_version( - messages: dict[FrameType, bytearray] -) -> None: - """Test parsing of regdata message with unknown message version.""" - test_message = messages[FrameType.MESSAGE_REGULATOR_DATA] - test_message[INDEX_VERSION_MAJOR], test_message[INDEX_VERSION_MINOR] = (2, 0) - frame = RegulatorDataMessage(message=test_message) +@pytest.mark.parametrize( + "schema, regdata", + zip( + load_json_test_data("responses/data_schema.json"), + load_json_test_data("messages/regulator_data.json"), + ), + ids=[ + "unknown_regulator_data_version", + "EM350P2_regulator_data", + "incomplete_boolean", + ], +) +async def test_regulator_data_message(schema, regdata) -> None: + """Test a regulator data message.""" + frame = RegulatorDataMessage(message=regdata["message"]) decoder = frame.data[ATTR_REGDATA_DECODER] - assert not decoder.decode(frame.message)[0] - - -def test_sensor_data_decode_message(messages: dict[FrameType, bytearray]) -> None: - """Test parsing sensor data message.""" - test_message = messages[FrameType.MESSAGE_SENSOR_DATA] - frame = SensorDataMessage(message=test_message) - data = frame.data[ATTR_SENSORS] - assert ATTR_FRAME_VERSIONS in data - assert data[ATTR_FRAME_VERSIONS][85] == 45559 - assert len(data[ATTR_FRAME_VERSIONS]) == 7 - assert data[ATTR_STATE] == DeviceState.OFF - assert round(data[ATTR_HEATING_TEMP], 2) == 22.38 - assert data[ATTR_HEATING_TARGET] == 41 - assert not data[ATTR_HEATING_PUMP] - assert data[ATTR_HEATING_STATUS] == 0 - assert data[ATTR_MODULES].module_a == "18.11.58.K1" - assert data[ATTR_MODULES].panel == "18.10.72" - assert data[ATTR_LAMBDA_LEVEL] == 4.0 - assert data[ATTR_PENDING_ALERTS] == 0 - assert data[ATTR_FUEL_LEVEL] == 32 - assert data[ATTR_MIXER_SENSORS] == { - 4: { - ATTR_CURRENT_TEMP: 20.0, - ATTR_TARGET_TEMP: 40, - ATTR_PUMP: False, - }, - } - assert data[ATTR_THERMOSTAT_SENSORS] == { - 0: { - ATTR_STATE: 3, - ATTR_CURRENT_TEMP: 43.5, - ATTR_TARGET_TEMP: 50.0, - ATTR_CONTACTS: True, - ATTR_SCHEDULE: False, - }, - } - - # Test with extra state. - test_message[INDEX_STATE] = 12 - frame = SensorDataMessage(message=test_message) - assert frame.data[ATTR_SENSORS][ATTR_STATE] == DeviceState.STABILIZATION - - # Test with the unknown state. - test_message[INDEX_STATE] = 99 - frame = SensorDataMessage(message=test_message) - assert frame.data[ATTR_SENSORS][ATTR_STATE] == 99 - - -def test_sensor_data_without_fuel_level_and_load( - messages: dict[FrameType, bytearray] -) -> None: - """Test that fuel level and load keys are not present in the device data - if they are unavailable. - """ - test_message = messages[FrameType.MESSAGE_SENSOR_DATA] - for index in (INDEX_FUEL_LEVEL, INDEX_LOAD): - test_message[index] = BYTE_UNDEFINED - - frame = SensorDataMessage(message=test_message) - assert ATTR_FUEL_LEVEL not in frame.data - assert ATTR_LOAD not in frame.data - - -def test_sensor_data_without_lambda_sensor( - messages: dict[FrameType, bytearray] -) -> None: - """Test that lambda sensor dict are not present in the device data - if it is unavailable. - """ - test_message = messages[FrameType.MESSAGE_SENSOR_DATA] - test_message[INDEX_LAMBDA_SENSOR] = BYTE_UNDEFINED - for byte in range(1, 6): - del test_message[INDEX_LAMBDA_SENSOR + byte] - - frame = SensorDataMessage(message=test_message) - assert ATTR_LAMBDA_STATE not in frame.data - assert ATTR_LAMBDA_TARGET not in frame.data - assert ATTR_LAMBDA_LEVEL not in frame.data - - -def test_sensor_data_without_fan_power_and_fuel_consumption( - messages: dict[FrameType, bytearray] -) -> None: - """Test that power, fan power and fuel consumption keys are not - present in the device data if they are unavailable. - """ - test_message = messages[FrameType.MESSAGE_SENSOR_DATA] - for index in (INDEX_FAN_POWER, INDEX_FUEL_CONSUMPTION, INDEX_POWER): - for byte in range(4): - test_message[index + byte] = 0xFF - - frame = SensorDataMessage(message=test_message) - assert ATTR_FAN_POWER not in frame.data - assert ATTR_FUEL_CONSUMPTION not in frame.data - assert ATTR_POWER not in frame.data + result = decoder.decode(frame.message, data=schema["data"])[0] + if regdata["id"] == "unknown_regulator_data_version": + assert ATTR_FRAME_VERSIONS not in result + assert ATTR_REGDATA not in result + else: + assert result[ATTR_FRAME_VERSIONS] == regdata["data"][ATTR_FRAME_VERSIONS] + assert result[ATTR_REGDATA].data == regdata["data"][ATTR_REGDATA] + + +@pytest.mark.parametrize( + "message, data", + load_json_parameters("messages/sensor_data.json"), +) +def test_sensor_data_message(message, data) -> None: + """Test a sensor data message.""" + assert SensorDataMessage(message=message).data == data -def test_current_data_without_thermostats( - sensor_data_without_thermostats: bytearray, -) -> None: - """Test that thermostats key is not present in the device data - if thermostats data is unavailable. - """ - frame = SensorDataMessage(message=sensor_data_without_thermostats) - assert ATTR_THERMOSTAT_SENSORS not in frame.data +def test_sensor_data_message_with_unknown_state() -> None: + """Test a sensor data message with an unknown device state.""" + test_data = load_json_test_data("messages/sensor_data.json")[0] + message = test_data["message"] + message[INDEX_STATE] = 99 + assert SensorDataMessage(message=message).data[ATTR_SENSORS][ATTR_STATE] == 99 diff --git a/tests/frames/test_requests.py b/tests/frames/test_requests.py index d18a0bc9..c94c554e 100644 --- a/tests/frames/test_requests.py +++ b/tests/frames/test_requests.py @@ -1,8 +1,8 @@ -"""Test PyPlumIO request frames.""" +"""Contains a tests for the request frame classes.""" import pytest -from pyplumio.const import DeviceType, FrameType +from pyplumio.const import DeviceType from pyplumio.exceptions import FrameDataError from pyplumio.frames import Request from pyplumio.frames.requests import ( @@ -24,16 +24,16 @@ UIDRequest, ) from pyplumio.frames.responses import DeviceAvailableResponse, ProgramVersionResponse -from pyplumio.helpers.typing import EventDataType +from tests import load_json_parameters -def test_base_class_response() -> None: - """Test response for base class.""" +def test_request_class_response_property() -> None: + """Test response property for a abstract request class.""" assert Request().response() is None def test_request_type() -> None: - """Test if request is instance of frame class.""" + """Test if request is an instance of frame class.""" for request in ( ProgramVersionRequest, CheckDeviceRequest, @@ -65,79 +65,85 @@ def test_check_device_response_recipient_and_type() -> None: assert frame.response().recipient == DeviceType.ECONET -def test_parameters(messages: dict[FrameType, bytearray]) -> None: - """Test parameters request bytes.""" - frame = EcomaxParametersRequest() - assert frame.message == messages[FrameType.REQUEST_ECOMAX_PARAMETERS] +@pytest.mark.parametrize( + "message, data", + load_json_parameters("requests/ecomax_control.json"), +) +def test_ecomax_control(message, data) -> None: + """Test an ecoMAX control parameter request.""" + assert EcomaxControlRequest(data=data).message == message -def test_set_parameter( - data: dict[FrameType, EventDataType], messages: dict[FrameType, bytearray] -) -> None: - """Test set parameter request bytes.""" - frame = SetEcomaxParameterRequest(data=data[FrameType.REQUEST_SET_ECOMAX_PARAMETER]) - assert frame.message == messages[FrameType.REQUEST_SET_ECOMAX_PARAMETER] +def test_ecomax_control_without_data() -> None: + """Test an ecoMAX control request without any data.""" + with pytest.raises(FrameDataError): + EcomaxControlRequest() -def test_set_parameter_with_no_data() -> None: - """Test set parameter request with no data.""" - with pytest.raises(FrameDataError): - SetEcomaxParameterRequest() +@pytest.mark.parametrize( + "message, data", + load_json_parameters("requests/ecomax_parameters.json"), +) +def test_ecomax_parameters(message, data) -> None: + """Test an ecoMAX parameters request.""" + assert EcomaxParametersRequest(data=data).message == message -def test_set_mixer_parameter( - data: dict[FrameType, EventDataType], messages: dict[FrameType, bytearray] -) -> None: - """Test set mixer parameter request bytes.""" - frame = SetMixerParameterRequest(data=data[FrameType.REQUEST_SET_MIXER_PARAMETER]) - assert frame.message == messages[FrameType.REQUEST_SET_MIXER_PARAMETER] +@pytest.mark.parametrize( + "message, data", + load_json_parameters("requests/set_ecomax_parameter.json"), +) +def test_set_ecomax_parameter(message, data) -> None: + """Test a set ecoMAX parameter request.""" + assert SetEcomaxParameterRequest(data=data).message == message -def test_set_mixer_parameter_with_no_data() -> None: - """Test set mixer parameter request with no data.""" +def test_set_ecomax_parameter_without_data() -> None: + """Test a set ecoMAX parameter request without any data.""" with pytest.raises(FrameDataError): - SetMixerParameterRequest() + SetEcomaxParameterRequest() -def test_set_thermostat_parameter( - data: dict[FrameType, EventDataType], messages: dict[FrameType, bytearray] -) -> None: - """Test set thermostat parameter request bytes.""" - frame = SetThermostatParameterRequest( - data=data[FrameType.REQUEST_SET_THERMOSTAT_PARAMETER] - ) - assert frame.message == messages[FrameType.REQUEST_SET_THERMOSTAT_PARAMETER] +@pytest.mark.parametrize( + "message, data", + load_json_parameters("requests/set_mixer_parameter.json"), +) +def test_set_mixer_parameter(message, data) -> None: + """Test a set mixer parameter request.""" + assert SetMixerParameterRequest(data=data).message == message -def test_set_thermostat_parameter_with_no_data() -> None: - """Test set thermostat parameter request with no data.""" +def test_set_mixer_parameter_without_data() -> None: + """Test a set mixer parameter request without any data.""" with pytest.raises(FrameDataError): - SetThermostatParameterRequest() + SetMixerParameterRequest() -def test_ecomax_control( - data: dict[FrameType, EventDataType], messages: dict[FrameType, bytearray] -) -> None: - """Test ecoMAX control parameter request bytes.""" - frame = EcomaxControlRequest(data=data[FrameType.REQUEST_ECOMAX_CONTROL]) - assert frame.message == messages[FrameType.REQUEST_ECOMAX_CONTROL] +@pytest.mark.parametrize( + "message, data", + load_json_parameters("requests/set_schedule.json"), +) +def test_set_schedule(message, data) -> None: + """Test a set schedule request bytes.""" + assert SetScheduleRequest(data=data).message == message -def test_ecomax_control_with_no_data() -> None: - """Test ecoMAX control request with no data.""" +def test_set_schedule_without_data() -> None: + """Test a set schedule request without any data.""" with pytest.raises(FrameDataError): - EcomaxControlRequest() + SetScheduleRequest() -def test_set_schedule( - data: dict[FrameType, EventDataType], messages: dict[FrameType, bytearray] -) -> None: - """Test set schedule request bytes.""" - frame = SetScheduleRequest(data=data[FrameType.REQUEST_SET_SCHEDULE]) - assert frame.message == messages[FrameType.REQUEST_SET_SCHEDULE] +@pytest.mark.parametrize( + "message, data", + load_json_parameters("requests/set_thermostat_parameter.json"), +) +def test_set_thermostat_parameter(message, data) -> None: + """Test a set thermostat parameter request.""" + assert SetThermostatParameterRequest(data=data).message == message -def test_set_schedule_with_no_data() -> None: - """Test set schedule request with no data.""" +def test_set_thermostat_parameter_without_data() -> None: + """Test a set thermostat parameter request without any data.""" with pytest.raises(FrameDataError): - SetScheduleRequest() + SetThermostatParameterRequest() diff --git a/tests/frames/test_responses.py b/tests/frames/test_responses.py index bac1f12d..a8ce0d72 100644 --- a/tests/frames/test_responses.py +++ b/tests/frames/test_responses.py @@ -1,6 +1,8 @@ -"""Test PyPlumIO response frames.""" +"""Contains tests for the response frame classes.""" -from pyplumio.const import DeviceType, FrameType +import pytest + +from pyplumio.const import DeviceType from pyplumio.frames.responses import ( AlertsResponse, DataSchemaResponse, @@ -13,20 +15,13 @@ ThermostatParametersResponse, UIDResponse, ) -from pyplumio.helpers.typing import EventDataType -from pyplumio.structures.alerts import ATTR_ALERTS -from pyplumio.structures.data_schema import ATTR_SCHEMA -from pyplumio.structures.mixer_parameters import ATTR_MIXER_PARAMETERS -from pyplumio.structures.thermostat_parameters import ( - ATTR_THERMOSTAT_PARAMETERS, - ATTR_THERMOSTAT_PARAMETERS_DECODER, - ATTR_THERMOSTAT_PROFILE, -) +from pyplumio.structures.thermostat_parameters import ATTR_THERMOSTAT_PARAMETERS_DECODER from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_COUNT +from tests import load_json_parameters def test_responses_type() -> None: - """Test if response is instance of frame class.""" + """Test if response is an instance of frame class.""" for response in ( ProgramVersionResponse, @@ -42,155 +37,108 @@ def test_responses_type() -> None: assert isinstance(frame, response) -def test_program_version_response( - data: dict[FrameType, EventDataType], messages: dict[FrameType, bytearray] -) -> None: - """Test creating program version message.""" - frame1 = ProgramVersionResponse(data=data[FrameType.RESPONSE_PROGRAM_VERSION]) - frame2 = ProgramVersionResponse( - message=messages[FrameType.RESPONSE_PROGRAM_VERSION] - ) - assert frame1.message == messages[FrameType.RESPONSE_PROGRAM_VERSION] - assert frame2.data == data[FrameType.RESPONSE_PROGRAM_VERSION] - - -def test_device_available_response( - data: dict[FrameType, EventDataType], - messages: dict[FrameType, bytearray], -) -> None: - """Test creating device available message.""" - frame1 = DeviceAvailableResponse(data=data[FrameType.RESPONSE_DEVICE_AVAILABLE]) - frame2 = DeviceAvailableResponse( - message=messages[FrameType.RESPONSE_DEVICE_AVAILABLE] - ) - assert frame1.message == messages[FrameType.RESPONSE_DEVICE_AVAILABLE] - assert frame2.data == data[FrameType.RESPONSE_DEVICE_AVAILABLE] +@pytest.mark.parametrize( + "message, data", + load_json_parameters("responses/alerts.json"), +) +def test_alerts_response(message, data) -> None: + """Test an alerts response.""" + assert AlertsResponse(message=message).data == data + assert not AlertsResponse(data=data).message + + +@pytest.mark.parametrize( + "message, data", + load_json_parameters("responses/data_schema.json"), +) +def test_data_schema_response(message, data) -> None: + """Test a data schema response.""" + assert DataSchemaResponse(message=message).data == data + assert not DataSchemaResponse(data=data).message -def test_uid_response( - data: dict[FrameType, EventDataType], - messages: dict[FrameType, bytearray], -) -> None: - """Test parsing UID message.""" - frame1 = UIDResponse(message=messages[FrameType.RESPONSE_UID]) - frame2 = UIDResponse(data=data[FrameType.RESPONSE_UID]) - assert frame1.data == data[FrameType.RESPONSE_UID] - assert not frame2.message +@pytest.mark.parametrize( + "message, data", + load_json_parameters("responses/device_available.json"), +) +def test_device_available_response(message, data) -> None: + """Test a device available response.""" + assert DeviceAvailableResponse(data=data).message == message + assert DeviceAvailableResponse(message=message).data == data + + +@pytest.mark.parametrize( + "message, data", + load_json_parameters("responses/ecomax_parameters.json"), +) +def test_ecomax_parameters_response(message, data) -> None: + """Test a ecoMAX parameters response.""" + assert EcomaxParametersResponse(message=message).data == data + assert not EcomaxParametersResponse(data=data).message -def test_password_response( - data: dict[FrameType, EventDataType], - messages: dict[FrameType, bytearray], -) -> None: +@pytest.mark.parametrize( + "message, data", + load_json_parameters("responses/mixer_parameters.json"), +) +def test_mixer_parameters_response(message, data) -> None: + """Test a mixer parameters response.""" + assert MixerParametersResponse(message=message).data == data + assert not MixerParametersResponse(data=data).message + + +@pytest.mark.parametrize( + "message, data", + load_json_parameters("responses/password.json"), +) +def test_password_response(message, data) -> None: """Test parsing password message.""" - frame1 = PasswordResponse(message=messages[FrameType.RESPONSE_PASSWORD]) - frame2 = PasswordResponse(data=data[FrameType.RESPONSE_PASSWORD]) - assert frame1.data == data[FrameType.RESPONSE_PASSWORD] - assert not frame2.message - - -def test_ecomax_parameters_response( - data: dict[FrameType, EventDataType], - messages: dict[FrameType, bytearray], -) -> None: - """Test parsing ecoMAX parameters message.""" - frame1 = EcomaxParametersResponse( - message=messages[FrameType.RESPONSE_ECOMAX_PARAMETERS] - ) - frame2 = EcomaxParametersResponse(data=data[FrameType.RESPONSE_ECOMAX_PARAMETERS]) - assert frame1.data == data[FrameType.RESPONSE_ECOMAX_PARAMETERS] - assert not frame2.message - - -def test_mixer_parameters_response( - data: dict[FrameType, EventDataType], - messages: dict[FrameType, bytearray], -) -> None: - """Test parsing message for mixer parameters response.""" - frame1 = MixerParametersResponse( - message=messages[FrameType.RESPONSE_MIXER_PARAMETERS] - ) - frame2 = MixerParametersResponse(data=data[FrameType.RESPONSE_MIXER_PARAMETERS]) - assert frame1.data == data[FrameType.RESPONSE_MIXER_PARAMETERS] - assert not frame2.message - - # Test with empty parameters. - frame1 = MixerParametersResponse(message=bytearray.fromhex("00000201")) - assert frame1.data == {ATTR_MIXER_PARAMETERS: {}} - - -def test_thermostat_parameters_response( - data: dict[FrameType, EventDataType], - messages: dict[FrameType, bytearray], -) -> None: - """Test parsing message for thermostat parameters response.""" - frame = ThermostatParametersResponse( - message=messages[FrameType.RESPONSE_THERMOSTAT_PARAMETERS] - ) + assert PasswordResponse(message=message).data == data + assert not PasswordResponse(data=data).message + + +@pytest.mark.parametrize( + "message, data", + load_json_parameters("responses/program_version.json"), +) +def test_program_version_response(message, data) -> None: + """Test a program version response.""" + assert ProgramVersionResponse(data=data).message == message + assert ProgramVersionResponse(message=message).data == data + + +@pytest.mark.parametrize( + "message, data", + load_json_parameters("responses/schedules.json"), +) +def test_schedules_response(message, data) -> None: + """Test a schedules response.""" + assert SchedulesResponse(message=message).data == data + assert not SchedulesResponse(data=data).message + + +@pytest.mark.parametrize( + "message, data", + load_json_parameters("responses/thermostat_parameters.json"), +) +def test_thermostat_parameters_response(message, data) -> None: + """Test a thermostat parameters response.""" + frame = ThermostatParametersResponse(message=message) decoder = frame.data[ATTR_THERMOSTAT_PARAMETERS_DECODER] - frame_data = decoder.decode( - message=frame.message, - data={ATTR_THERMOSTAT_COUNT: 3}, - )[0] - assert frame_data == data[FrameType.RESPONSE_THERMOSTAT_PARAMETERS] - - -def test_thermostat_parameters_response_with_no_parameters() -> None: - """Test parsing message for the thermosat parameters response - with no parameters.""" - frame = ThermostatParametersResponse( - message=bytearray.fromhex("00000300FFFFFFFFFFFFFFFFFF") + assert ( + decoder.decode( + message=frame.message, + data={ATTR_THERMOSTAT_COUNT: 3}, + )[0] + == data ) - decoder = frame.data[ATTR_THERMOSTAT_PARAMETERS_DECODER] - frame_data = decoder.decode( - message=frame.message, - data={ATTR_THERMOSTAT_COUNT: 2}, - )[0] - assert frame_data == { - ATTR_THERMOSTAT_COUNT: 2, - ATTR_THERMOSTAT_PROFILE: None, - ATTR_THERMOSTAT_PARAMETERS: {}, - } - - -def test_data_schema_response(messages: dict[FrameType, bytearray]) -> None: - """Test parsing message for data schema response.""" - frame = DataSchemaResponse(message=messages[FrameType.RESPONSE_DATA_SCHEMA]) - assert ATTR_SCHEMA in frame.data - assert len(frame.data[ATTR_SCHEMA]) == 257 - - -def test_data_schema_response_with_no_parameters() -> None: - """Test parsing message for data schema with no parameters.""" - frame = DataSchemaResponse(message=bytearray.fromhex("0000")) - assert ATTR_SCHEMA not in frame.data - - -def test_alerts_response( - data: dict[FrameType, EventDataType], - messages: dict[FrameType, bytearray], -) -> None: - """Test alerts response.""" - frame1 = AlertsResponse(message=messages[FrameType.RESPONSE_ALERTS]) - frame2 = AlertsResponse(data=data[FrameType.RESPONSE_ALERTS]) - assert frame1.data == data[FrameType.RESPONSE_ALERTS] - assert not frame2.message - - -def test_alerts_response_with_no_alerts( - data: dict[FrameType, EventDataType], - messages: dict[FrameType, bytearray], -) -> None: - """Test alerts response with no alerts.""" - frame = AlertsResponse(message=bytearray.fromhex("000000")) - assert ATTR_ALERTS not in frame.data - - -def test_schedule_response( - data: dict[FrameType, EventDataType], messages: dict[FrameType, bytearray] -) -> None: - """Test schedule response.""" - frame1 = SchedulesResponse(message=messages[FrameType.RESPONSE_SCHEDULES]) - frame2 = SchedulesResponse(data=data[FrameType.RESPONSE_SCHEDULES]) - assert frame1.data == data[FrameType.RESPONSE_SCHEDULES] - assert not frame2.message + + +@pytest.mark.parametrize( + "message, data", + load_json_parameters("responses/uid.json"), +) +def test_uid_response(message, data) -> None: + """Test an UID response.""" + assert UIDResponse(message=message).data == data + assert not UIDResponse(data=data).message diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index 78cd572e..66219e16 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -1 +1 @@ -"""Contains tests for helper classes.""" +"""Contains tests for the helper classes.""" diff --git a/tests/helpers/test_data_types.py b/tests/helpers/test_data_types.py index a3a804fb..93160233 100644 --- a/tests/helpers/test_data_types.py +++ b/tests/helpers/test_data_types.py @@ -1,85 +1,105 @@ -"""Contains tests for data types.""" +"""Contains tests for the data type helper classes.""" + +import pytest from pyplumio.helpers import data_types def test_type_repr(): - """Test serializable type representation.""" + """Test a serializable type representation.""" assert repr(data_types.SignedChar(22)) == "SignedChar(value=22)" def test_type_unpack() -> None: - """Test generic type unpack.""" + """Test a generic type unpack.""" data_type = data_types.SignedChar.from_bytes(bytearray([0x16])) assert data_type.value == 22 assert data_type.size == 1 +@pytest.mark.parametrize( + "one, another", + [ + ( + data_types.SignedChar.from_bytes(bytearray([0x16])), + data_types.SignedChar.from_bytes(bytearray([0x16])), + ), + ( + data_types.SignedChar.from_bytes(bytearray([0x16])), + 22, + ), + ], +) +def test_type_eq(one, another): + """Test a generic type comparison.""" + assert one == another + + def test_undefined0() -> None: - """Test undefined0 data_type.""" + """Test an undefined0 data type.""" data_type = data_types.Undefined0.from_bytes(bytearray([0x0])) assert data_type.value is None assert data_type.size == 0 def test_signed_char() -> None: - """Test signed char data_type.""" + """Test a signed char data type.""" data_type = data_types.SignedChar.from_bytes(bytearray([0x16])) assert data_type.value == 22 assert data_type.size == 1 def test_short() -> None: - """Test short data_type.""" + """Test a short data type.""" data_type = data_types.Short.from_bytes(bytearray([0xEC, 0xFF])) assert data_type.value == -20 assert data_type.size == 2 def test_int() -> None: - """Test integer data_type.""" + """Test an integer data type.""" data_type = data_types.Int.from_bytes(bytearray([0x01, 0x9A, 0xFF, 0xFF])) assert data_type.value == -26111 assert data_type.size == 4 def test_byte() -> None: - """Test byte data_type.""" + """Test a byte data type.""" data_type = data_types.Byte.from_bytes(bytearray([0x3])) assert data_type.value == 3 assert data_type.size == 1 def test_ushort() -> None: - """Test unsigned short data_type.""" + """Test an unsigned short data type.""" data_type = data_types.UnsignedShort.from_bytes(bytearray([0x2A, 0x01])) assert data_type.value == 298 assert data_type.size == 2 def test_uint() -> None: - """Test unsigned integer data_type.""" + """Test an unsigned integer data type.""" data_type = data_types.UnsignedInt.from_bytes(bytearray([0x9A, 0x3F, 0x0, 0x0])) assert data_type.value == 16282 assert data_type.size == 4 def test_float() -> None: - """Test float data_type.""" + """Test a float data type.""" data_type = data_types.Float.from_bytes(bytearray([0x0, 0x0, 0x40, 0x41])) assert data_type.value == 12.0 assert data_type.size == 4 def test_undefined8() -> None: - """Test undefined8 data_type.""" + """Test a undefined8 data type.""" data_type = data_types.Undefined8.from_bytes(bytearray([0x0])) assert data_type.value is None assert data_type.size == 0 def test_double() -> None: - """Test double data_type.""" + """Test a double data type.""" data_type = data_types.Double.from_bytes( bytearray([0x3D, 0x0A, 0xD7, 0xA3, 0x70, 0x3D, 0x28, 0x40]) ) @@ -88,7 +108,7 @@ def test_double() -> None: def test_boolean() -> None: - """Test boolean data_type.""" + """Test a boolean data type.""" data_type = data_types.Boolean.from_bytes(bytearray([0x55])) for index, value in enumerate([1, 0, 1, 0, 1, 0, 1, 0]): next_bit = data_type.next(index) @@ -102,14 +122,14 @@ def test_boolean() -> None: def test_boolean_unpack() -> None: - """Test boolean unpack.""" + """Test a boolean unpack.""" data_type = data_types.Boolean.from_bytes(bytearray([0x55])) assert data_type.value assert data_type.size == 0 def test_int64() -> None: - """Test 64 bit integer data_type.""" + """Test a 64 bit integer data type.""" data_type = data_types.Int64.from_bytes( bytearray([0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0xA4, 0x32, 0xEB]) ) @@ -118,7 +138,7 @@ def test_int64() -> None: def test_uint64() -> None: - """Test unsigned 64 bit integer data_type.""" + """Test a unsigned 64 bit integer data type.""" data_type = data_types.UInt64.from_bytes( bytearray([0x45, 0x49, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55]) ) @@ -127,14 +147,14 @@ def test_uint64() -> None: def test_ipv4() -> None: - """Test IPv4 data_type.""" + """Test an IPv4 data type.""" data_type = data_types.IPv4.from_bytes(bytearray([0x7F, 0x00, 0x00, 0x01])) assert data_type.value == "127.0.0.1" assert data_type.size == 4 def test_ipv6() -> None: - """Test IPv6 data_type.""" + """Test an IPv6 data type.""" data_type = data_types.IPv6.from_bytes( bytearray( [ @@ -162,7 +182,7 @@ def test_ipv6() -> None: def test_string() -> None: - """Test string data_type.""" + """Test a string data type.""" data_type = data_types.String.from_bytes(b"test\x00") assert data_type.value == "test" assert data_type.size == 5 diff --git a/tests/helpers/test_event_manager.py b/tests/helpers/test_event_manager.py index 5df7034f..c03baa0e 100644 --- a/tests/helpers/test_event_manager.py +++ b/tests/helpers/test_event_manager.py @@ -9,19 +9,19 @@ @pytest.fixture(name="event_manager") def fixture_event_manager() -> EventManager: - """Return the event manager.""" + """Return an event manager object.""" event_manager = EventManager() event_manager.data = {"test_key": "test_value"} return event_manager def test_getattr(event_manager: EventManager) -> None: - """Test __getattr__ dunder method.""" + """Test getting an event manager attribute.""" assert event_manager.test_key == "test_value" async def test_wait_for(event_manager: EventManager) -> None: - """Test wait_for method.""" + """Test waiting for event.""" with patch("asyncio.wait_for") as mock_wait_for, patch( "pyplumio.helpers.event_manager.EventManager.create_event" ) as mock_create_event: @@ -35,18 +35,18 @@ async def test_wait_for(event_manager: EventManager) -> None: async def test_get(event_manager: EventManager) -> None: - """Test async getter.""" + """Test getting an event value asynchronously.""" assert await event_manager.get("test_key") == "test_value" def test_get_nowait(event_manager: EventManager) -> None: - """Test getter.""" + """Test getting an event value.""" assert event_manager.get_nowait("test_key") == "test_value" assert event_manager.get_nowait("test_key2") is None async def test_subscribe(event_manager: EventManager) -> None: - """Test subscribe.""" + """Test subscribing to an event.""" callback = AsyncMock(return_value=True) event_manager.subscribe("test_key2", callback) event_manager.dispatch_nowait("test_key2", "test_value2") @@ -56,7 +56,7 @@ async def test_subscribe(event_manager: EventManager) -> None: async def test_subscribe_once(event_manager: EventManager) -> None: - """Test subscribe once.""" + """Test subscribing to an event once.""" callback = AsyncMock(return_value=True) event_manager.subscribe_once("test_key2", callback) event_manager.dispatch_nowait("test_key2", "test_value2") @@ -66,7 +66,7 @@ async def test_subscribe_once(event_manager: EventManager) -> None: async def test_unsubscribe(event_manager: EventManager) -> None: - """Test unsubscribe.""" + """Test unsubscribing from the event.""" callback = AsyncMock(return_value=True) event_manager.subscribe("test_key2", callback) event_manager.unsubscribe("test_key2", callback) @@ -76,7 +76,7 @@ async def test_unsubscribe(event_manager: EventManager) -> None: async def test_load(event_manager: EventManager) -> None: - """Test load.""" + """Test loading an event data.""" callback = AsyncMock(return_value=True) callback2 = AsyncMock(return_value=True) event_manager.subscribe("test_key1", callback) @@ -88,7 +88,7 @@ async def test_load(event_manager: EventManager) -> None: async def test_create_event(event_manager: EventManager) -> None: - """Test create event.""" + """Test creating an event.""" event = event_manager.create_event("test") assert event == event_manager.create_event("test") assert "test" in event_manager.events diff --git a/tests/helpers/test_factory.py b/tests/helpers/test_factory.py index cee1019e..a860dfbd 100644 --- a/tests/helpers/test_factory.py +++ b/tests/helpers/test_factory.py @@ -7,14 +7,20 @@ def test_get_object() -> None: - """Test getting object with factory.""" + """Test getting an object via class path.""" cls = factory("frames.requests.StopMasterRequest") assert isinstance(cls, StopMasterRequest) - # Check with nonexistent class. + +def test_get_object_with_nonexistent_class() -> None: + """Test getting an object via class path for nonexistent class.""" with pytest.raises(AttributeError): factory("frames.requests.NonExistent") - # Check with nonexistent module. + +def test_get_object_with_nonexistent_module() -> None: + """Test getting an object via class path for a class within + a nonexistent module. + """ with pytest.raises(ModuleNotFoundError): factory("frames.request.StopMasterRequest") diff --git a/tests/helpers/test_parameter.py b/tests/helpers/test_parameter.py index 47952966..24083e3c 100644 --- a/tests/helpers/test_parameter.py +++ b/tests/helpers/test_parameter.py @@ -16,23 +16,25 @@ class TestParameter(Parameter): - """Concrete implementation of the parameter class.""" + """Represents a concrete implementation of the parameter class.""" __test__: bool = False @property def request(self) -> Request: - """Return request to change the parameter.""" + """A request to change the parameter.""" return Request() class TestBinaryParameter(BinaryParameter, TestParameter): - """Concrete implementation of the binary parameter class.""" + """Represents a concrete implementation of the binary parameter + class. + """ @pytest.fixture(name="parameter") def fixture_parameter(ecomax: EcoMAX) -> Parameter: - """Return an instance of the parameter.""" + """Return a parameter object.""" return TestParameter( device=ecomax, value=1, @@ -44,7 +46,7 @@ def fixture_parameter(ecomax: EcoMAX) -> Parameter: @pytest.fixture(name="binary_parameter") def fixture_binary_parameter(ecomax: EcoMAX) -> BinaryParameter: - """Return an instance of the parameter.""" + """Return a binary parameter object.""" return TestBinaryParameter( device=ecomax, value=STATE_OFF, @@ -76,7 +78,7 @@ async def test_parameter_values(parameter: Parameter) -> None: def test_base_parameter_request(ecomax: EcoMAX) -> None: - """Test that base class request throws not implemented error.""" + """Test that a base class request throws not implemented error.""" parameter = Parameter( device=ecomax, value=1, @@ -93,7 +95,7 @@ def test_base_parameter_request(ecomax: EcoMAX) -> None: async def test_parameter_set( mock_subscribe_once, parameter: Parameter, bypass_asyncio_sleep ) -> None: - """Test setting parameter.""" + """Test setting a parameter.""" await parameter.set(5) assert parameter == 5 mock_subscribe_once.assert_called_once() @@ -110,7 +112,7 @@ async def test_parameter_set( @patch("pyplumio.devices.Device.create_task") @patch("pyplumio.helpers.parameter.Parameter.set", new_callable=Mock) async def test_parameter_set_nowait(mock_set, mock_create_task, parameter: Parameter): - """Test setting parameter without waiting for result.""" + """Test setting a parameter without waiting for result.""" parameter.set_nowait(1) await parameter.device.wait_until_done() mock_create_task.assert_called_once() @@ -124,7 +126,7 @@ async def test_parameter_set_out_of_range(parameter: Parameter) -> None: def test_parameter_relational(parameter: Parameter): - """Test a parameter subtraction.""" + """Test parameter relational methods.""" assert (parameter - 1) == 0 assert (parameter + 1) == 2 assert (parameter * 5) == 5 @@ -133,7 +135,7 @@ def test_parameter_relational(parameter: Parameter): def test_parameter_compare(parameter: Parameter) -> None: - """Test a parameter comparison.""" + """Test parameter comparison.""" assert parameter == 1 parameter_values = ParameterValues(value=1, min_value=0, max_value=5) assert parameter == parameter_values @@ -144,7 +146,7 @@ def test_parameter_compare(parameter: Parameter) -> None: def test_parameter_int(parameter: Parameter) -> None: - """Test a parameter conversion to integer.""" + """Test parameter conversion to an integer.""" assert int(parameter) == 1 @@ -161,8 +163,8 @@ def test_parameter_repr(parameter: Parameter) -> None: async def test_parameter_request_with_unchanged_value( mock_put, parameter: Parameter, bypass_asyncio_sleep, caplog ) -> None: - """Test that frame doesn't get dispatched if it's - value is not changed.""" + """Test that a frame doesn't get dispatched if it's + value is unchanged.""" assert not parameter.is_changed assert not await parameter.set(5, retries=3) assert parameter.is_changed @@ -177,7 +179,7 @@ async def test_parameter_request_with_unchanged_value( async def test_binary_parameter_turn_on( mock_set, binary_parameter: BinaryParameter ) -> None: - """Test that binary parameter can be turned on.""" + """Test that a binary parameter can be turned on.""" await binary_parameter.turn_on() mock_set.assert_called_once_with(STATE_ON) @@ -186,7 +188,7 @@ async def test_binary_parameter_turn_on( async def test_binary_parameter_turn_off( mock_set, binary_parameter: BinaryParameter ) -> None: - """Test that binary parameter can be turned off.""" + """Test that a binary parameter can be turned off.""" await binary_parameter.turn_off() mock_set.assert_called_once_with(STATE_OFF) @@ -195,7 +197,7 @@ async def test_binary_parameter_turn_off( async def test_binary_parameter_turn_on_nowait( mock_set_nowait, binary_parameter: BinaryParameter ) -> None: - """Test that binary parameter can be turned on without waiting.""" + """Test that a binary parameter can be turned on without waiting.""" binary_parameter.turn_on_nowait() mock_set_nowait.assert_called_once_with(STATE_ON) @@ -204,13 +206,15 @@ async def test_binary_parameter_turn_on_nowait( async def test_binary_parameter_turn_off_nowait( mock_set_nowait, binary_parameter: BinaryParameter ) -> None: - """Test that binary parameter can be turned off without waiting.""" + """Test that a binary parameter can be turned off without + waiting. + """ binary_parameter.turn_off_nowait() mock_set_nowait.assert_called_once_with(STATE_OFF) async def test_binary_parameter_values(binary_parameter: BinaryParameter) -> None: - """Test the binary parameter values.""" + """Test a binary parameter values.""" assert binary_parameter.value == STATE_OFF assert binary_parameter.min_value == STATE_OFF assert binary_parameter.max_value == STATE_ON diff --git a/tests/helpers/test_schedule.py b/tests/helpers/test_schedule.py index ded2be01..7fef7f86 100644 --- a/tests/helpers/test_schedule.py +++ b/tests/helpers/test_schedule.py @@ -1,4 +1,4 @@ -"""Contains test for schedule helpers.""" +"""Contains tests for the schedule helper classes.""" import asyncio from unittest.mock import Mock, patch @@ -23,13 +23,13 @@ @pytest.fixture(name="schedule_day") def fixture_schedule_day() -> ScheduleDay: - """Return instance of schedule day.""" + """Return a schedule day object.""" return ScheduleDay([False for _ in range(48)]) @pytest.fixture(name="schedule") def fixture_schedule(schedule_day: ScheduleDay) -> Schedule: - """Return instance of schedule.""" + """Return a schedule object.""" return Schedule( name="test", device=Mock(spec=Device), @@ -44,7 +44,7 @@ def fixture_schedule(schedule_day: ScheduleDay) -> Schedule: def test_schedule_day(schedule_day: ScheduleDay) -> None: - """Test schedule day.""" + """Test a schedule day.""" schedule_day.set_state(STATE_ON, "00:00", "01:00") assert schedule_day.intervals[0] # 00:00 assert schedule_day.intervals[1] # 00:30 @@ -52,39 +52,47 @@ def test_schedule_day(schedule_day: ScheduleDay) -> None: assert not schedule_day.intervals[3] # 01:30 assert len(schedule_day) == 48 - # Test with incorrect interval. + +def test_schedule_day_with_incorrect_interval(schedule_day: ScheduleDay) -> None: + """Test a schedule day with an incorrect interval.""" for start, end in (("01:00", "00:30"), ("00:foo", "bar")): with pytest.raises(ValueError): schedule_day.set_state(STATE_ON, start, end) - # Test with incorrect state. + +def test_schedule_day_with_incorrect_state(schedule_day: ScheduleDay) -> None: + """Test a schedule day with an incorrect state.""" with pytest.raises(ValueError): schedule_day.set_state("invalid_state", "00:00", "01:00") # type: ignore [arg-type] - # set whole day schedule. + +def test_setting_whole_day_schedule(schedule_day: ScheduleDay) -> None: + """Test setting schedule for a whole day.""" schedule_day.set_on() assert schedule_day.intervals == [True for _ in range(48)] - schedule_day.set_off("00:30", "01:00") - # Test sequence and iterable methods. + +def test_setting_schedule_with_sequence(schedule_day: ScheduleDay) -> None: + """Test setting a schedule via sequence methods.""" + schedule_day.set_on("00:30", "01:00") schedule_day_iter = iter(schedule_day) - assert next(schedule_day_iter) assert not next(schedule_day_iter) - assert not schedule_day[1] - schedule_day[1] = True + assert next(schedule_day_iter) + assert schedule_day[1] + schedule_day[1] = False del schedule_day[0] - assert schedule_day[0] - schedule_day.append(False) - assert not schedule_day[-1] + assert not schedule_day[0] + schedule_day.append(True) + assert schedule_day[-1] def test_schedule_day_repr(schedule_day: ScheduleDay) -> None: - """Test serializable representation of schedule day.""" + """Test serializable representation of a schedule day.""" assert repr(schedule_day) == f"ScheduleDay({[False for _ in range(48)]})" def test_schedule(schedule: Schedule) -> None: - """Test schedule.""" + """Test a schedule.""" schedule_iter = iter(schedule) assert not next(schedule_iter)[0] assert next(schedule_iter)[0] @@ -92,7 +100,7 @@ def test_schedule(schedule: Schedule) -> None: @patch("pyplumio.helpers.schedule.factory") def test_schedule_commit(mock_factory, schedule: Schedule) -> None: - """Test schedule commit.""" + """Test committing a schedule.""" schedule.device = Mock(spec=Device) schedule.device.address = DeviceType.ECOMAX schedule.device.data = { diff --git a/tests/helpers/test_task_manager.py b/tests/helpers/test_task_manager.py index 65eef982..70f512a3 100644 --- a/tests/helpers/test_task_manager.py +++ b/tests/helpers/test_task_manager.py @@ -1,4 +1,4 @@ -"""Contains tests for the task manager.""" +"""Contains tests for the task manager helper class.""" import asyncio from unittest.mock import AsyncMock, Mock, patch @@ -10,7 +10,7 @@ @pytest.fixture(name="task_manager") async def fixture_task_manager() -> TaskManager: - """Return the task manager.""" + """Return a task manager object.""" task_manager = TaskManager() with patch("asyncio.create_task"): task_manager.create_task(Mock()) @@ -19,7 +19,7 @@ async def fixture_task_manager() -> TaskManager: def test_create_task(task_manager: TaskManager) -> None: - """Test create task.""" + """Test creating a task.""" mock_coro = Mock() mock_task = Mock(spec=asyncio.Task) with patch("asyncio.create_task", return_value=mock_task) as create_task_mock: @@ -30,7 +30,7 @@ def test_create_task(task_manager: TaskManager) -> None: def test_cancel_task(task_manager: TaskManager) -> None: - """Test cancel task.""" + """Test canceling a task.""" mock_coro = Mock() mock_task = Mock(spec=asyncio.Task) with patch("asyncio.create_task", return_value=mock_task): @@ -41,7 +41,7 @@ def test_cancel_task(task_manager: TaskManager) -> None: async def test_wait_until_done(task_manager: TaskManager) -> None: - """Test wait until done.""" + """Test waiting until all tasks are done.""" with patch("asyncio.gather", new_callable=AsyncMock) as mock_gather: await task_manager.wait_until_done() diff --git a/tests/helpers/test_timeout.py b/tests/helpers/test_timeout.py index 8282d90b..28128105 100644 --- a/tests/helpers/test_timeout.py +++ b/tests/helpers/test_timeout.py @@ -1,4 +1,4 @@ -"""Contains tests for the timeout decorator.""" +"""Contains tests for the timeout decorator class.""" import asyncio import logging @@ -11,7 +11,7 @@ @patch("asyncio.wait_for", new_callable=AsyncMock, side_effect=(asyncio.TimeoutError)) async def test_timeout(mock_wait_for, caplog) -> None: - """Test timeout decorator.""" + """Test a timeout decorator.""" # Mock function to pass to the decorator. mock_func = Mock() mock_func.return_value = "test" diff --git a/tests/helpers/test_uid.py b/tests/helpers/test_uid.py index 88890698..ae7e4c19 100644 --- a/tests/helpers/test_uid.py +++ b/tests/helpers/test_uid.py @@ -1,11 +1,17 @@ -"""Contains tests for the UID decoder.""" +"""Contains tests for the UID helper class.""" + +import pytest from pyplumio.helpers.uid import unpack_uid -def test_from_bytes() -> None: - """Test conversion from bytes.""" - message1 = bytearray.fromhex("0B001600110D383338365539") - message2 = bytearray.fromhex("0D002500300E191932135831") - assert unpack_uid(message1) == "D251PAKR3GCPZ1K8G05G0" - assert unpack_uid(message2) == "CE71HB09J468P1ZZ00980" +@pytest.mark.parametrize( + "message, uid", + [ + ("0B001600110D383338365539", "D251PAKR3GCPZ1K8G05G0"), + ("0D002500300E191932135831", "CE71HB09J468P1ZZ00980"), + ], +) +def test_from_bytes(message, uid) -> None: + """Test unpacking an UID from bytes.""" + assert unpack_uid(bytearray.fromhex(message)) == uid diff --git a/tests/test_connection.py b/tests/test_connection.py index f5a1bb51..4dea1c3f 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,4 +1,4 @@ -"""Contains tests for connection.""" +"""Contains tests for the connection classes.""" from asyncio import StreamReader, StreamWriter import logging @@ -20,14 +20,14 @@ @pytest.fixture(name="stream_writer") def fixture_stream_writer(): - """Return mock of asyncio stream writer.""" + """Return a mock of asyncio stream writer.""" with patch("asyncio.StreamWriter", autospec=True) as mock_stream_writer: yield mock_stream_writer @pytest.fixture(name="stream_reader") def fixture_stream_reader(): - """Return mock of asyncio stream reader.""" + """Return a mock of asyncio stream reader.""" with patch("asyncio.StreamReader", autospec=True) as mock_stream_reader: yield mock_stream_reader @@ -57,25 +57,25 @@ def fixture_serial_asyncio_open_serial_connection( @pytest.fixture(name="tcp_connection") def fixture_tcp_connection() -> TcpConnection: - """Return tcp connection object.""" + """Return at TCP connection object.""" return TcpConnection(host=HOST, port=PORT, test="test") @pytest.fixture(name="serial_connection") def fixture_serial_connection() -> SerialConnection: - """Return serial connection object.""" + """Return a serial connection object.""" return SerialConnection(device="/dev/ttyUSB0", test="test") @pytest.fixture(name="mock_protocol") def fixture_mock_protocol(): - """Return mock Protocol object.""" + """Return a mock protocol object.""" with patch("pyplumio.connection.Protocol", autospec=True) as mock_protocol: yield mock_protocol async def test_tcp_connect(mock_protocol: Protocol, asyncio_open_connection) -> None: - """Test tcp connection logic.""" + """Test TCP connection logic.""" with patch( "pyplumio.connection.Connection._connection_lost" @@ -98,7 +98,7 @@ async def test_tcp_connect(mock_protocol: Protocol, asyncio_open_connection) -> @pytest.mark.usefixtures("mock_protocol") async def test_serial_connect(serial_asyncio_open_serial_connection) -> None: - """Test serial connection logic.""" + """Test a serial connection logic.""" serial_connection = SerialConnection( device=DEVICE, test="test", reconnect_on_failure=False ) @@ -120,7 +120,7 @@ async def test_serial_connect(serial_asyncio_open_serial_connection) -> None: @pytest.mark.usefixtures("mock_protocol", "asyncio_open_connection") async def test_reconnect(tcp_connection: TcpConnection, caplog) -> None: - """Test reconnect logic.""" + """Test a reconnect logic.""" with caplog.at_level(logging.ERROR), patch( "pyplumio.connection.Connection._connect", side_effect=(ConnectionFailedError, None), diff --git a/tests/test_devices.py b/tests/test_devices.py index 028b465e..e8647518 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -1,4 +1,4 @@ -"""Contains tests for devices.""" +"""Contains tests for the device handler classes.""" import asyncio from unittest.mock import AsyncMock, Mock, call, patch @@ -55,7 +55,6 @@ ThermostatParametersResponse, ) from pyplumio.helpers.schedule import Schedule -from pyplumio.helpers.typing import EventDataType from pyplumio.structures.ecomax_parameters import ( ATTR_ECOMAX_CONTROL, EcomaxBinaryParameter, @@ -86,6 +85,7 @@ ATTR_THERMOSTAT_COUNT, ATTR_THERMOSTAT_SENSORS, ) +from tests import load_json_parameters, load_json_test_data UNKNOWN_DEVICE: int = 99 UNKNOWN_FRAME: int = 99 @@ -151,14 +151,11 @@ async def test_async_setup_error() -> None: assert mock_request.await_count == len(DATA_FRAME_TYPES) -async def test_frame_versions_update( - ecomax: EcoMAX, messages: dict[FrameType, bytearray] -) -> None: +async def test_frame_versions_update(ecomax: EcoMAX) -> None: """Test requesting updated frames.""" + test_data = load_json_test_data("messages/sensor_data.json")[0] with patch("asyncio.Queue.put_nowait") as mock_put_nowait: - ecomax.handle_frame( - SensorDataMessage(message=messages[FrameType.MESSAGE_SENSOR_DATA]) - ) + ecomax.handle_frame(SensorDataMessage(message=test_data["message"])) await ecomax.wait_until_done() mock_put_nowait.assert_has_calls( @@ -171,13 +168,10 @@ async def test_frame_versions_update( ) -async def test_ecomax_data_callbacks( - ecomax: EcoMAX, messages: dict[FrameType, bytearray] -) -> None: +async def test_ecomax_data_callbacks(ecomax: EcoMAX) -> None: """Test callbacks that are dispatched on received data frames.""" - ecomax.handle_frame( - SensorDataMessage(message=messages[FrameType.MESSAGE_SENSOR_DATA]) - ) + test_data = load_json_test_data("messages/sensor_data.json")[0] + ecomax.handle_frame(SensorDataMessage(message=test_data["message"])) await ecomax.wait_until_done() heating_target = await ecomax.get("heating_target") assert heating_target == 41.0 @@ -189,13 +183,10 @@ async def test_ecomax_data_callbacks( assert ecomax_control.request.data == {ATTR_VALUE: 0} -async def test_ecomax_parameters_callbacks( - ecomax: EcoMAX, messages: dict[FrameType, bytearray] -) -> None: +async def test_ecomax_parameters_callbacks(ecomax: EcoMAX) -> None: """Test callbacks that are dispatched on received parameter frames.""" - ecomax.handle_frame( - EcomaxParametersResponse(message=messages[FrameType.RESPONSE_ECOMAX_PARAMETERS]) - ) + test_data = load_json_test_data("responses/ecomax_parameters.json")[0] + ecomax.handle_frame(EcomaxParametersResponse(message=test_data["message"])) await ecomax.wait_until_done() fuzzy_logic = await ecomax.get("fuzzy_logic") assert isinstance(fuzzy_logic, EcomaxBinaryParameter) @@ -253,44 +244,34 @@ async def test_fuel_consumption_callbacks(mock_time, caplog) -> None: assert "Skipping outdated fuel consumption" in caplog.text -async def test_regdata_callbacks( - ecomax: EcoMAX, messages: dict[FrameType, bytearray] -) -> None: +async def test_regdata_callbacks(ecomax: EcoMAX) -> None: """Test callbacks that are dispatched on received regdata.""" - ecomax.handle_frame( - DataSchemaResponse(message=messages[FrameType.RESPONSE_DATA_SCHEMA]) - ) - ecomax.handle_frame( - RegulatorDataMessage(message=messages[FrameType.MESSAGE_REGULATOR_DATA]) + test_schema, test_regdata = ( + load_json_test_data("responses/data_schema.json")[0], + load_json_test_data("messages/regulator_data.json")[0], ) + ecomax.handle_frame(DataSchemaResponse(message=test_schema["message"])) + ecomax.handle_frame(RegulatorDataMessage(message=test_regdata["message"])) await ecomax.wait_until_done() regdata = await ecomax.get(ATTR_REGDATA) assert isinstance(regdata, RegulatorData) - assert regdata.data[1792] == 0 - assert round(regdata.data[1024], 1) == 22.4 - assert regdata.data[1280] == 41 - assert regdata.data[183] == "0.0.0.0" - assert regdata.data[184] == "255.255.255.0" -async def test_regdata_callbacks_without_schema( - ecomax: EcoMAX, messages: dict[FrameType, bytearray] -) -> None: +async def test_regdata_callbacks_without_schema(ecomax: EcoMAX) -> None: """Test callbacks that are dispatched on received regdata.""" - ecomax.handle_frame( - RegulatorDataMessage(message=messages[FrameType.MESSAGE_REGULATOR_DATA]) - ) + test_data = load_json_test_data("messages/regulator_data.json")[0] + ecomax.handle_frame(RegulatorDataMessage(message=test_data["message"])) await ecomax.wait_until_done() assert ATTR_FRAME_VERSIONS in ecomax.data assert ATTR_REGDATA not in ecomax.data -async def test_mixer_sensors_callbacks( - ecomax: EcoMAX, messages: dict[FrameType, bytearray] -) -> None: +async def test_mixer_sensors_callbacks(ecomax: EcoMAX) -> None: """Test callbacks that are dispatched on receiving mixer sensors info.""" ecomax.handle_frame( - SensorDataMessage(message=messages[FrameType.MESSAGE_SENSOR_DATA]) + SensorDataMessage( + message=load_json_test_data("messages/sensor_data.json")[0]["message"] + ) ) await ecomax.wait_until_done() mixers = await ecomax.get(ATTR_MIXERS) @@ -314,13 +295,10 @@ async def test_mixer_sensors_callbacks_without_mixers(ecomax: EcoMAX) -> None: assert not await ecomax.get(ATTR_MIXER_SENSORS) -async def test_thermostat_sensors_callbacks( - ecomax: EcoMAX, messages: dict[FrameType, bytearray] -) -> None: +async def test_thermostat_sensors_callbacks(ecomax: EcoMAX) -> None: """Test callbacks that are dispatched on receiving thermostat sensors info.""" - ecomax.handle_frame( - SensorDataMessage(message=messages[FrameType.MESSAGE_SENSOR_DATA]) - ) + test_data = load_json_test_data("messages/sensor_data.json")[0] + ecomax.handle_frame(SensorDataMessage(message=test_data["message"])) await ecomax.wait_until_done() thermostats = await ecomax.get(ATTR_THERMOSTATS) assert len(thermostats) == 1 @@ -336,7 +314,7 @@ async def test_thermostat_sensors_callbacks( "thermostat_sensors": True, } thermostat_count = await ecomax.get(ATTR_THERMOSTAT_COUNT) - assert thermostat_count == 1 + assert thermostat_count == 2 async def test_thermostat_sensors_callbacks_without_thermostats(ecomax: EcoMAX) -> None: @@ -349,16 +327,11 @@ async def test_thermostat_sensors_callbacks_without_thermostats(ecomax: EcoMAX) assert not await ecomax.get(ATTR_THERMOSTAT_SENSORS) -async def test_thermostat_parameters_callbacks( - ecomax: EcoMAX, messages: dict[FrameType, bytearray] -) -> None: +async def test_thermostat_parameters_callbacks(ecomax: EcoMAX) -> None: """Test callbacks that are dispatched on receiving thermostat parameters.""" + test_data = load_json_test_data("responses/thermostat_parameters.json")[0] ecomax.handle_frame(Response(data={ATTR_THERMOSTAT_COUNT: 3})) - ecomax.handle_frame( - ThermostatParametersResponse( - message=messages[FrameType.RESPONSE_THERMOSTAT_PARAMETERS] - ) - ) + ecomax.handle_frame(ThermostatParametersResponse(message=test_data["message"])) await ecomax.wait_until_done() thermostats = await ecomax.get(ATTR_THERMOSTATS) assert len(thermostats) == 1 @@ -381,30 +354,27 @@ async def test_thermostat_parameters_callbacks( async def test_thermostat_parameters_callbacks_without_thermostats( - ecomax: EcoMAX, messages: dict[FrameType, bytearray] + ecomax: EcoMAX, ) -> None: """Test callbacks that are dispatched on receiving thermostat parameters without any thermostats.""" + test_data = load_json_test_data("responses/thermostat_parameters.json")[0] ecomax.handle_frame(Response(data={ATTR_THERMOSTAT_COUNT: 0})) - ecomax.handle_frame( - ThermostatParametersResponse( - message=messages[FrameType.RESPONSE_THERMOSTAT_PARAMETERS] - ) - ) + ecomax.handle_frame(ThermostatParametersResponse(message=test_data["message"])) await ecomax.wait_until_done() assert not await ecomax.get(ATTR_THERMOSTAT_PARAMETERS) thermostat_profile = await ecomax.get(ATTR_THERMOSTAT_PROFILE) assert thermostat_profile is None -async def test_thermostat_profile_callbacks( - ecomax: EcoMAX, messages: dict[FrameType, bytearray] -) -> None: +async def test_thermostat_profile_callbacks(ecomax: EcoMAX) -> None: """Test callbacks that are dispatched on receiving thermostat profile.""" ecomax.handle_frame(Response(data={ATTR_THERMOSTAT_COUNT: 3})) ecomax.handle_frame( ThermostatParametersResponse( - message=messages[FrameType.RESPONSE_THERMOSTAT_PARAMETERS] + message=load_json_test_data("responses/thermostat_parameters.json")[0][ + "message" + ] ) ) await ecomax.wait_until_done() @@ -426,13 +396,10 @@ async def test_thermostat_profile_callbacks( assert await ecomax.get(ATTR_THERMOSTAT_PROFILE) is None -async def test_mixer_parameters_callbacks( - ecomax: EcoMAX, messages: dict[FrameType, bytearray] -) -> None: +async def test_mixer_parameters_callbacks(ecomax: EcoMAX) -> None: """Test callbacks that are dispatched on receiving mixer parameters.""" - ecomax.handle_frame( - MixerParametersResponse(message=messages[FrameType.RESPONSE_MIXER_PARAMETERS]) - ) + test_data = load_json_test_data("responses/mixer_parameters.json")[0] + ecomax.handle_frame(MixerParametersResponse(message=test_data["message"])) await ecomax.wait_until_done() mixers = await ecomax.get(ATTR_MIXERS) assert len(mixers) == 1 @@ -469,15 +436,13 @@ async def test_mixer_parameters_callbacks_without_mixers(ecomax: EcoMAX) -> None assert not await ecomax.get(ATTR_MIXER_PARAMETERS) -async def test_schedule_callback( - ecomax: EcoMAX, - messages: dict[FrameType, bytearray], - data: dict[FrameType, EventDataType], -) -> None: +@pytest.mark.parametrize( + "message, data", + load_json_parameters("responses/schedules.json"), +) +async def test_schedule_callback(ecomax: EcoMAX, message, data) -> None: """Test callback that is dispatched on receiving schedule data.""" - ecomax.handle_frame( - SchedulesResponse(message=messages[FrameType.RESPONSE_SCHEDULES]) - ) + ecomax.handle_frame(SchedulesResponse(message=message)) schedules = await ecomax.get(ATTR_SCHEDULES) assert len(schedules) == 1 heating_schedule = schedules["heating"] @@ -500,7 +465,7 @@ async def test_schedule_callback( ATTR_SCHEDULE: ecomax.data[ATTR_SCHEDULES]["heating"], } - schedule_data = data[FrameType.RESPONSE_SCHEDULES][ATTR_SCHEDULES][0][1] + schedule_data = data[ATTR_SCHEDULES][0][1] for index, weekday in enumerate( ("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday") ): @@ -533,20 +498,19 @@ async def test_request_error(ecomax: EcoMAX) -> None: @patch("pyplumio.helpers.parameter.Parameter.is_changed", False) -async def test_set(ecomax: EcoMAX, messages: dict[FrameType, bytearray]) -> None: +async def test_set(ecomax: EcoMAX) -> None: """Test setting parameter value via set helper.""" - ecomax.handle_frame( - EcomaxParametersResponse(message=messages[FrameType.RESPONSE_ECOMAX_PARAMETERS]) + test_ecomax_data, test_thermostat_data, test_mixer_data = ( + load_json_test_data("responses/ecomax_parameters.json")[0], + load_json_test_data("responses/thermostat_parameters.json")[0], + load_json_test_data("responses/mixer_parameters.json")[0], ) + ecomax.handle_frame(EcomaxParametersResponse(message=test_ecomax_data["message"])) ecomax.handle_frame(Response(data={ATTR_THERMOSTAT_COUNT: 3})) ecomax.handle_frame( - ThermostatParametersResponse( - message=messages[FrameType.RESPONSE_THERMOSTAT_PARAMETERS] - ) - ) - ecomax.handle_frame( - MixerParametersResponse(message=messages[FrameType.RESPONSE_MIXER_PARAMETERS]) + ThermostatParametersResponse(message=test_thermostat_data["message"]) ) + ecomax.handle_frame(MixerParametersResponse(message=test_mixer_data["message"])) # Test setting an ecomax parameter. assert await ecomax.set("fuel_flow_kg_h", 13.0) @@ -617,10 +581,12 @@ async def test_turn_off_nowait(mock_create_task, mock_turn_off, ecomax: EcoMAX) mock_turn_off.assert_called_once() -async def test_shutdown(ecomax: EcoMAX, messages: dict[FrameType, bytearray]) -> None: +async def test_shutdown(ecomax: EcoMAX) -> None: """Test device tasks shutdown.""" ecomax.handle_frame( - SensorDataMessage(message=messages[FrameType.MESSAGE_SENSOR_DATA]) + SensorDataMessage( + message=load_json_test_data("messages/sensor_data.json")[0]["message"] + ) ) await ecomax.wait_until_done() diff --git a/tests/test_filters.py b/tests/test_filters.py index 536f26fe..366056da 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,4 +1,4 @@ -"""Contains tests for callback filters.""" +"""Contains tests for the filter classes.""" from datetime import datetime from unittest.mock import AsyncMock, patch diff --git a/tests/test_init.py b/tests/test_init.py index 88e79a89..69a2f177 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,4 +1,4 @@ -"""Contains tests for init.""" +"""Contains tests for the init module.""" from typing import Final diff --git a/tests/test_main.py b/tests/test_main.py index 0d5ba925..cb81d286 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,4 @@ -"""Contains tests for main.""" +"""Contains tests for the main module.""" from unittest.mock import AsyncMock, call, patch diff --git a/tests/test_protocol.py b/tests/test_protocol.py index c6feb3c9..4a1bef64 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -1,4 +1,4 @@ -"""Contains tests for protocol.""" +"""Contains tests for the protocol classes.""" import asyncio import logging diff --git a/tests/test_stream.py b/tests/test_stream.py index d6f98e4e..e30cae81 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -1,4 +1,4 @@ -"""Contains tests for frame reader and writer.""" +"""Contains tests for the frame reader and writer classes.""" import asyncio from collections.abc import Generator diff --git a/tests/test_utils.py b/tests/test_utils.py index 6b8e8b4d..4d6b7cf3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,4 +1,4 @@ -"""Contains tests for utility functions.""" +"""Contains tests for the utility functions.""" from pyplumio import utils diff --git a/tests/testdata/messages/regulator_data.json b/tests/testdata/messages/regulator_data.json new file mode 100644 index 00000000..5b084a00 --- /dev/null +++ b/tests/testdata/messages/regulator_data.json @@ -0,0 +1,97 @@ +[ + { + "id": "EM350P2_regulator_data", + "message": { + "items": [ + "62640001075500005400006167013D9ED236010064010040000007010050F53142EDD52140C85", + "1E441B3474442847E5E4220BE43C3000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000037000014332400" + ], + "__bytearray__": true + }, + "data": { + "frame_versions": { + "85": 0, + "84": 0, + "97": 359, + "61": 53918, + "54": 1, + "100": 1, + "64": 0 + }, + "regdata": { + "1792": 7, + "1536": true, + "1538": false, + "1542": false, + "1541": false, + "5": false, + "3": false, + "1543": false, + "1544": false, + "1545": false, + "1546": false, + "1547": false, + "1548": false, + "1549": false, + "2": false, + "6": false, + "1024": 44.48956298828125, + "1027": 2.528681993484497, + "1026": 28.539932250976562, + "1030": 49.07001876831055, + "1025": 55.62355041503906, + "29": 0.0, + "1028": 0.0, + "1032": 0.0, + "1031": 0.0, + "1029": 0.0, + "25": 0.0, + "27": 0.0, + "1280": 55, + "1283": 0, + "1282": 0, + "1287": 20, + "1281": 51, + "1288": 36, + "2048": 0 + } + } + }, + { + "id": "unknown_regulator_data_version", + "message": { + "items": [ + "62640002" + ], + "__bytearray__": true + }, + "data": {} + }, + { + "id": "incomplete_boolean", + "message": { + "items": [ + "62640001075500005400006167013D9ED23601006401004000000702" + ], + "__bytearray__": true + }, + "data": { + "frame_versions": { + "85": 0, + "84": 0, + "97": 359, + "61": 53918, + "54": 1, + "100": 1, + "64": 0 + }, + "regdata": { + "1536": true, + "1537": true, + "1538": true, + "1792": 2 + } + } + } +] \ No newline at end of file diff --git a/tests/testdata/messages/sensor_data.json b/tests/testdata/messages/sensor_data.json new file mode 100644 index 00000000..01610453 --- /dev/null +++ b/tests/testdata/messages/sensor_data.json @@ -0,0 +1,175 @@ +[ + { + "id": "full_sensor_data", + "message": { + "items": [ + "0755F7B15420BE5698FA3601003802003901003D18310000000000FF0300000900D012B34101F", + "FFFFFFF02FFFFFFFF03FFFFFFFF04FFFFFFFF05FFFFFFFF060000000007FFFFFFFF08FFFFFFFF", + "29002D800020000000000000000000000000000001120B3A4B01FFFFFFFF120A4801022800050", + "20300002E42000048420200000E420000000005FFFFFFFF28000800FFFFFFFF28000800FFFFFF", + "FF28000800FFFFFFFF280008000000A04128000800" + ], + "__bytearray__": true + }, + "data": { + "sensors": { + "frame_versions": { + "85": 45559, + "84": 48672, + "86": 64152, + "54": 1, + "56": 2, + "57": 1, + "61": 12568 + }, + "state": { + "value": 0, + "__module__": "pyplumio.const", + "__class__": "DeviceState" + }, + "fan": false, + "feeder": false, + "heating_pump": false, + "water_heater_pump": false, + "circulation_pump": false, + "lighter": false, + "alarm": false, + "outer_boiler": false, + "fan2_exhaust": false, + "feeder2": false, + "outer_feeder": false, + "solar_pump": false, + "fireplace_pump": false, + "gcz_contact": false, + "blow_fan1": false, + "blow_fan2": false, + "heating_pump_flag": true, + "water_heater_pump_flag": true, + "circulation_pump_flag": true, + "solar_pump_flag": false, + "heating_temp": 22.384185791015625, + "optical_temp": 0.0, + "heating_target": 41, + "heating_status": 0, + "water_heater_target": 45, + "water_heater_status": 128, + "pending_alerts": 0, + "fuel_level": 32, + "transmission": 0, + "fan_power": 0.0, + "load": 0, + "power": 0.0, + "fuel_consumption": 0.0, + "thermostat": 1, + "modules": { + "module_a": "18.11.58.K1", + "module_b": null, + "module_c": null, + "ecolambda": null, + "ecoster": null, + "panel": "18.10.72", + "__module__": "pyplumio.structures.modules", + "__class__": "ConnectedModules" + }, + "lambda_level": 4.0, + "lambda_state": 1, + "lambda_target": 2, + "thermostat_sensors": { + "0": { + "state": 3, + "current_temp": 43.5, + "target_temp": 50.0, + "contacts": true, + "schedule": false + } + }, + "thermostat_count": 2, + "mixer_sensors": { + "4": { + "current_temp": 20.0, + "target_temp": 40, + "pump": false + } + }, + "mixer_count": 5 + } + } + }, + { + "id": "short_sensor_data_without_thermostats", + "message": { + "items": [ + "0755F7B15420BE5698FA3601003802003901003D18310C00000000FF0300000900D012B34101F", + "FFFFFFF02FFFFFFFF03FFFFFFFF04FFFFFFFF05FFFFFFFF060000000007FFFFFFFF08FFFFFFFF", + "29002D8000FF00FFFFFFFFFFFFFFFFFFFFFFFFFF01120B3A4B01FFFFFFFF120A48FFFF05FFFFF", + "FFF28000800FFFFFFFF28000800FFFFFFFF28000800FFFFFFFF280008000000A04128000800" + ], + "__bytearray__": true + }, + "data": { + "sensors": { + "frame_versions": { + "85": 45559, + "84": 48672, + "86": 64152, + "54": 1, + "56": 2, + "57": 1, + "61": 12568 + }, + "state": { + "value": 1, + "__module__": "pyplumio.const", + "__class__": "DeviceState" + }, + "fan": false, + "feeder": false, + "heating_pump": false, + "water_heater_pump": false, + "circulation_pump": false, + "lighter": false, + "alarm": false, + "outer_boiler": false, + "fan2_exhaust": false, + "feeder2": false, + "outer_feeder": false, + "solar_pump": false, + "fireplace_pump": false, + "gcz_contact": false, + "blow_fan1": false, + "blow_fan2": false, + "heating_pump_flag": true, + "water_heater_pump_flag": true, + "circulation_pump_flag": true, + "solar_pump_flag": false, + "heating_temp": 22.384185791015625, + "optical_temp": 0.0, + "heating_target": 41, + "heating_status": 0, + "water_heater_target": 45, + "water_heater_status": 128, + "pending_alerts": 0, + "transmission": 0, + "thermostat": 1, + "modules": { + "module_a": "18.11.58.K1", + "module_b": null, + "module_c": null, + "ecolambda": null, + "ecoster": null, + "panel": "18.10.72", + "__module__": "pyplumio.structures.modules", + "__class__": "ConnectedModules" + }, + "mixer_sensors": { + "4": { + "current_temp": 20.0, + "target_temp": 40, + "pump": false + } + }, + "mixer_count": 5 + } + } + } +] \ No newline at end of file diff --git a/tests/testdata/requests/ecomax_control.json b/tests/testdata/requests/ecomax_control.json new file mode 100644 index 00000000..ea08771b --- /dev/null +++ b/tests/testdata/requests/ecomax_control.json @@ -0,0 +1,26 @@ +[ + { + "id": "ecomax_control_turn_on", + "message": { + "items": [ + "01" + ], + "__bytearray__": true + }, + "data": { + "value": 1 + } + }, + { + "id": "ecomax_control_turn_off", + "message": { + "items": [ + "00" + ], + "__bytearray__": true + }, + "data": { + "value": 0 + } + } +] \ No newline at end of file diff --git a/tests/testdata/requests/ecomax_parameters.json b/tests/testdata/requests/ecomax_parameters.json new file mode 100644 index 00000000..3675b71c --- /dev/null +++ b/tests/testdata/requests/ecomax_parameters.json @@ -0,0 +1,41 @@ +[ + { + "id": "get_ecomax_parameters_from_zero_no_limit", + "message": { + "items": [ + "FF00" + ], + "__bytearray__": true + }, + "data": { + "index": 0, + "count": 255 + } + }, + { + "id": "get_ecomax_parameters_from_0_limit_10", + "message": { + "items": [ + "0A00" + ], + "__bytearray__": true + }, + "data": { + "index": 0, + "count": 10 + } + }, + { + "id": "get_ecomax_parameters_from_50_limit_10", + "message": { + "items": [ + "0A32" + ], + "__bytearray__": true + }, + "data": { + "index": 50, + "count": 10 + } + } +] \ No newline at end of file diff --git a/tests/testdata/requests/set_ecomax_parameter.json b/tests/testdata/requests/set_ecomax_parameter.json new file mode 100644 index 00000000..626f2a8b --- /dev/null +++ b/tests/testdata/requests/set_ecomax_parameter.json @@ -0,0 +1,15 @@ +[ + { + "id": "set_airflow_power_100_to_80", + "message": { + "items": [ + "0050" + ], + "__bytearray__": true + }, + "data": { + "index": 0, + "value": 80 + } + } +] \ No newline at end of file diff --git a/tests/testdata/requests/set_mixer_parameter.json b/tests/testdata/requests/set_mixer_parameter.json new file mode 100644 index 00000000..7fb24deb --- /dev/null +++ b/tests/testdata/requests/set_mixer_parameter.json @@ -0,0 +1,30 @@ +[ + { + "id": "set_mixer_0_target_temp_to_40", + "message": { + "items": [ + "000028" + ], + "__bytearray__": true + }, + "data": { + "index": 0, + "value": 40, + "device_index": 0 + } + }, + { + "id": "set_mixer_1_min_target_temp_to_30", + "message": { + "items": [ + "01001E" + ], + "__bytearray__": true + }, + "data": { + "index": 0, + "value": 30, + "device_index": 1 + } + } +] \ No newline at end of file diff --git a/tests/testdata/requests/set_schedule.json b/tests/testdata/requests/set_schedule.json new file mode 100644 index 00000000..0a53ab6d --- /dev/null +++ b/tests/testdata/requests/set_schedule.json @@ -0,0 +1,369 @@ +[ + { + "id": "set_heating_schedule", + "message": { + "items": [ + "010000050000FFFFFFFE0000FFFFFFFE0000FFFFFFFE0000FFFFFFFE0000FFFFFFFE0000FFFFF", + "FFE0000FFFFFFFE" + ], + "__bytearray__": true + }, + "data": { + "type": "heating", + "switch": 0, + "parameter": 5, + "schedule": [ + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ], + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ], + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ], + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ], + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ], + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ], + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ] + ] + } + } +] \ No newline at end of file diff --git a/tests/testdata/requests/set_thermostat_parameter.json b/tests/testdata/requests/set_thermostat_parameter.json new file mode 100644 index 00000000..3b63f389 --- /dev/null +++ b/tests/testdata/requests/set_thermostat_parameter.json @@ -0,0 +1,32 @@ +[ + { + "id": "set_thermostat_0_correction_to_5", + "message": { + "items": [ + "0305" + ], + "__bytearray__": true + }, + "data": { + "index": 3, + "value": 5, + "offset": 0, + "size": 1 + } + }, + { + "id": "set_thermostat_1_party_target_temp_to_42", + "message": { + "items": [ + "0D2A00" + ], + "__bytearray__": true + }, + "data": { + "index": 1, + "value": 42, + "offset": 12, + "size": 2 + } + } +] \ No newline at end of file diff --git a/tests/testdata/responses/alerts.json b/tests/testdata/responses/alerts.json new file mode 100644 index 00000000..85ccfed4 --- /dev/null +++ b/tests/testdata/responses/alerts.json @@ -0,0 +1,68 @@ +[ + { + "id": "alerts", + "message": { + "items": [ + "6400021A5493382B9B94382B009C97372B00000000" + ], + "__bytearray__": true + }, + "data": { + "alerts": [ + { + "code": 26, + "from_dt": { + "year": 2022, + "month": 7, + "day": 23, + "hour": 16, + "minute": 27, + "__module__": "datetime", + "__class__": "datetime" + }, + "to_dt": { + "year": 2022, + "month": 7, + "day": 23, + "hour": 16, + "minute": 32, + "second": 27, + "__module__": "datetime", + "__class__": "datetime" + }, + "__module__": "pyplumio.structures.alerts", + "__class__": "Alert" + }, + { + "code": { + "value": 0, + "__module__": "pyplumio.const", + "__class__": "AlertType" + }, + "from_dt": { + "year": 2022, + "month": 7, + "day": 22, + "hour": 22, + "minute": 33, + "__module__": "datetime", + "__class__": "datetime" + }, + "to_dt": null, + "__module__": "pyplumio.structures.alerts", + "__class__": "Alert" + } + ] + } + }, + { + "id": "empty_alerts", + "message": { + "items": [ + "000000" + ], + "__bytearray__": true + }, + "data": {} + } +] \ No newline at end of file diff --git a/tests/testdata/responses/data_schema.json b/tests/testdata/responses/data_schema.json new file mode 100644 index 00000000..fe5deb64 --- /dev/null +++ b/tests/testdata/responses/data_schema.json @@ -0,0 +1,481 @@ +[ + { + "id": "EM350P2_data_schema", + "message": { + "items": [ + "28000400070A00060A02060A06060A05060A05000A03000A07060A08060A09060A0A060A0B060", + "A0C060A0D060A02000A06000A0600070004070304070204070604070104071D00070404070804", + "070704070504071900071B00071D00071D00071D00071D0004000504030504020504070504010", + "5040805040008" + ], + "__bytearray__": true + }, + "data": { + "schema": [ + { + "items": [ + 1792, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Byte" + } + ], + "__tuple__": true + }, + { + "items": [ + 1536, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1538, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1542, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1541, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 5, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 3, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1543, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1544, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1545, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1546, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1547, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1548, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1549, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 2, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 6, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 6, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1024, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 1027, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 1026, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 1030, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 1025, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 29, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 1028, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 1032, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 1031, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 1029, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 25, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 27, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 29, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 29, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 29, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 29, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Float" + } + ], + "__tuple__": true + }, + { + "items": [ + 1280, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Byte" + } + ], + "__tuple__": true + }, + { + "items": [ + 1283, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Byte" + } + ], + "__tuple__": true + }, + { + "items": [ + 1282, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Byte" + } + ], + "__tuple__": true + }, + { + "items": [ + 1287, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Byte" + } + ], + "__tuple__": true + }, + { + "items": [ + 1281, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Byte" + } + ], + "__tuple__": true + }, + { + "items": [ + 1288, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Byte" + } + ], + "__tuple__": true + }, + { + "items": [ + 2048, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Byte" + } + ], + "__tuple__": true + } + ] + } + }, + { + "id": "empty_data_schema", + "message": { + "items": [ + "0000" + ], + "__bytearray__": true + }, + "data": {} + }, + { + "id": "incomplete_boolean", + "message": { + "items": [ + "04000A02060A00060A0106040007" + ], + "__bytearray__": true + }, + "data": { + "schema": [ + { + "items": [ + 1538, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1536, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1537, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Boolean" + } + ], + "__tuple__": true + }, + { + "items": [ + 1792, + { + "__module__": "pyplumio.helpers.data_types", + "__class__": "Byte" + } + ], + "__tuple__": true + } + ] + } + } +] \ No newline at end of file diff --git a/tests/testdata/responses/device_available.json b/tests/testdata/responses/device_available.json new file mode 100644 index 00000000..685db2f7 --- /dev/null +++ b/tests/testdata/responses/device_available.json @@ -0,0 +1,35 @@ +[ + { + "id": "EN300_device_available", + "message": { + "items": [ + "01C0A80102FFFFFF00C0A8010101C0A80202FFFFFF00C0A802010101640100000000057465737", + "473" + ], + "__bytearray__": true + }, + "data": { + "network": { + "eth": { + "ip": "192.168.1.2", + "netmask": "255.255.255.0", + "gateway": "192.168.1.1", + "status": true, + "__module__": "pyplumio.structures.network_info", + "__class__": "EthernetParameters" + }, + "wlan": { + "ip": "192.168.2.2", + "netmask": "255.255.255.0", + "gateway": "192.168.2.1", + "status": true, + "ssid": "tests", + "__module__": "pyplumio.structures.network_info", + "__class__": "WirelessParameters" + }, + "__module__": "pyplumio.structures.network_info", + "__class__": "NetworkInfo" + } + } + } +] \ No newline at end of file diff --git a/tests/testdata/responses/ecomax_parameters.json b/tests/testdata/responses/ecomax_parameters.json new file mode 100644 index 00000000..d13f07ea --- /dev/null +++ b/tests/testdata/responses/ecomax_parameters.json @@ -0,0 +1,714 @@ +[ + { + "id": "EM350P2_parameters", + "message": { + "items": [ + "00008B3D3D643C293C28143BFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "FFFFFFFFFFFFF1401FA03011E01011E05011E01000100003C3C0064FFFFFFFFFFFF140A64FFFF", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E146404011E000", + "0640801FA3228550A0A1E1E1432FFFFFFFFFFFF0A0AF0FFFFFFFFFFFF0F0A14FFFFFFFFFFFF32", + "2896FFFFFFFFFFFF02010F03010A28143CFFFFFFFFFFFFFFFFFFFFFFFF3C01FA1E1432FFFFFFF", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D01FAFFFFFF0201642F01F", + "A0A0A1EFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF413250321E50503C5A321E", + "50000063FFFFFFFFFFFF05030F0000010D0128140028FFFFFFFFFFFF00000105001EFFFFFF5A5", + "55F3C285AFFFFFFFFFFFFFFFFFF3328462814374628500200020A011E00000100000210051E0A", + "010F030063FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + ], + "__bytearray__": true + }, + "data": { + "ecomax_parameters": [ + { + "items": [ + 0, + { + "value": 61, + "min_value": 61, + "max_value": 100, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 1, + { + "value": 60, + "min_value": 41, + "max_value": 60, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 2, + { + "value": 40, + "min_value": 20, + "max_value": 59, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 14, + { + "value": 20, + "min_value": 1, + "max_value": 250, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 15, + { + "value": 3, + "min_value": 1, + "max_value": 30, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 16, + { + "value": 1, + "min_value": 1, + "max_value": 30, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 17, + { + "value": 5, + "min_value": 1, + "max_value": 30, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 18, + { + "value": 1, + "min_value": 0, + "max_value": 1, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 19, + { + "value": 0, + "min_value": 0, + "max_value": 60, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 20, + { + "value": 60, + "min_value": 0, + "max_value": 100, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 23, + { + "value": 20, + "min_value": 10, + "max_value": 100, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 35, + { + "value": 30, + "min_value": 20, + "max_value": 100, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 36, + { + "value": 4, + "min_value": 1, + "max_value": 30, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 37, + { + "value": 0, + "min_value": 0, + "max_value": 100, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 38, + { + "value": 8, + "min_value": 1, + "max_value": 250, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 39, + { + "value": 50, + "min_value": 40, + "max_value": 85, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 40, + { + "value": 10, + "min_value": 10, + "max_value": 30, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 41, + { + "value": 30, + "min_value": 20, + "max_value": 50, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 44, + { + "value": 10, + "min_value": 10, + "max_value": 240, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 47, + { + "value": 15, + "min_value": 10, + "max_value": 20, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 50, + { + "value": 50, + "min_value": 40, + "max_value": 150, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 53, + { + "value": 2, + "min_value": 1, + "max_value": 15, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 54, + { + "value": 3, + "min_value": 1, + "max_value": 10, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 55, + { + "value": 40, + "min_value": 20, + "max_value": 60, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 60, + { + "value": 60, + "min_value": 1, + "max_value": 250, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 61, + { + "value": 30, + "min_value": 20, + "max_value": 50, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 85, + { + "value": 125, + "min_value": 1, + "max_value": 250, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 87, + { + "value": 2, + "min_value": 1, + "max_value": 100, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 88, + { + "value": 47, + "min_value": 1, + "max_value": 250, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 89, + { + "value": 10, + "min_value": 10, + "max_value": 30, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 98, + { + "value": 65, + "min_value": 50, + "max_value": 80, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 99, + { + "value": 50, + "min_value": 30, + "max_value": 80, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 100, + { + "value": 80, + "min_value": 60, + "max_value": 90, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 101, + { + "value": 50, + "min_value": 30, + "max_value": 80, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 102, + { + "value": 0, + "min_value": 0, + "max_value": 99, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 105, + { + "value": 5, + "min_value": 3, + "max_value": 15, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 106, + { + "value": 0, + "min_value": 0, + "max_value": 1, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 107, + { + "value": 13, + "min_value": 1, + "max_value": 40, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 108, + { + "value": 20, + "min_value": 0, + "max_value": 40, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 111, + { + "value": 0, + "min_value": 0, + "max_value": 1, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 112, + { + "value": 5, + "min_value": 0, + "max_value": 30, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 114, + { + "value": 90, + "min_value": 85, + "max_value": 95, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 115, + { + "value": 60, + "min_value": 40, + "max_value": 90, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 119, + { + "value": 51, + "min_value": 40, + "max_value": 70, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 120, + { + "value": 40, + "min_value": 20, + "max_value": 55, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 121, + { + "value": 70, + "min_value": 40, + "max_value": 80, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 122, + { + "value": 2, + "min_value": 0, + "max_value": 2, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 123, + { + "value": 10, + "min_value": 1, + "max_value": 30, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 124, + { + "value": 0, + "min_value": 0, + "max_value": 1, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 125, + { + "value": 0, + "min_value": 0, + "max_value": 2, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 126, + { + "value": 16, + "min_value": 5, + "max_value": 30, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 127, + { + "value": 10, + "min_value": 1, + "max_value": 15, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 128, + { + "value": 3, + "min_value": 0, + "max_value": 99, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + } + ] + } + } +] \ No newline at end of file diff --git a/tests/testdata/responses/mixer_parameters.json b/tests/testdata/responses/mixer_parameters.json new file mode 100644 index 00000000..efcef450 --- /dev/null +++ b/tests/testdata/responses/mixer_parameters.json @@ -0,0 +1,107 @@ +[ + { + "id": "1_mixer_detected", + "message": { + "items": [ + "00000601281E3C141E2850465A140A1E0100010D0A1E" + ], + "__bytearray__": true + }, + "data": { + "mixer_parameters": { + "0": [ + { + "items": [ + 0, + { + "value": 40, + "min_value": 30, + "max_value": 60, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 1, + { + "value": 20, + "min_value": 30, + "max_value": 40, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 2, + { + "value": 80, + "min_value": 70, + "max_value": 90, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 3, + { + "value": 20, + "min_value": 10, + "max_value": 30, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 4, + { + "value": 1, + "min_value": 0, + "max_value": 1, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 5, + { + "value": 13, + "min_value": 10, + "max_value": 30, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + } + ] + } + } + }, + { + "id": "no_mixers_detected", + "message": { + "items": [ + "00000201" + ], + "__bytearray__": true + }, + "data": { + "mixer_parameters": {} + } + } +] \ No newline at end of file diff --git a/tests/testdata/responses/password.json b/tests/testdata/responses/password.json new file mode 100644 index 00000000..be98b54b --- /dev/null +++ b/tests/testdata/responses/password.json @@ -0,0 +1,26 @@ +[ + { + "id": "EM_service_password_0000", + "message": { + "items": [ + "0430303030" + ], + "__bytearray__": true + }, + "data": { + "password": "0000" + } + }, + { + "id": "EM_service_password_1234", + "message": { + "items": [ + "0431323334" + ], + "__bytearray__": true + }, + "data": { + "password": "1234" + } + } +] \ No newline at end of file diff --git a/tests/testdata/responses/program_version.json b/tests/testdata/responses/program_version.json new file mode 100644 index 00000000..1e5513fa --- /dev/null +++ b/tests/testdata/responses/program_version.json @@ -0,0 +1,18 @@ +[ + { + "id": "EN300_program_version", + "message": { + "items": [ + "FFFF057A0000000001000000000056" + ], + "__bytearray__": true + }, + "data": { + "version": { + "software": "1.0.0", + "__module__": "pyplumio.structures.program_version", + "__class__": "VersionInfo" + } + } + } +] \ No newline at end of file diff --git a/tests/testdata/responses/schedules.json b/tests/testdata/responses/schedules.json new file mode 100644 index 00000000..4003a43a --- /dev/null +++ b/tests/testdata/responses/schedules.json @@ -0,0 +1,402 @@ +[ + { + "id": "EM_heating_schedule_and_parameter", + "message": { + "items": [ + "100101000005001E0000FFFFFFFE0000FFFFFFFE0000FFFFFFFE0000FFFFFFFE0000FFFFFFFE0", + "000FFFFFFFE0000FFFFFFFE" + ], + "__bytearray__": true + }, + "data": { + "schedules": [ + { + "items": [ + 0, + [ + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ], + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ], + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ], + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ], + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ], + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ], + [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false + ] + ] + ], + "__tuple__": true + } + ], + "schedule_parameters": [ + { + "items": [ + 0, + { + "value": 0, + "min_value": 0, + "max_value": 1, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 1, + { + "value": 5, + "min_value": 0, + "max_value": 30, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + } + ] + } + } +] \ No newline at end of file diff --git a/tests/testdata/responses/thermostat_parameters.json b/tests/testdata/responses/thermostat_parameters.json new file mode 100644 index 00000000..28419643 --- /dev/null +++ b/tests/testdata/responses/thermostat_parameters.json @@ -0,0 +1,199 @@ +[ + { + "id": "3_thermostats_connected", + "message": { + "items": [ + "000025000005000007DC0064005E01960064005E01643C8C02003C01003C01003C0A003C09003", + "2DE0064005E01D40064005E015A0032002C01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + "FFFFFFFFFF" + ], + "__bytearray__": true + }, + "data": { + "thermostat_count": 3, + "thermostat_profile": { + "value": 0, + "min_value": 0, + "max_value": 5, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + }, + "thermostat_parameters": { + "0": [ + { + "items": [ + 0, + { + "value": 0, + "min_value": 0, + "max_value": 7, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 1, + { + "value": 220, + "min_value": 100, + "max_value": 350, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 2, + { + "value": 150, + "min_value": 100, + "max_value": 350, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 3, + { + "value": 100, + "min_value": 60, + "max_value": 140, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 4, + { + "value": 2, + "min_value": 0, + "max_value": 60, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 5, + { + "value": 1, + "min_value": 0, + "max_value": 60, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 6, + { + "value": 1, + "min_value": 0, + "max_value": 60, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 7, + { + "value": 10, + "min_value": 0, + "max_value": 60, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 8, + { + "value": 9, + "min_value": 0, + "max_value": 50, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 9, + { + "value": 222, + "min_value": 100, + "max_value": 350, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 10, + { + "value": 212, + "min_value": 100, + "max_value": 350, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + }, + { + "items": [ + 11, + { + "value": 90, + "min_value": 50, + "max_value": 300, + "__module__": "pyplumio.helpers.parameter", + "__class__": "ParameterValues" + } + ], + "__tuple__": true + } + ] + } + } + }, + { + "id": "no_thermostats_connected", + "message": { + "items": [ + "00000300FFFFFFFFFFFFFFFFFF" + ], + "__bytearray__": true + }, + "data": { + "thermostat_count": 3, + "thermostat_profile": null, + "thermostat_parameters": {} + } + } +] \ No newline at end of file diff --git a/tests/testdata/responses/uid.json b/tests/testdata/responses/uid.json new file mode 100644 index 00000000..c59076aa --- /dev/null +++ b/tests/testdata/responses/uid.json @@ -0,0 +1,23 @@ +[ + { + "id": "EM350P2_uid", + "message": { + "items": [ + "005A000B001600110D3833383655395A0000000A454D33353050322D5A46" + ], + "__bytearray__": true + }, + "data": { + "product": { + "type": 0, + "id": 90, + "uid": "D251PAKR3GCPZ1K8G05G0", + "logo": 23040, + "image": 2816, + "model": "EM350P2-ZF", + "__module__": "pyplumio.structures.product_info", + "__class__": "ProductInfo" + } + } + } +] \ No newline at end of file