From 41626ed500901e4f1d072b02e9c7ddfb20ac6051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C4=8Cerm=C3=A1k?= Date: Wed, 22 Nov 2023 20:00:28 +0100 Subject: [PATCH] Support for more features on smartthings AC (#99424) * ability to set swing mode on samsung AC * support for windFree mode on samsung AC * Apply suggestions from code review Co-authored-by: G Johansson * suggestion from code reviews * Apply suggestions from code review --------- Co-authored-by: G Johansson --- .../components/smartthings/climate.py | 109 ++++++++++++++++-- tests/components/smartthings/test_climate.py | 55 ++++++++- 2 files changed, 151 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 52a02aca745..16558d2c795 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -13,6 +13,10 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE_DOMAIN, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_VERTICAL, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -71,6 +75,20 @@ STATE_TO_AC_MODE = { HVACMode.FAN_ONLY: "fanOnly", } +SWING_TO_FAN_OSCILLATION = { + SWING_BOTH: "all", + SWING_HORIZONTAL: "horizontal", + SWING_VERTICAL: "vertical", + SWING_OFF: "fixed", +} + +FAN_OSCILLATION_TO_SWING = { + value: key for key, value in SWING_TO_FAN_OSCILLATION.items() +} + + +WINDFREE = "windFree" + UNIT_MAP = {"C": UnitOfTemperature.CELSIUS, "F": UnitOfTemperature.FAHRENHEIT} _LOGGER = logging.getLogger(__name__) @@ -322,18 +340,34 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity): class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): """Define a SmartThings Air Conditioner.""" - _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE - ) + _hvac_modes: list[HVACMode] - def __init__(self, device): + def __init__(self, device) -> None: """Init the class.""" super().__init__(device) - self._hvac_modes = None + self._hvac_modes = [] + self._attr_preset_mode = None + self._attr_preset_modes = self._determine_preset_modes() + self._attr_swing_modes = self._determine_swing_modes() + self._attr_supported_features = self._determine_supported_features() + + def _determine_supported_features(self) -> ClimateEntityFeature: + features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ) + if self._device.get_capability(Capability.fan_oscillation_mode): + features |= ClimateEntityFeature.SWING_MODE + if (self._attr_preset_modes is not None) and len(self._attr_preset_modes) > 0: + features |= ClimateEntityFeature.PRESET_MODE + return features async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" await self._device.set_fan_mode(fan_mode, set_status=True) + + # setting the fan must reset the preset mode (it deactivates the windFree function) + self._attr_preset_mode = None + # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates self.async_write_ha_state() @@ -407,12 +441,12 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): self._hvac_modes = list(modes) @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._device.status.temperature @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes. Include attributes from the Demand Response Load Control (drlc) @@ -432,12 +466,12 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): return state_attributes @property - def fan_mode(self): + def fan_mode(self) -> str: """Return the fan setting.""" return self._device.status.fan_mode @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" return self._device.status.supported_ac_fan_modes @@ -454,11 +488,62 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): return self._hvac_modes @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the temperature we try to reach.""" return self._device.status.cooling_setpoint @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" - return UNIT_MAP.get(self._device.status.attributes[Attribute.temperature].unit) + return UNIT_MAP[self._device.status.attributes[Attribute.temperature].unit] + + def _determine_swing_modes(self) -> list[str]: + """Return the list of available swing modes.""" + supported_modes = self._device.status.attributes[ + Attribute.supported_fan_oscillation_modes + ][0] + supported_swings = [ + FAN_OSCILLATION_TO_SWING.get(m, SWING_OFF) for m in supported_modes + ] + return supported_swings + + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set swing mode.""" + fan_oscillation_mode = SWING_TO_FAN_OSCILLATION[swing_mode] + await self._device.set_fan_oscillation_mode(fan_oscillation_mode) + + # setting the fan must reset the preset mode (it deactivates the windFree function) + self._attr_preset_mode = None + + self.async_schedule_update_ha_state(True) + + @property + def swing_mode(self) -> str: + """Return the swing setting.""" + return FAN_OSCILLATION_TO_SWING.get( + self._device.status.fan_oscillation_mode, SWING_OFF + ) + + def _determine_preset_modes(self) -> list[str] | None: + """Return a list of available preset modes.""" + supported_modes = self._device.status.attributes[ + "supportedAcOptionalMode" + ].value + if WINDFREE in supported_modes: + return [WINDFREE] + return None + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set special modes (currently only windFree is supported).""" + result = await self._device.command( + "main", + "custom.airConditionerOptionalMode", + "setAcOptionalMode", + [preset_mode], + ) + if result: + self._device.status.update_attribute_value("acOptionalMode", preset_mode) + + self._attr_preset_mode = preset_mode + + self.async_write_ha_state() diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index ce875190efb..e74d69f04c9 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -15,16 +15,20 @@ from homeassistant.components.climate import ( ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, + ATTR_PRESET_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, ClimateEntityFeature, HVACAction, HVACMode, ) +from homeassistant.components.climate.const import ATTR_SWING_MODE from homeassistant.components.smartthings import climate from homeassistant.components.smartthings.const import DOMAIN from homeassistant.const import ( @@ -155,6 +159,7 @@ def air_conditioner_fixture(device_factory): Capability.switch, Capability.temperature_measurement, Capability.thermostat_cooling_setpoint, + Capability.fan_oscillation_mode, ], status={ Attribute.air_conditioner_mode: "auto", @@ -182,6 +187,14 @@ def air_conditioner_fixture(device_factory): ], Attribute.switch: "on", Attribute.cooling_setpoint: 23, + "supportedAcOptionalMode": ["windFree"], + Attribute.supported_fan_oscillation_modes: [ + "all", + "horizontal", + "vertical", + "fixed", + ], + Attribute.fan_oscillation_mode: "vertical", }, ) device.status.attributes[Attribute.temperature] = Status(24, "C", None) @@ -303,7 +316,10 @@ async def test_air_conditioner_entity_state( assert state.state == HVACMode.HEAT_COOL assert ( state.attributes[ATTR_SUPPORTED_FEATURES] - == ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + == ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.SWING_MODE ) assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVACMode.COOL, @@ -591,3 +607,40 @@ async def test_entity_and_device_attributes(hass: HomeAssistant, thermostat) -> assert entry.manufacturer == "Generic manufacturer" assert entry.hw_version == "v4.56" assert entry.sw_version == "v7.89" + + +async def test_set_windfree_off(hass: HomeAssistant, air_conditioner) -> None: + """Test if the windfree preset can be turned on and is turned off when fan mode is set.""" + entity_ids = ["climate.air_conditioner"] + air_conditioner.status.update_attribute_value(Attribute.switch, "on") + await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_ids, ATTR_PRESET_MODE: "windFree"}, + blocking=True, + ) + state = hass.states.get("climate.air_conditioner") + assert state.attributes[ATTR_PRESET_MODE] == "windFree" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_ids, ATTR_FAN_MODE: "low"}, + blocking=True, + ) + state = hass.states.get("climate.air_conditioner") + assert not state.attributes[ATTR_PRESET_MODE] + + +async def test_set_swing_mode(hass: HomeAssistant, air_conditioner) -> None: + """Test the fan swing is set successfully.""" + await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) + entity_ids = ["climate.air_conditioner"] + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: entity_ids, ATTR_SWING_MODE: "vertical"}, + blocking=True, + ) + state = hass.states.get("climate.air_conditioner") + assert state.attributes[ATTR_SWING_MODE] == "vertical"