From e538f300cbff42445cabbe2bb9ad0e09b1b044ee Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Wed, 7 Aug 2024 14:51:40 -0600 Subject: [PATCH] Add "rate select" functionality (#157) * Add rate select (gear) capability parser and property * Add rate select parsing to property response * Add rate select to device properties * Add methods for supported rates --- msmart/device/AC/command.py | 14 ++++++++ msmart/device/AC/device.py | 62 ++++++++++++++++++++++++++++++++ msmart/device/AC/test_command.py | 16 +++++---- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/msmart/device/AC/command.py b/msmart/device/AC/command.py index 1fb088b..0feee35 100644 --- a/msmart/device/AC/command.py +++ b/msmart/device/AC/command.py @@ -500,6 +500,11 @@ def get_value(w) -> Callable[[int], bool]: return lambda v: v == w reader("turbo_heat", lambda v: v == 1 or v == 3), reader("turbo_cool", lambda v: v < 2), ], + CapabilityId.RATE_SELECT: [ + reader("rate_select_2_level", get_value(1)), # Gear + reader("rate_select_5_level", lambda v: v in [ + 2, 3]), # Genmode and Gear5 + ], CapabilityId.SELF_CLEAN: reader("self_clean", get_value(1)), CapabilityId.SILKY_COOL: reader("silky_cool", get_value(1)), CapabilityId.SMART_EYE: reader("smart_eye", get_value(1)), @@ -728,6 +733,15 @@ def target_humidity(self) -> bool: def self_clean(self) -> bool: return self._capabilities.get("self_clean", False) + @property + def rate_select_levels(self) -> Optional[int]: + if self._capabilities.get("rate_select_5_level", False): + return 5 + elif self._capabilities.get("rate_select_2_level", False): + return 2 + + return None + class StateResponse(Response): """Response to state query.""" diff --git a/msmart/device/AC/device.py b/msmart/device/AC/device.py index 3d3d12a..780fdbc 100644 --- a/msmart/device/AC/device.py +++ b/msmart/device/AC/device.py @@ -59,8 +59,25 @@ class SwingAngle(MideaIntEnum): DEFAULT = OFF + class RateSelect(MideaIntEnum): + OFF = 100 + + # 2 levels + GEAR_50 = 50 + GEAR_75 = 75 + + # 5 levels + LEVEL_1 = 1 + LEVEL_2 = 20 + LEVEL_3 = 40 + LEVEL_4 = 60 + LEVEL_5 = 80 + + DEFAULT = OFF + # Create a dict to map properties to attribute names _PROPERTY_MAP = { + PropertyId.RATE_SELECT: "_rate_select", PropertyId.SWING_LR_ANGLE: "_horizontal_swing_angle", PropertyId.SWING_UD_ANGLE: "_vertical_swing_angle" } @@ -124,6 +141,8 @@ def __init__(self, ip: str, device_id: int, port: int, **kwargs) -> None: self._horizontal_swing_angle = AirConditioner.SwingAngle.OFF self._vertical_swing_angle = AirConditioner.SwingAngle.OFF self._self_clean_active = False + self._rate_select = AirConditioner.RateSelect.OFF + self._supported_rate_selects = [AirConditioner.RateSelect.OFF] def _update_state(self, res: Response) -> None: """Update the local state from a device state response.""" @@ -183,6 +202,11 @@ def _update_state(self, res: Response) -> None: if (value := res.get_property(PropertyId.SELF_CLEAN)) is not None: self._self_clean_active = bool(value) + if (rate := res.get_property(PropertyId.RATE_SELECT)) is not None: + self._rate_select = cast( + AirConditioner.RateSelect, + AirConditioner.RateSelect.get_from_value(rate)) + elif isinstance(res, EnergyUsageResponse): self._total_energy_usage = res.total_energy self._current_energy_usage = res.current_energy @@ -272,6 +296,26 @@ def _update_capabilities(self, res: CapabilitiesResponse) -> None: if res.self_clean: self._supported_properties.add(PropertyId.SELF_CLEAN) + # Add supported rate select levels + if (rates := res.rate_select_levels) is not None: + self._supported_properties.add(PropertyId.RATE_SELECT) + + if rates > 2: + self._supported_rate_selects = [ + AirConditioner.RateSelect.OFF, + AirConditioner.RateSelect.LEVEL_5, + AirConditioner.RateSelect.LEVEL_4, + AirConditioner.RateSelect.LEVEL_3, + AirConditioner.RateSelect.LEVEL_2, + AirConditioner.RateSelect.LEVEL_1, + ] + else: + self._supported_rate_selects = [ + AirConditioner.RateSelect.OFF, + AirConditioner.RateSelect.GEAR_75, + AirConditioner.RateSelect.GEAR_50, + ] + async def _send_command_get_responses(self, command) -> List[Response]: """Send a command and return all valid responses.""" @@ -430,6 +474,10 @@ async def apply(self) -> None: if self._freeze_protection_mode and not self._supports_freeze_protection_mode: _LOGGER.warning("Device is not capable of freeze protection.") + if self._rate_select != AirConditioner.RateSelect.OFF and self._rate_select not in self._supported_rate_selects: + _LOGGER.warning( + "Device is not capable of rate select %r.", self._rate_select) + # Define function to return value or a default if value is None def or_default(v, d) -> Any: return v if v is not None else d @@ -716,6 +764,19 @@ def supports_self_clean(self) -> bool: def self_clean_active(self) -> bool: return self._self_clean_active + @property + def supported_rate_selects(self) -> List[RateSelect]: + return self._supported_rate_selects + + @property + def rate_select(self) -> RateSelect: + return self._rate_select + + @rate_select.setter + def rate_select(self, rate: RateSelect) -> None: + self._rate_select = rate + self._updated_properties.add(PropertyId.RATE_SELECT) + def to_dict(self) -> dict: return {**super().to_dict(), **{ "power": self.power_state, @@ -743,4 +804,5 @@ def to_dict(self) -> dict: "total_energy_usage": self.total_energy_usage, "current_energy_usage": self.current_energy_usage, "real_time_power_usage": self.real_time_power_usage, + "rate_select": self.rate_select, }} diff --git a/msmart/device/AC/test_command.py b/msmart/device/AC/test_command.py index 9261a39..b13ff39 100644 --- a/msmart/device/AC/test_command.py +++ b/msmart/device/AC/test_command.py @@ -236,7 +236,7 @@ class TestCapabilitiesResponse(_TestResponseBase): "dry_mode", "cool_mode", "heat_mode", "auto_mode", "eco_mode", "turbo_mode", "freeze_protection_mode", "min_temperature", "max_temperature", - "display_control", "filter_reminder"] + "display_control", "filter_reminder", "rate_select_levels"] def test_properties(self) -> None: """Test that the capabilities response has the expected properties.""" @@ -330,7 +330,8 @@ def test_capabilities(self) -> None: "fan_custom": False, "fan_silent": False, "fan_low": True, "fan_medium": True, "fan_high": True, "fan_auto": True, "min_temperature": 16, "max_temperature": 30, - "display_control": False, "filter_reminder": False + "display_control": False, "filter_reminder": False, + "rate_select_levels": None } # Check capabilities properties match for prop in self.EXPECTED_PROPERTIES: @@ -379,7 +380,8 @@ def test_capabilities_2(self) -> None: "fan_custom": True, "fan_silent": True, "fan_low": True, "fan_medium": True, "fan_high": True, "fan_auto": True, "min_temperature": 16, "max_temperature": 30, - "display_control": False, "filter_reminder": False + "display_control": False, "filter_reminder": False, + "rate_select_levels": None } # Check capabilities properties match for prop in self.EXPECTED_PROPERTIES: @@ -417,7 +419,8 @@ def test_capabilities_3(self) -> None: "fan_custom": False, "fan_silent": False, "fan_low": True, "fan_medium": True, "fan_high": True, "fan_auto": True, "min_temperature": 16, "max_temperature": 30, - "display_control": True, "filter_reminder": True + "display_control": True, "filter_reminder": True, + "rate_select_levels": None } # Check capabilities properties match for prop in self.EXPECTED_PROPERTIES: @@ -457,7 +460,8 @@ def test_capabilities_4(self) -> None: "fan_custom": True, "fan_silent": True, "fan_low": True, "fan_medium": True, "fan_high": True, "fan_auto": True, "min_temperature": 16, "max_temperature": 30, - "display_control": True, "filter_reminder": True + "display_control": True, "filter_reminder": True, + "rate_select_levels": None } # Check capabilities properties match for prop in self.EXPECTED_PROPERTIES: @@ -557,7 +561,7 @@ def test_additional_capabilities(self) -> None: "fan_medium": True, "fan_high": True, "fan_auto": True, "min_temperature": 16, "max_temperature": 30, "display_control": False, "filter_reminder": False, - "anion": True + "anion": True, "rate_select_levels": None } # Check capabilities properties match for prop in self.EXPECTED_PROPERTIES: