diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index c5c18efa5a1..9cfdb8da7ec 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -27,6 +27,14 @@ "stop": "mdi:stop" } } + }, + "switch": { + "wrinkle_prevent": { + "default": "mdi:tumble-dryer", + "state": { + "off": "mdi:tumble-dryer-off" + } + } } } } diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index c534c2ba29d..9616c97fbe1 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -442,6 +442,11 @@ "freeze_protection": "Freeze protection" } } + }, + "switch": { + "wrinkle_prevent": { + "name": "Wrinkle prevent" + } } }, "issues": { diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index 380005f1b93..6e0dc1ac93d 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -2,15 +2,16 @@ from __future__ import annotations +from dataclasses import dataclass from typing import Any -from pysmartthings import Attribute, Capability, Command +from pysmartthings import Attribute, Capability, Command, SmartThings -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import SmartThingsConfigEntry +from . import FullDevice, SmartThingsConfigEntry from .const import MAIN from .entity import SmartThingsEntity @@ -29,6 +30,37 @@ AC_CAPABILITIES = ( ) +@dataclass(frozen=True, kw_only=True) +class SmartThingsSwitchEntityDescription(SwitchEntityDescription): + """Describe a SmartThings switch entity.""" + + status_attribute: Attribute + + +@dataclass(frozen=True, kw_only=True) +class SmartThingsCommandSwitchEntityDescription(SmartThingsSwitchEntityDescription): + """Describe a SmartThings switch entity.""" + + command: Command + + +SWITCH = SmartThingsSwitchEntityDescription( + key=Capability.SWITCH, + status_attribute=Attribute.SWITCH, + name=None, +) +CAPABILITY_TO_COMMAND_SWITCHES: dict[ + Capability | str, SmartThingsCommandSwitchEntityDescription +] = { + Capability.CUSTOM_DRYER_WRINKLE_PREVENT: SmartThingsCommandSwitchEntityDescription( + key=Capability.CUSTOM_DRYER_WRINKLE_PREVENT, + translation_key="wrinkle_prevent", + status_attribute=Attribute.DRYER_WRINKLE_PREVENT, + command=Command.SET_DRYER_WRINKLE_PREVENT, + ) +} + + async def async_setup_entry( hass: HomeAssistant, entry: SmartThingsConfigEntry, @@ -36,35 +68,89 @@ async def async_setup_entry( ) -> None: """Add switches for a config entry.""" entry_data = entry.runtime_data - async_add_entities( - SmartThingsSwitch(entry_data.client, device, {Capability.SWITCH}) + entities: list[SmartThingsEntity] = [ + SmartThingsSwitch(entry_data.client, device, SWITCH, Capability.SWITCH) for device in entry_data.devices.values() if Capability.SWITCH in device.status[MAIN] and not any(capability in device.status[MAIN] for capability in CAPABILITIES) and not all(capability in device.status[MAIN] for capability in AC_CAPABILITIES) + ] + entities.extend( + SmartThingsCommandSwitch( + entry_data.client, + device, + description, + Capability(capability), + ) + for device in entry_data.devices.values() + for capability, description in CAPABILITY_TO_COMMAND_SWITCHES.items() + if capability in device.status[MAIN] ) + async_add_entities(entities) class SmartThingsSwitch(SmartThingsEntity, SwitchEntity): """Define a SmartThings switch.""" - _attr_name = None + entity_description: SmartThingsSwitchEntityDescription + + def __init__( + self, + client: SmartThings, + device: FullDevice, + entity_description: SmartThingsSwitchEntityDescription, + capability: Capability, + ) -> None: + """Initialize the switch.""" + super().__init__(client, device, {capability}) + self.entity_description = entity_description + self.switch_capability = capability + self._attr_unique_id = device.device.device_id + if capability is not Capability.SWITCH: + self._attr_unique_id = f"{device.device.device_id}_{MAIN}_{capability}" async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.execute_device_command( - Capability.SWITCH, + self.switch_capability, Command.OFF, ) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.execute_device_command( - Capability.SWITCH, + self.switch_capability, Command.ON, ) @property def is_on(self) -> bool: - """Return true if light is on.""" - return self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on" + """Return true if switch is on.""" + return ( + self.get_attribute_value( + self.switch_capability, self.entity_description.status_attribute + ) + == "on" + ) + + +class SmartThingsCommandSwitch(SmartThingsSwitch): + """Define a SmartThings command switch.""" + + entity_description: SmartThingsCommandSwitchEntityDescription + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self.execute_device_command( + self.switch_capability, + self.entity_description.command, + "off", + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self.execute_device_command( + self.switch_capability, + self.entity_description.command, + "on", + ) diff --git a/tests/components/smartthings/snapshots/test_switch.ambr b/tests/components/smartthings/snapshots/test_switch.ambr index 08db5ffc244..40f242e82f5 100644 --- a/tests/components/smartthings/snapshots/test_switch.ambr +++ b/tests/components/smartthings/snapshots/test_switch.ambr @@ -281,6 +281,53 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_wm_wd_000001][switch.dryer_wrinkle_prevent-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.dryer_wrinkle_prevent', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wrinkle prevent', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wrinkle_prevent', + 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_custom.dryerWrinklePrevent', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wd_000001][switch.dryer_wrinkle_prevent-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dryer Wrinkle prevent', + }), + 'context': , + 'entity_id': 'switch.dryer_wrinkle_prevent', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_wm_wd_000001_1][switch.seca_roupa-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -328,6 +375,53 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_wm_wd_000001_1][switch.seca_roupa_wrinkle_prevent-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.seca_roupa_wrinkle_prevent', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wrinkle prevent', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wrinkle_prevent', + 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_custom.dryerWrinklePrevent', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wd_000001_1][switch.seca_roupa_wrinkle_prevent-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Seca-Roupa Wrinkle prevent', + }), + 'context': , + 'entity_id': 'switch.seca_roupa_wrinkle_prevent', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_wm_wm_000001][switch.washer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index a1e420a8edb..28bac49b0b0 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -66,6 +66,39 @@ async def test_switch_turn_on_off( ) +@pytest.mark.parametrize("device_fixture", ["da_wm_wd_000001"]) +@pytest.mark.parametrize( + ("action", "argument"), + [ + (SERVICE_TURN_ON, "on"), + (SERVICE_TURN_OFF, "off"), + ], +) +async def test_command_switch_turn_on_off( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + action: str, + argument: str, +) -> None: + """Test switch turn on and off command.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SWITCH_DOMAIN, + action, + {ATTR_ENTITY_ID: "switch.dryer_wrinkle_prevent"}, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "02f7256e-8353-5bdd-547f-bd5b1647e01b", + Capability.CUSTOM_DRYER_WRINKLE_PREVENT, + Command.SET_DRYER_WRINKLE_PREVENT, + MAIN, + argument, + ) + + @pytest.mark.parametrize("device_fixture", ["c2c_arlo_pro_3_switch"]) async def test_state_update( hass: HomeAssistant,