From 4da6e3eb18eae9b24b4300274e307db0af8633de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Mind=C3=AAllo=20de=20Andrade?= Date: Tue, 9 Jul 2024 06:58:55 -0300 Subject: [PATCH] feat(c3): temperature step as customize (#209) ## Summary by CodeRabbit - **New Features** - Added ability to set a custom temperature step for Midea C3 devices. - **Tests** - Introduced tests for the Midea C3 device to ensure proper functionality and handling of various scenarios. --- midealocal/devices/c3/__init__.py | 34 +++- tests/devices/c3/__init__.py | 1 + tests/devices/c3/device_c3_test.py | 244 +++++++++++++++++++++++++++++ 3 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 tests/devices/c3/__init__.py create mode 100644 tests/devices/c3/device_c3_test.py diff --git a/midealocal/devices/c3/__init__.py b/midealocal/devices/c3/__init__.py index 41233232..0c9dd9b0 100644 --- a/midealocal/devices/c3/__init__.py +++ b/midealocal/devices/c3/__init__.py @@ -1,5 +1,6 @@ """Midea local C3 device.""" +import json import logging from enum import IntEnum, StrEnum from typing import Any @@ -83,7 +84,7 @@ def __init__( protocol: int, model: str, subtype: int, - customize: str, # noqa: ARG002 + customize: str, ) -> None: """Initialize Midea C3 device.""" super().__init__( @@ -140,6 +141,14 @@ def __init__( DeviceAttributes.error_code: 0, }, ) + self._default_temperature_step: float = 0.5 + self._temperature_step: float = 0.5 + self.set_customize(customize) + + @property + def temperature_step(self) -> float | None: + """Midea C3 device temperature step.""" + return self._temperature_step def build_query(self) -> list[MessageQuery]: """Midea C3 device build query.""" @@ -309,6 +318,29 @@ def set_target_temperature( message.mode = mode self.build_send(message) + def set_customize(self, customize: str) -> None: + """Midea C3 device set customize.""" + self._temperature_step = self._default_temperature_step + if customize and len(customize) > 0: + try: + params = json.loads(customize) + if params and "temperature_step" in params: + temp_step = params.get("temperature_step") + if isinstance(temp_step, float | int): + self._temperature_step = float(temp_step) + else: + _LOGGER.error( + "[%s] Invalid type for temperature_step: %s", + self.device_id, + temp_step, + ) + except json.JSONDecodeError: + _LOGGER.exception( + "[%s] JSON decode error in set_customize", + self.device_id, + ) + self.update_all({"temperature_step": self._temperature_step}) + class MideaAppliance(MideaC3Device): """Midea C3 appliance.""" diff --git a/tests/devices/c3/__init__.py b/tests/devices/c3/__init__.py new file mode 100644 index 00000000..e0961475 --- /dev/null +++ b/tests/devices/c3/__init__.py @@ -0,0 +1 @@ +"""Midea local C3 device tests.""" diff --git a/tests/devices/c3/device_c3_test.py b/tests/devices/c3/device_c3_test.py new file mode 100644 index 00000000..5bf0dcc9 --- /dev/null +++ b/tests/devices/c3/device_c3_test.py @@ -0,0 +1,244 @@ +"""Test C3 Device.""" + +from unittest.mock import patch + +import pytest + +from midealocal.devices.c3 import C3DeviceMode, DeviceAttributes, MideaC3Device +from midealocal.devices.c3.message import ( + MessageQuery, +) + + +class TestMideaC3Device: + """Test Midea C3 Device.""" + + device: MideaC3Device + + @pytest.fixture(autouse=True) + def _setup_device(self) -> None: + """Midea C3 Device setup.""" + self.device = MideaC3Device( + name="Test Device", + device_id=1, + ip_address="192.168.1.1", + port=12345, + token="AA", + key="BB", + protocol=1, + model="test_model", + subtype=1, + customize='{"temperature_step": 1}', + ) + + def test_initial_attributes(self) -> None: + """Test initial attributes.""" + assert self.device.attributes[DeviceAttributes.zone1_power] is False + assert self.device.attributes[DeviceAttributes.zone2_power] is False + assert self.device.attributes[DeviceAttributes.dhw_power] is False + assert self.device.attributes[DeviceAttributes.zone1_curve] is False + assert self.device.attributes[DeviceAttributes.zone2_curve] is False + assert self.device.attributes[DeviceAttributes.disinfect] is False + assert self.device.attributes[DeviceAttributes.fast_dhw] is False + assert self.device.attributes[DeviceAttributes.zone_temp_type] == [False, False] + assert self.device.attributes[DeviceAttributes.zone1_room_temp_mode] is False + assert self.device.attributes[DeviceAttributes.zone2_room_temp_mode] is False + assert self.device.attributes[DeviceAttributes.zone1_water_temp_mode] is False + assert self.device.attributes[DeviceAttributes.zone2_water_temp_mode] is False + assert self.device.attributes[DeviceAttributes.silent_mode] is False + assert self.device.attributes[DeviceAttributes.eco_mode] is False + assert self.device.attributes[DeviceAttributes.tbh] is False + assert self.device.attributes[DeviceAttributes.mode] == 1 + assert self.device.attributes[DeviceAttributes.mode_auto] == 1 + assert self.device.attributes[DeviceAttributes.zone_target_temp] == [25, 25] + assert self.device.attributes[DeviceAttributes.dhw_target_temp] == 25 + assert self.device.attributes[DeviceAttributes.room_target_temp] == 30 + assert self.device.attributes[DeviceAttributes.zone_heating_temp_max] == [ + 55, + 55, + ] + assert self.device.attributes[DeviceAttributes.zone_heating_temp_min] == [ + 25, + 25, + ] + assert self.device.attributes[DeviceAttributes.zone_cooling_temp_max] == [ + 25, + 25, + ] + assert self.device.attributes[DeviceAttributes.zone_cooling_temp_min] == [5, 5] + assert self.device.attributes[DeviceAttributes.room_temp_max] == 60 + assert self.device.attributes[DeviceAttributes.room_temp_min] == 34 + assert self.device.attributes[DeviceAttributes.dhw_temp_max] == 60 + assert self.device.attributes[DeviceAttributes.dhw_temp_min] == 20 + assert self.device.attributes[DeviceAttributes.tank_actual_temperature] is None + assert self.device.attributes[DeviceAttributes.target_temperature] == [25, 25] + assert self.device.attributes[DeviceAttributes.temperature_max] == [0, 0] + assert self.device.attributes[DeviceAttributes.temperature_min] == [0, 0] + assert self.device.attributes[DeviceAttributes.total_energy_consumption] is None + assert self.device.attributes[DeviceAttributes.status_heating] is None + assert self.device.attributes[DeviceAttributes.status_dhw] is None + assert self.device.attributes[DeviceAttributes.status_tbh] is None + assert self.device.attributes[DeviceAttributes.status_ibh] is None + assert self.device.attributes[DeviceAttributes.total_produced_energy] is None + assert self.device.attributes[DeviceAttributes.outdoor_temperature] is None + assert self.device.attributes[DeviceAttributes.error_code] == 0 + assert self.device.temperature_step == 1 + + def test_set_attribute(self) -> None: + """Test set attribute.""" + with patch.object(self.device, "send_message_v2") as mock_build_send: + self.device.set_attribute(DeviceAttributes.zone1_power.value, True) + mock_build_send.assert_called() + + self.device.set_attribute(DeviceAttributes.zone1_power.value, False) + mock_build_send.assert_called() + + self.device.set_attribute(DeviceAttributes.eco_mode.value, True) + mock_build_send.assert_called() + + self.device.set_attribute(DeviceAttributes.silent_mode.value, True) + mock_build_send.assert_called() + + def test_build_query(self) -> None: + """Test build query.""" + queries = self.device.build_query() + assert len(queries) == 1 + assert isinstance(queries[0], MessageQuery) + + def test_process_message(self) -> None: + """Test process message.""" + with patch("midealocal.devices.c3.MessageC3Response") as mock_message_response: + mock_message = mock_message_response.return_value + mock_message.zone1_power = True + mock_message.zone2_power = False + mock_message.dhw_power = True + mock_message.zone1_curve = False + mock_message.zone2_curve = True + mock_message.disinfect = False + mock_message.fast_dhw = True + mock_message.zone_temp_type = [True, False] + mock_message.zone1_room_temp_mode = True + mock_message.zone2_room_temp_mode = False + mock_message.zone1_water_temp_mode = False + mock_message.zone2_water_temp_mode = True + mock_message.mode = 2 + mock_message.mode_auto = C3DeviceMode.COOL + mock_message.zone_target_temp = [25, 30] + mock_message.dhw_target_temp = 40 + mock_message.room_target_temp = 22 + mock_message.zone_heating_temp_max = [55, 55] + mock_message.zone_heating_temp_min = [25, 25] + mock_message.zone_cooling_temp_max = [25, 25] + mock_message.zone_cooling_temp_min = [5, 5] + mock_message.room_temp_max = 60 + mock_message.room_temp_min = 34 + mock_message.dhw_temp_max = 60 + mock_message.dhw_temp_min = 20 + mock_message.tank_actual_temperature = 50 + mock_message.target_temperature = [25, 25] + mock_message.temperature_max = [0, 0] + mock_message.temperature_min = [0, 0] + mock_message.total_energy_consumption = 100 + mock_message.status_heating = 1 + mock_message.status_dhw = 1 + mock_message.status_tbh = 0 + mock_message.status_ibh = 0 + mock_message.total_produced_energy = 200 + mock_message.outdoor_temperature = 18 + mock_message.error_code = 0 + + result = self.device.process_message(bytearray()) + + assert result[DeviceAttributes.zone1_power.value] is True + assert result[DeviceAttributes.zone2_power.value] is False + assert result[DeviceAttributes.dhw_power.value] is True + assert result[DeviceAttributes.zone1_curve.value] is False + assert result[DeviceAttributes.zone2_curve.value] is True + assert result[DeviceAttributes.disinfect.value] is False + assert result[DeviceAttributes.fast_dhw.value] is True + assert result[DeviceAttributes.zone_temp_type.value] == [True, False] + assert result[DeviceAttributes.zone1_room_temp_mode.value] is True + assert result[DeviceAttributes.zone2_room_temp_mode.value] is False + assert result[DeviceAttributes.zone1_water_temp_mode.value] is False + assert result[DeviceAttributes.zone2_water_temp_mode.value] is False + assert result[DeviceAttributes.mode.value] == 2 + assert result[DeviceAttributes.mode_auto.value] == 2 + assert result[DeviceAttributes.zone_target_temp.value] == [25, 30] + assert result[DeviceAttributes.dhw_target_temp.value] == 40 + assert result[DeviceAttributes.room_target_temp.value] == 22 + assert result[DeviceAttributes.zone_heating_temp_max.value] == [55, 55] + assert result[DeviceAttributes.zone_heating_temp_min.value] == [25, 25] + assert result[DeviceAttributes.zone_cooling_temp_max.value] == [25, 25] + assert result[DeviceAttributes.zone_cooling_temp_min.value] == [5, 5] + assert result[DeviceAttributes.room_temp_max.value] == 60 + assert result[DeviceAttributes.room_temp_min.value] == 34 + assert result[DeviceAttributes.dhw_temp_max.value] == 60 + assert result[DeviceAttributes.dhw_temp_min.value] == 20 + assert result[DeviceAttributes.tank_actual_temperature.value] == 50 + assert result[DeviceAttributes.total_energy_consumption.value] == 100 + assert result[DeviceAttributes.status_heating.value] == 1 + assert result[DeviceAttributes.status_dhw.value] == 1 + assert result[DeviceAttributes.status_tbh.value] == 0 + assert result[DeviceAttributes.status_ibh.value] == 0 + assert result[DeviceAttributes.total_produced_energy.value] == 200 + assert result[DeviceAttributes.outdoor_temperature.value] == 18 + assert result[DeviceAttributes.error_code.value] == 0 + + mock_message.zone2_power = True + mock_message.zone_temp_type = [False, True] + mock_message.mode = C3DeviceMode.HEAT + mock_message.mode_auto = C3DeviceMode.HEAT + + result = self.device.process_message(bytearray()) + + mock_message.zone1_power = False + mock_message.zone2_power = False + + result = self.device.process_message(bytearray()) + + assert result[DeviceAttributes.mode.value] == 3 + + def test_set_target_temperature(self) -> None: + """Test set target temperature.""" + with pytest.raises(ValueError): # noqa: PT011 + self.device.set_target_temperature(22.5, 1) + with patch("midealocal.devices.c3.MessageC3Response") as mock_message_response: + mock_message = mock_message_response.return_value + mock_message.zone_temp_type = [True, False] + self.device.process_message(bytearray()) + + with patch.object(self.device, "build_send") as mock_build_send: + self.device.set_target_temperature(22.5, 1, 0) + mock_build_send.assert_called_once() + message = mock_build_send.call_args[0][0] + assert message.mode == 1 + assert message.zone1_power + + with patch.object(self.device, "build_send") as mock_build_send: + self.device.set_target_temperature(23, 1, 1) + mock_build_send.assert_called_once() + message = mock_build_send.call_args[0][0] + assert message.room_target_temp == 23 + assert message.mode == 1 + assert message.zone2_power + + def test_set_mode(self) -> None: + """Test set mode.""" + with patch.object(self.device, "build_send") as mock_build_send: + self.device.set_mode(0, C3DeviceMode.COOL) + mock_build_send.assert_called_once() + message = mock_build_send.call_args[0][0] + assert message.zone1_power is True + assert message.mode == C3DeviceMode.COOL + + with patch.object(self.device, "build_send") as mock_build_send: + self.device.set_mode(1, C3DeviceMode.HEAT) + mock_build_send.assert_called_once() + message = mock_build_send.call_args[0][0] + assert message.zone2_power is True + assert message.mode == C3DeviceMode.HEAT + + def test_invalid_customize_format(self) -> None: + """Test invalid customize format.""" + self.device.set_customize("{") + self.device.set_customize('{"temperature_step":"10"}')