mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
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:
parent
058759c76a
commit
8395d84bbb
@ -41,19 +41,50 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
|
def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
|
||||||
"""Return all capabilities supported if minimum required are present."""
|
"""Return all capabilities supported if minimum required are present."""
|
||||||
supported = [Capability.switch, Capability.fan_speed]
|
|
||||||
# Must have switch and fan_speed
|
# MUST support switch as we need a way to turn it on and off
|
||||||
if all(capability in capabilities for capability in supported):
|
if Capability.switch not in capabilities:
|
||||||
return supported
|
return None
|
||||||
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):
|
class SmartThingsFan(SmartThingsEntity, FanEntity):
|
||||||
"""Define a SmartThings Fan."""
|
"""Define a SmartThings Fan."""
|
||||||
|
|
||||||
_attr_supported_features = FanEntityFeature.SET_SPEED
|
|
||||||
_attr_speed_count = int_states_in_range(SPEED_RANGE)
|
_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:
|
async def async_set_percentage(self, percentage: int) -> None:
|
||||||
"""Set the speed percentage of the fan."""
|
"""Set the speed percentage of the fan."""
|
||||||
await self._async_set_percentage(percentage)
|
await self._async_set_percentage(percentage)
|
||||||
@ -70,6 +101,11 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
|
|||||||
# the entity state ahead of receiving the confirming push updates
|
# the entity state ahead of receiving the confirming push updates
|
||||||
self.async_write_ha_state()
|
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(
|
async def async_turn_on(
|
||||||
self,
|
self,
|
||||||
percentage: int | None = None,
|
percentage: int | None = None,
|
||||||
@ -77,7 +113,15 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
|
|||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Turn the fan on."""
|
"""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:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the fan off."""
|
"""Turn the fan off."""
|
||||||
@ -92,6 +136,22 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
|
|||||||
return self._device.status.switch
|
return self._device.status.switch
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def percentage(self) -> int:
|
def percentage(self) -> int | None:
|
||||||
"""Return the current speed percentage."""
|
"""Return the current speed percentage."""
|
||||||
return ranged_value_to_percentage(SPEED_RANGE, self._device.status.fan_speed)
|
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
|
||||||
|
@ -7,6 +7,8 @@ from pysmartthings import Attribute, Capability
|
|||||||
|
|
||||||
from homeassistant.components.fan import (
|
from homeassistant.components.fan import (
|
||||||
ATTR_PERCENTAGE,
|
ATTR_PERCENTAGE,
|
||||||
|
ATTR_PRESET_MODE,
|
||||||
|
ATTR_PRESET_MODES,
|
||||||
DOMAIN as FAN_DOMAIN,
|
DOMAIN as FAN_DOMAIN,
|
||||||
FanEntityFeature,
|
FanEntityFeature,
|
||||||
)
|
)
|
||||||
@ -77,7 +79,87 @@ async def test_entity_and_device_attributes(
|
|||||||
assert entry.sw_version == "v7.89"
|
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."""
|
"""Test the fan turns of successfully."""
|
||||||
# Arrange
|
# Arrange
|
||||||
device = device_factory(
|
device = device_factory(
|
||||||
@ -96,7 +178,7 @@ async def test_turn_off(hass: HomeAssistant, device_factory) -> None:
|
|||||||
assert state.state == "off"
|
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."""
|
"""Test the fan turns of successfully."""
|
||||||
# Arrange
|
# Arrange
|
||||||
device = device_factory(
|
device = device_factory(
|
||||||
@ -115,7 +197,9 @@ async def test_turn_on(hass: HomeAssistant, device_factory) -> None:
|
|||||||
assert state.state == "on"
|
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."""
|
"""Test the fan turns on to the specified speed."""
|
||||||
# Arrange
|
# Arrange
|
||||||
device = device_factory(
|
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
|
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."""
|
"""Test setting to specific fan speed."""
|
||||||
# Arrange
|
# Arrange
|
||||||
device = device_factory(
|
device = device_factory(
|
||||||
@ -161,7 +271,9 @@ async def test_set_percentage(hass: HomeAssistant, device_factory) -> None:
|
|||||||
assert state.attributes[ATTR_PERCENTAGE] == 100
|
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."""
|
"""Test the fan updates when receiving a signal."""
|
||||||
# Arrange
|
# Arrange
|
||||||
device = device_factory(
|
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")
|
await hass.config_entries.async_forward_entry_unload(config_entry, "fan")
|
||||||
# Assert
|
# Assert
|
||||||
assert hass.states.get("fan.fan_1").state == STATE_UNAVAILABLE
|
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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user