From 0802fc8a210a7e950d8312dbd5a3cf4ccabd5122 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 26 May 2025 17:01:11 +0300 Subject: [PATCH] Add switch platform to Amazon Devices (#145588) * Add switch platform to Amazon Devices * apply review comment * make logic generic * test cleanup --- .../components/amazon_devices/__init__.py | 5 +- .../components/amazon_devices/strings.json | 5 + .../components/amazon_devices/switch.py | 84 +++++++++++++++++ .../amazon_devices/snapshots/test_switch.ambr | 48 ++++++++++ .../components/amazon_devices/test_switch.py | 91 +++++++++++++++++++ 5 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/amazon_devices/switch.py create mode 100644 tests/components/amazon_devices/snapshots/test_switch.ambr create mode 100644 tests/components/amazon_devices/test_switch.py diff --git a/homeassistant/components/amazon_devices/__init__.py b/homeassistant/components/amazon_devices/__init__.py index a7318824b4c..c63c8ab7664 100644 --- a/homeassistant/components/amazon_devices/__init__.py +++ b/homeassistant/components/amazon_devices/__init__.py @@ -5,7 +5,10 @@ from homeassistant.core import HomeAssistant from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator -PLATFORMS = [Platform.BINARY_SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: diff --git a/homeassistant/components/amazon_devices/strings.json b/homeassistant/components/amazon_devices/strings.json index edc10aa9d40..a3219eaa449 100644 --- a/homeassistant/components/amazon_devices/strings.json +++ b/homeassistant/components/amazon_devices/strings.json @@ -42,6 +42,11 @@ "bluetooth": { "name": "Bluetooth" } + }, + "switch": { + "do_not_disturb": { + "name": "Do not disturb" + } } } } diff --git a/homeassistant/components/amazon_devices/switch.py b/homeassistant/components/amazon_devices/switch.py new file mode 100644 index 00000000000..428ef3e3b45 --- /dev/null +++ b/homeassistant/components/amazon_devices/switch.py @@ -0,0 +1,84 @@ +"""Support for switches.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Final + +from aioamazondevices.api import AmazonDevice + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import AmazonConfigEntry +from .entity import AmazonEntity + +PARALLEL_UPDATES = 1 + + +@dataclass(frozen=True, kw_only=True) +class AmazonSwitchEntityDescription(SwitchEntityDescription): + """Amazon Devices switch entity description.""" + + is_on_fn: Callable[[AmazonDevice], bool] + subkey: str + method: str + + +SWITCHES: Final = ( + AmazonSwitchEntityDescription( + key="do_not_disturb", + subkey="AUDIO_PLAYER", + translation_key="do_not_disturb", + is_on_fn=lambda _device: _device.do_not_disturb, + method="set_do_not_disturb", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AmazonConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Amazon Devices switches based on a config entry.""" + + coordinator = entry.runtime_data + + async_add_entities( + AmazonSwitchEntity(coordinator, serial_num, switch_desc) + for switch_desc in SWITCHES + for serial_num in coordinator.data + if switch_desc.subkey in coordinator.data[serial_num].capabilities + ) + + +class AmazonSwitchEntity(AmazonEntity, SwitchEntity): + """Switch device.""" + + entity_description: AmazonSwitchEntityDescription + + async def _switch_set_state(self, state: bool) -> None: + """Set desired switch state.""" + method = getattr(self.coordinator.api, self.entity_description.method) + + if TYPE_CHECKING: + assert method is not None + + await method(self.device, state) + await self.coordinator.async_request_refresh() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self._switch_set_state(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self._switch_set_state(False) + + @property + def is_on(self) -> bool: + """Return True if switch is on.""" + return self.entity_description.is_on_fn(self.device) diff --git a/tests/components/amazon_devices/snapshots/test_switch.ambr b/tests/components/amazon_devices/snapshots/test_switch.ambr new file mode 100644 index 00000000000..b6b1d0579d2 --- /dev/null +++ b/tests/components/amazon_devices/snapshots/test_switch.ambr @@ -0,0 +1,48 @@ +# serializer version: 1 +# name: test_all_entities[switch.echo_test_do_not_disturb-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.echo_test_do_not_disturb', + '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': 'Do not disturb', + 'platform': 'amazon_devices', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'do_not_disturb', + 'unique_id': 'echo_test_serial_number-do_not_disturb', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.echo_test_do_not_disturb-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Echo Test Do not disturb', + }), + 'context': , + 'entity_id': 'switch.echo_test_do_not_disturb', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/amazon_devices/test_switch.py b/tests/components/amazon_devices/test_switch.py new file mode 100644 index 00000000000..004d6cce842 --- /dev/null +++ b/tests/components/amazon_devices/test_switch.py @@ -0,0 +1,91 @@ +"""Tests for the Amazon Devices switch platform.""" + +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration +from .conftest import TEST_SERIAL_NUMBER + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.amazon_devices.PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_switch_dnd( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test switching DND.""" + await setup_integration(hass, mock_config_entry) + + entity_id = "switch.echo_test_do_not_disturb" + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert mock_amazon_devices_client.set_do_not_disturb.call_count == 1 + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].do_not_disturb = True + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].do_not_disturb = False + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert mock_amazon_devices_client.set_do_not_disturb.call_count == 2 + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_OFF