diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index 6c814b781b2..647a99ebfa6 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -41,19 +41,50 @@ async def async_setup_entry( def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: """Return all capabilities supported if minimum required are present.""" - supported = [Capability.switch, Capability.fan_speed] - # Must have switch and fan_speed - if all(capability in capabilities for capability in supported): - return supported - return None + + # MUST support switch as we need a way to turn it on and off + if Capability.switch not in capabilities: + return None + + # These are all optional but at least one must be supported + optional = [ + Capability.air_conditioner_fan_mode, + Capability.fan_speed, + ] + + # If none of the optional capabilities are supported then error + if not any(capability in capabilities for capability in optional): + return None + + supported = [Capability.switch] + + for capability in optional: + if capability in capabilities: + supported.append(capability) + + return supported class SmartThingsFan(SmartThingsEntity, FanEntity): """Define a SmartThings Fan.""" - _attr_supported_features = FanEntityFeature.SET_SPEED _attr_speed_count = int_states_in_range(SPEED_RANGE) + def __init__(self, device): + """Init the class.""" + super().__init__(device) + self._attr_supported_features = self._determine_features() + + def _determine_features(self): + flags = FanEntityFeature(0) + + if self._device.get_capability(Capability.fan_speed): + flags |= FanEntityFeature.SET_SPEED + if self._device.get_capability(Capability.air_conditioner_fan_mode): + flags |= FanEntityFeature.PRESET_MODE + + return flags + async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" await self._async_set_percentage(percentage) @@ -70,6 +101,11 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): # the entity state ahead of receiving the confirming push updates self.async_write_ha_state() + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset_mode of the fan.""" + await self._device.set_fan_mode(preset_mode, set_status=True) + self.async_write_ha_state() + async def async_turn_on( self, percentage: int | None = None, @@ -77,7 +113,15 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): **kwargs: Any, ) -> None: """Turn the fan on.""" - await self._async_set_percentage(percentage) + if FanEntityFeature.SET_SPEED in self._attr_supported_features: + # If speed is set in features then turn the fan on with the speed. + await self._async_set_percentage(percentage) + else: + # If speed is not valid then turn on the fan with the + await self._device.switch_on(set_status=True) + # 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() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" @@ -92,6 +136,22 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): return self._device.status.switch @property - def percentage(self) -> int: + def percentage(self) -> int | None: """Return the current speed percentage.""" return ranged_value_to_percentage(SPEED_RANGE, self._device.status.fan_speed) + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode, e.g., auto, smart, interval, favorite. + + Requires FanEntityFeature.PRESET_MODE. + """ + return self._device.status.fan_mode + + @property + def preset_modes(self) -> list[str] | None: + """Return a list of available preset modes. + + Requires FanEntityFeature.PRESET_MODE. + """ + return self._device.status.supported_ac_fan_modes diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index ccf4b50fa1b..751646580d9 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -7,6 +7,8 @@ from pysmartthings import Attribute, Capability from homeassistant.components.fan import ( ATTR_PERCENTAGE, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, DOMAIN as FAN_DOMAIN, FanEntityFeature, ) @@ -77,7 +79,87 @@ async def test_entity_and_device_attributes( assert entry.sw_version == "v7.89" -async def test_turn_off(hass: HomeAssistant, device_factory) -> None: +# Setup platform tests with varying capabilities +async def test_setup_mode_capability(hass: HomeAssistant, device_factory) -> None: + """Test setting up a fan with only the mode capability.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.air_conditioner_fan_mode], + status={ + Attribute.switch: "off", + Attribute.fan_mode: "high", + Attribute.supported_ac_fan_modes: ["high", "low", "medium"], + }, + ) + + await setup_platform(hass, FAN_DOMAIN, devices=[device]) + + # Assert + state = hass.states.get("fan.fan_1") + assert state is not None + assert state.attributes[ATTR_SUPPORTED_FEATURES] == FanEntityFeature.PRESET_MODE + assert state.attributes[ATTR_PRESET_MODE] == "high" + assert state.attributes[ATTR_PRESET_MODES] == ["high", "low", "medium"] + + +async def test_setup_speed_capability(hass: HomeAssistant, device_factory) -> None: + """Test setting up a fan with only the speed capability.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.fan_speed], + status={ + Attribute.switch: "off", + Attribute.fan_speed: 2, + }, + ) + + await setup_platform(hass, FAN_DOMAIN, devices=[device]) + + # Assert + state = hass.states.get("fan.fan_1") + assert state is not None + assert state.attributes[ATTR_SUPPORTED_FEATURES] == FanEntityFeature.SET_SPEED + assert state.attributes[ATTR_PERCENTAGE] == 66 + + +async def test_setup_both_capabilities(hass: HomeAssistant, device_factory) -> None: + """Test setting up a fan with both the mode and speed capability.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[ + Capability.switch, + Capability.fan_speed, + Capability.air_conditioner_fan_mode, + ], + status={ + Attribute.switch: "off", + Attribute.fan_speed: 2, + Attribute.fan_mode: "high", + Attribute.supported_ac_fan_modes: ["high", "low", "medium"], + }, + ) + + await setup_platform(hass, FAN_DOMAIN, devices=[device]) + + # Assert + state = hass.states.get("fan.fan_1") + assert state is not None + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE + ) + assert state.attributes[ATTR_PERCENTAGE] == 66 + assert state.attributes[ATTR_PRESET_MODE] == "high" + assert state.attributes[ATTR_PRESET_MODES] == ["high", "low", "medium"] + + +# Speed Capability Tests + + +async def test_turn_off_speed_capability(hass: HomeAssistant, device_factory) -> None: """Test the fan turns of successfully.""" # Arrange device = device_factory( @@ -96,7 +178,7 @@ async def test_turn_off(hass: HomeAssistant, device_factory) -> None: assert state.state == "off" -async def test_turn_on(hass: HomeAssistant, device_factory) -> None: +async def test_turn_on_speed_capability(hass: HomeAssistant, device_factory) -> None: """Test the fan turns of successfully.""" # Arrange device = device_factory( @@ -115,7 +197,9 @@ async def test_turn_on(hass: HomeAssistant, device_factory) -> None: assert state.state == "on" -async def test_turn_on_with_speed(hass: HomeAssistant, device_factory) -> None: +async def test_turn_on_with_speed_speed_capability( + hass: HomeAssistant, device_factory +) -> None: """Test the fan turns on to the specified speed.""" # Arrange device = device_factory( @@ -138,7 +222,33 @@ async def test_turn_on_with_speed(hass: HomeAssistant, device_factory) -> None: assert state.attributes[ATTR_PERCENTAGE] == 100 -async def test_set_percentage(hass: HomeAssistant, device_factory) -> None: +async def test_turn_off_with_speed_speed_capability( + hass: HomeAssistant, device_factory +) -> None: + """Test the fan turns off with the speed.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.fan_speed], + status={Attribute.switch: "on", Attribute.fan_speed: 100}, + ) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) + # Act + await hass.services.async_call( + "fan", + "set_percentage", + {ATTR_ENTITY_ID: "fan.fan_1", ATTR_PERCENTAGE: 0}, + blocking=True, + ) + # Assert + state = hass.states.get("fan.fan_1") + assert state is not None + assert state.state == "off" + + +async def test_set_percentage_speed_capability( + hass: HomeAssistant, device_factory +) -> None: """Test setting to specific fan speed.""" # Arrange device = device_factory( @@ -161,7 +271,9 @@ async def test_set_percentage(hass: HomeAssistant, device_factory) -> None: assert state.attributes[ATTR_PERCENTAGE] == 100 -async def test_update_from_signal(hass: HomeAssistant, device_factory) -> None: +async def test_update_from_signal_speed_capability( + hass: HomeAssistant, device_factory +) -> None: """Test the fan updates when receiving a signal.""" # Arrange device = device_factory( @@ -194,3 +306,108 @@ async def test_unload_config_entry(hass: HomeAssistant, device_factory) -> None: await hass.config_entries.async_forward_entry_unload(config_entry, "fan") # Assert assert hass.states.get("fan.fan_1").state == STATE_UNAVAILABLE + + +# Preset Mode Tests + + +async def test_turn_off_mode_capability(hass: HomeAssistant, device_factory) -> None: + """Test the fan turns of successfully.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.air_conditioner_fan_mode], + status={ + Attribute.switch: "on", + Attribute.fan_mode: "high", + Attribute.supported_ac_fan_modes: ["high", "low", "medium"], + }, + ) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) + # Act + await hass.services.async_call( + "fan", "turn_off", {"entity_id": "fan.fan_1"}, blocking=True + ) + # Assert + state = hass.states.get("fan.fan_1") + assert state is not None + assert state.state == "off" + assert state.attributes[ATTR_PRESET_MODE] == "high" + + +async def test_turn_on_mode_capability(hass: HomeAssistant, device_factory) -> None: + """Test the fan turns of successfully.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.air_conditioner_fan_mode], + status={ + Attribute.switch: "off", + Attribute.fan_mode: "high", + Attribute.supported_ac_fan_modes: ["high", "low", "medium"], + }, + ) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) + # Act + await hass.services.async_call( + "fan", "turn_on", {ATTR_ENTITY_ID: "fan.fan_1"}, blocking=True + ) + # Assert + state = hass.states.get("fan.fan_1") + assert state is not None + assert state.state == "on" + assert state.attributes[ATTR_PRESET_MODE] == "high" + + +async def test_update_from_signal_mode_capability( + hass: HomeAssistant, device_factory +) -> None: + """Test the fan updates when receiving a signal.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.air_conditioner_fan_mode], + status={ + Attribute.switch: "off", + Attribute.fan_mode: "high", + Attribute.supported_ac_fan_modes: ["high", "low", "medium"], + }, + ) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) + await device.switch_on(True) + # Act + async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, [device.device_id]) + # Assert + await hass.async_block_till_done() + state = hass.states.get("fan.fan_1") + assert state is not None + assert state.state == "on" + + +async def test_set_preset_mode_mode_capability( + hass: HomeAssistant, device_factory +) -> None: + """Test setting to specific fan mode.""" + # Arrange + device = device_factory( + "Fan 1", + capabilities=[Capability.switch, Capability.air_conditioner_fan_mode], + status={ + Attribute.switch: "off", + Attribute.fan_mode: "high", + Attribute.supported_ac_fan_modes: ["high", "low", "medium"], + }, + ) + + await setup_platform(hass, FAN_DOMAIN, devices=[device]) + # Act + await hass.services.async_call( + "fan", + "set_preset_mode", + {ATTR_ENTITY_ID: "fan.fan_1", ATTR_PRESET_MODE: "low"}, + blocking=True, + ) + # Assert + state = hass.states.get("fan.fan_1") + assert state is not None + assert state.attributes[ATTR_PRESET_MODE] == "low"