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:
|
||||
"""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
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user