Add fan mode support to SmartThings fan entity (#106794)

* Fix the fan to support preset modes

* Add more tests and fix some comments

* Don't override inherited member

* Don't check for supported feature as the check is already performed before here

* Do not check for feature on properties

* Update homeassistant/components/smartthings/fan.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Fix tests

---------

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
This commit is contained in:
Grant 2024-01-14 02:25:26 +10:00 committed by GitHub
parent 058759c76a
commit 8395d84bbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 290 additions and 13 deletions

View File

@ -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

View File

@ -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"