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:
committed by
GitHub
parent
8778d4c704
commit
a1a1d65ee4
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user