Add select platform to SmartThings (#141115)

* Add select platform to SmartThings

* Add select platform to SmartThings
This commit is contained in:
Joost Lekkerkerker 2025-03-22 18:03:50 +01:00 committed by GitHub
parent 765691c84d
commit 1b8b348eff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 493 additions and 0 deletions

View File

@ -86,6 +86,7 @@ PLATFORMS = [
Platform.LIGHT,
Platform.LOCK,
Platform.SCENE,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,

View File

@ -13,6 +13,15 @@
"on": "mdi:lock"
}
}
},
"select": {
"operating_state": {
"state": {
"run": "mdi:play",
"pause": "mdi:pause",
"stop": "mdi:stop"
}
}
}
}
}

View File

@ -0,0 +1,120 @@
"""Support for select entities through the SmartThings cloud API."""
from __future__ import annotations
from dataclasses import dataclass
from pysmartthings import Attribute, Capability, Command, SmartThings
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FullDevice, SmartThingsConfigEntry
from .const import MAIN
from .entity import SmartThingsEntity
@dataclass(frozen=True, kw_only=True)
class SmartThingsSelectDescription(SelectEntityDescription):
"""Class describing SmartThings select entities."""
key: Capability
requires_remote_control_status: bool
options_attribute: Attribute
status_attribute: Attribute
command: Command
CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = {
Capability.DRYER_OPERATING_STATE: SmartThingsSelectDescription(
key=Capability.DRYER_OPERATING_STATE,
name=None,
translation_key="operating_state",
requires_remote_control_status=True,
options_attribute=Attribute.SUPPORTED_MACHINE_STATES,
status_attribute=Attribute.MACHINE_STATE,
command=Command.SET_MACHINE_STATE,
),
Capability.WASHER_OPERATING_STATE: SmartThingsSelectDescription(
key=Capability.WASHER_OPERATING_STATE,
name=None,
translation_key="operating_state",
requires_remote_control_status=True,
options_attribute=Attribute.SUPPORTED_MACHINE_STATES,
status_attribute=Attribute.MACHINE_STATE,
command=Command.SET_MACHINE_STATE,
),
}
async def async_setup_entry(
hass: HomeAssistant,
entry: SmartThingsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add select entities for a config entry."""
entry_data = entry.runtime_data
async_add_entities(
SmartThingsSelectEntity(
entry_data.client, device, CAPABILITIES_TO_SELECT[capability]
)
for device in entry_data.devices.values()
for capability in device.status[MAIN]
if capability in CAPABILITIES_TO_SELECT
)
class SmartThingsSelectEntity(SmartThingsEntity, SelectEntity):
"""Define a SmartThings select."""
entity_description: SmartThingsSelectDescription
def __init__(
self,
client: SmartThings,
device: FullDevice,
entity_description: SmartThingsSelectDescription,
) -> None:
"""Initialize the instance."""
capabilities = {entity_description.key}
if entity_description.requires_remote_control_status:
capabilities.add(Capability.REMOTE_CONTROL_STATUS)
super().__init__(client, device, capabilities)
self.entity_description = entity_description
self._attr_unique_id = (
f"{device.device.device_id}_{MAIN}_{entity_description.key}"
)
@property
def options(self) -> list[str]:
"""Return the list of options."""
return self.get_attribute_value(
self.entity_description.key, self.entity_description.options_attribute
)
@property
def current_option(self) -> str | None:
"""Return the current option."""
return self.get_attribute_value(
self.entity_description.key, self.entity_description.status_attribute
)
async def async_select_option(self, option: str) -> None:
"""Select an option."""
if (
self.entity_description.requires_remote_control_status
and self.get_attribute_value(
Capability.REMOTE_CONTROL_STATUS, Attribute.REMOTE_CONTROL_ENABLED
)
== "false"
):
raise ServiceValidationError(
"Can only be updated when remote control is enabled"
)
await self.execute_device_command(
self.entity_description.key,
self.entity_description.command,
option,
)

View File

@ -78,6 +78,15 @@
}
}
},
"select": {
"operating_state": {
"state": {
"run": "[%key:component::smartthings::entity::sensor::dishwasher_machine_state::state::run%]",
"pause": "[%key:common::state::paused%]",
"stop": "[%key:component::smartthings::entity::sensor::dishwasher_machine_state::state::stop%]"
}
}
},
"sensor": {
"lighting_mode": {
"name": "Activity lighting mode"

View File

@ -0,0 +1,233 @@
# serializer version: 1
# name: test_all_entities[da_wm_wd_000001][select.dryer-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'stop',
'run',
'pause',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.dryer',
'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,
'supported_features': 0,
'translation_key': 'operating_state',
'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_dryerOperatingState',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_wd_000001][select.dryer-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Dryer',
'options': list([
'stop',
'run',
'pause',
]),
}),
'context': <ANY>,
'entity_id': 'select.dryer',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'stop',
})
# ---
# name: test_all_entities[da_wm_wd_000001_1][select.seca_roupa-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'stop',
'run',
'pause',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.seca_roupa',
'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,
'supported_features': 0,
'translation_key': 'operating_state',
'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_dryerOperatingState',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_wd_000001_1][select.seca_roupa-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Seca-Roupa',
'options': list([
'stop',
'run',
'pause',
]),
}),
'context': <ANY>,
'entity_id': 'select.seca_roupa',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'stop',
})
# ---
# name: test_all_entities[da_wm_wm_000001][select.washer-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'stop',
'run',
'pause',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.washer',
'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,
'supported_features': 0,
'translation_key': 'operating_state',
'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_washerOperatingState',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_wm_000001][select.washer-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Washer',
'options': list([
'stop',
'run',
'pause',
]),
}),
'context': <ANY>,
'entity_id': 'select.washer',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'stop',
})
# ---
# name: test_all_entities[da_wm_wm_000001_1][select.washing_machine-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'stop',
'run',
'pause',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.washing_machine',
'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,
'supported_features': 0,
'translation_key': 'operating_state',
'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_washerOperatingState',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_wm_wm_000001_1][select.washing_machine-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Washing Machine',
'options': list([
'stop',
'run',
'pause',
]),
}),
'context': <ANY>,
'entity_id': 'select.washing_machine',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'run',
})
# ---

View File

@ -0,0 +1,121 @@
"""Test for the SmartThings select platform."""
from unittest.mock import AsyncMock
from pysmartthings import Attribute, Capability, Command
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.select import (
ATTR_OPTION,
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
)
from homeassistant.components.smartthings import MAIN
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er
from . import (
set_attribute_value,
setup_integration,
snapshot_smartthings_entities,
trigger_update,
)
from tests.common import MockConfigEntry
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
await setup_integration(hass, mock_config_entry)
snapshot_smartthings_entities(hass, entity_registry, snapshot, Platform.SELECT)
@pytest.mark.parametrize("device_fixture", ["da_wm_wd_000001"])
async def test_state_update(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test state update."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("select.dryer").state == "stop"
await trigger_update(
hass,
devices,
"02f7256e-8353-5bdd-547f-bd5b1647e01b",
Capability.DRYER_OPERATING_STATE,
Attribute.MACHINE_STATE,
"run",
)
assert hass.states.get("select.dryer").state == "run"
@pytest.mark.parametrize("device_fixture", ["da_wm_wd_000001"])
async def test_select_option(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test state update."""
set_attribute_value(
devices,
Capability.REMOTE_CONTROL_STATUS,
Attribute.REMOTE_CONTROL_ENABLED,
"true",
)
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: "select.dryer", ATTR_OPTION: "run"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"02f7256e-8353-5bdd-547f-bd5b1647e01b",
Capability.DRYER_OPERATING_STATE,
Command.SET_MACHINE_STATE,
MAIN,
argument="run",
)
@pytest.mark.parametrize("device_fixture", ["da_wm_wd_000001"])
async def test_select_option_without_remote_control(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test state update."""
set_attribute_value(
devices,
Capability.REMOTE_CONTROL_STATUS,
Attribute.REMOTE_CONTROL_ENABLED,
"false",
)
await setup_integration(hass, mock_config_entry)
with pytest.raises(
ServiceValidationError,
match="Can only be updated when remote control is enabled",
):
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: "select.dryer", ATTR_OPTION: "run"},
blocking=True,
)
devices.execute_device_command.assert_not_called()