1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Add Hood fan speed select entity to SmartThings (#157841)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Joost Lekkerkerker
2025-12-29 19:56:55 +01:00
committed by GitHub
parent 8778d4c704
commit a1a1d65ee4
7 changed files with 309 additions and 12 deletions
+119 -1
View File
@@ -11,6 +11,8 @@ from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.percentage import (
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
percentage_to_ranged_value,
ranged_value_to_percentage,
)
@@ -22,6 +24,9 @@ from .entity import SmartThingsEntity
SPEED_RANGE = (1, 3) # off is not included
SMART = 14
PRESET_SMART = "smart"
async def async_setup_entry(
hass: HomeAssistant,
@@ -30,7 +35,7 @@ async def async_setup_entry(
) -> None:
"""Add fans for a config entry."""
entry_data = entry.runtime_data
async_add_entities(
entities: list[FanEntity] = [
SmartThingsFan(entry_data.client, device)
for device in entry_data.devices.values()
if Capability.SWITCH in device.status[MAIN]
@@ -42,7 +47,20 @@ async def async_setup_entry(
)
)
and Capability.THERMOSTAT_COOLING_SETPOINT not in device.status[MAIN]
]
entities.extend(
SmartThingsHood(entry_data.client, device)
for device in entry_data.devices.values()
if Capability.SWITCH in device.status[MAIN]
and Capability.SAMSUNG_CE_HOOD_FAN_SPEED in device.status[MAIN]
and (
device.status[MAIN][Capability.SAMSUNG_CE_HOOD_FAN_SPEED][
Attribute.SETTABLE_MIN_FAN_SPEED
].value
== SMART
)
)
async_add_entities(entities)
class SmartThingsFan(SmartThingsEntity, FanEntity):
@@ -149,3 +167,103 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
return self.get_attribute_value(
Capability.AIR_CONDITIONER_FAN_MODE, Attribute.SUPPORTED_AC_FAN_MODES
)
class SmartThingsHood(SmartThingsEntity, FanEntity):
"""Define a SmartThings Hood."""
_attr_name = None
_attr_supported_features = (
FanEntityFeature.TURN_ON
| FanEntityFeature.TURN_OFF
| FanEntityFeature.PRESET_MODE
| FanEntityFeature.SET_SPEED
)
_attr_preset_modes = [PRESET_SMART]
_attr_translation_key = "hood"
def __init__(self, client: SmartThings, device: FullDevice) -> None:
"""Init the class."""
super().__init__(
client,
device,
{
Capability.SWITCH,
Capability.SAMSUNG_CE_HOOD_FAN_SPEED,
},
)
@property
def fan_speeds(self) -> list[int]:
"""Return a list of available fan speeds."""
return [
speed
for speed in self.get_attribute_value(
Capability.SAMSUNG_CE_HOOD_FAN_SPEED, Attribute.SUPPORTED_HOOD_FAN_SPEED
)
if speed != SMART
]
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset_mode of the fan."""
await self.execute_device_command(
Capability.SAMSUNG_CE_HOOD_FAN_SPEED,
Command.SET_HOOD_FAN_SPEED,
argument=SMART,
)
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed percentage of the fan."""
if percentage == 0:
await self.execute_device_command(Capability.SWITCH, Command.OFF)
else:
await self.execute_device_command(
Capability.SAMSUNG_CE_HOOD_FAN_SPEED,
Command.SET_HOOD_FAN_SPEED,
argument=percentage_to_ordered_list_item(self.fan_speeds, percentage),
)
async def async_turn_on(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn the fan on."""
await self.execute_device_command(Capability.SWITCH, Command.ON)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the fan off."""
await self.execute_device_command(Capability.SWITCH, Command.OFF)
@property
def is_on(self) -> bool:
"""Return true if fan is on."""
return self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on"
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
if (
self.get_attribute_value(
Capability.SAMSUNG_CE_HOOD_FAN_SPEED, Attribute.HOOD_FAN_SPEED
)
== SMART
):
return PRESET_SMART
return None
@property
def percentage(self) -> int | None:
"""Return the current speed percentage."""
fan_speed = self.get_attribute_value(
Capability.SAMSUNG_CE_HOOD_FAN_SPEED, Attribute.HOOD_FAN_SPEED
)
if fan_speed == SMART:
return None
return ordered_list_item_to_percentage(self.fan_speeds, fan_speed)
@property
def speed_count(self) -> int:
"""Return the number of available speeds."""
return len(self.fan_speeds)
@@ -53,6 +53,17 @@
}
}
},
"fan": {
"hood": {
"state_attributes": {
"preset_mode": {
"state": {
"smart": "mdi:brain"
}
}
}
}
},
"number": {
"freezer_temperature": {
"default": "mdi:snowflake-thermometer"
@@ -138,6 +138,17 @@
}
}
},
"fan": {
"hood": {
"state_attributes": {
"preset_mode": {
"state": {
"smart": "Smart"
}
}
}
}
},
"number": {
"cool_select_plus_temperature": {
"name": "CoolSelect+ temperature"
@@ -48,7 +48,7 @@
},
"switch": {
"switch": {
"value": "off",
"value": "on",
"timestamp": "2025-11-11T23:41:24.907Z"
}
},
@@ -305,7 +305,7 @@
"timestamp": "2025-11-11T23:41:21.525Z"
},
"hoodFanSpeed": {
"value": 14,
"value": 15,
"timestamp": "2025-11-11T23:41:21.525Z"
},
"supportedHoodFanSpeed": {
@@ -1,4 +1,63 @@
# serializer version: 1
# name: test_all_entities[da_ks_hood_01001][fan.range_hood-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'preset_modes': list([
'smart',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'fan',
'entity_category': None,
'entity_id': 'fan.range_hood',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <FanEntityFeature: 57>,
'translation_key': 'hood',
'unique_id': 'fa5fca25-fa7a-1807-030a-2f72ee0f7bff_main',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_hood_01001][fan.range_hood-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Range hood',
'percentage': 25,
'percentage_step': 25.0,
'preset_mode': None,
'preset_modes': list([
'smart',
]),
'supported_features': <FanEntityFeature: 57>,
}),
'context': <ANY>,
'entity_id': 'fan.range_hood',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[fake_fan][fan.fake_fan-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -236,7 +236,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
'state': 'on',
})
# ---
# name: test_all_entities[da_ks_walloven_0107x][switch.four_sabbath_mode-entry]
+106 -8
View File
@@ -2,7 +2,7 @@
from unittest.mock import AsyncMock
from pysmartthings import Capability, Command
from pysmartthings import Attribute, Capability, Command
from pysmartthings.models import HealthStatus
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -26,7 +26,12 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration, snapshot_smartthings_entities, trigger_health_update
from . import (
setup_integration,
snapshot_smartthings_entities,
trigger_health_update,
trigger_update,
)
from tests.common import MockConfigEntry
@@ -44,7 +49,13 @@ async def test_all_entities(
snapshot_smartthings_entities(hass, entity_registry, snapshot, Platform.FAN)
@pytest.mark.parametrize("device_fixture", ["fake_fan"])
@pytest.mark.parametrize(
("device_fixture", "entity_id", "device_id"),
[
("fake_fan", "fan.fake_fan", "f1af21a2-d5a1-437c-b10a-b34a87394b71"),
("da_ks_hood_01001", "fan.range_hood", "fa5fca25-fa7a-1807-030a-2f72ee0f7bff"),
],
)
@pytest.mark.parametrize(
("action", "command"),
[
@@ -58,6 +69,8 @@ async def test_turn_on_off(
mock_config_entry: MockConfigEntry,
action: str,
command: Command,
entity_id: str,
device_id: str,
) -> None:
"""Test turning on and off."""
await setup_integration(hass, mock_config_entry)
@@ -65,11 +78,11 @@ async def test_turn_on_off(
await hass.services.async_call(
FAN_DOMAIN,
action,
{ATTR_ENTITY_ID: "fan.fake_fan"},
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"f1af21a2-d5a1-437c-b10a-b34a87394b71",
device_id,
Capability.SWITCH,
command,
MAIN,
@@ -100,11 +113,19 @@ async def test_set_percentage(
)
@pytest.mark.parametrize("device_fixture", ["fake_fan"])
@pytest.mark.parametrize(
("device_fixture", "entity_id", "device_id"),
[
("fake_fan", "fan.fake_fan", "f1af21a2-d5a1-437c-b10a-b34a87394b71"),
("da_ks_hood_01001", "fan.range_hood", "fa5fca25-fa7a-1807-030a-2f72ee0f7bff"),
],
)
async def test_set_percentage_off(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_id: str,
device_id: str,
) -> None:
"""Test setting the speed percentage of the fan."""
await setup_integration(hass, mock_config_entry)
@@ -112,11 +133,11 @@ async def test_set_percentage_off(
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_SET_PERCENTAGE,
{ATTR_ENTITY_ID: "fan.fake_fan", ATTR_PERCENTAGE: 0},
{ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 0},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"f1af21a2-d5a1-437c-b10a-b34a87394b71",
device_id,
Capability.SWITCH,
Command.OFF,
MAIN,
@@ -204,3 +225,80 @@ async def test_availability_at_start(
"""Test unavailable at boot."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("fan.fake_fan").state == STATE_UNAVAILABLE
@pytest.mark.parametrize("device_fixture", ["da_ks_hood_01001"])
async def test_set_hood_percentage(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test setting the speed percentage of the hood."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_SET_PERCENTAGE,
{ATTR_ENTITY_ID: "fan.range_hood", ATTR_PERCENTAGE: 50},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"fa5fca25-fa7a-1807-030a-2f72ee0f7bff",
Capability.SAMSUNG_CE_HOOD_FAN_SPEED,
Command.SET_HOOD_FAN_SPEED,
MAIN,
argument=16,
)
@pytest.mark.parametrize("device_fixture", ["da_ks_hood_01001"])
async def test_set_hood_preset_mode(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test setting the preset mode of the hood."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: "fan.range_hood", ATTR_PRESET_MODE: "smart"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"fa5fca25-fa7a-1807-030a-2f72ee0f7bff",
Capability.SAMSUNG_CE_HOOD_FAN_SPEED,
Command.SET_HOOD_FAN_SPEED,
MAIN,
argument=14,
)
@pytest.mark.parametrize("device_fixture", ["da_ks_hood_01001"])
async def test_updating_hood_preset_mode(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test updating the preset mode of the hood."""
await setup_integration(hass, mock_config_entry)
state = hass.states.get("fan.range_hood")
assert state
assert state.attributes[ATTR_PRESET_MODE] is None
assert state.attributes[ATTR_PERCENTAGE] == 25
await trigger_update(
hass,
devices,
"fa5fca25-fa7a-1807-030a-2f72ee0f7bff",
Capability.SAMSUNG_CE_HOOD_FAN_SPEED,
Attribute.HOOD_FAN_SPEED,
14,
)
state = hass.states.get("fan.range_hood")
assert state
assert state.attributes[ATTR_PRESET_MODE] == "smart"
assert state.attributes[ATTR_PERCENTAGE] is None