diff --git a/homeassistant/components/ring/siren.py b/homeassistant/components/ring/siren.py index 1a008695586..b1452f7aeb5 100644 --- a/homeassistant/components/ring/siren.py +++ b/homeassistant/components/ring/siren.py @@ -5,7 +5,13 @@ from dataclasses import dataclass import logging from typing import Any, Generic, cast -from ring_doorbell import RingChime, RingEventKind, RingGeneric +from ring_doorbell import ( + RingCapability, + RingChime, + RingEventKind, + RingGeneric, + RingStickUpCam, +) from homeassistant.components.siren import ( ATTR_TONE, @@ -61,6 +67,14 @@ SIRENS: tuple[RingSirenEntityDescription[Any], ...] = ( kind=str(kwargs.get(ATTR_TONE) or "") or RingEventKind.DING.value ), ), + RingSirenEntityDescription[RingStickUpCam]( + key="siren", + translation_key="siren", + exists_fn=lambda device: device.has_capability(RingCapability.SIREN), + is_on_fn=lambda device: device.siren > 0, + turn_on_fn=lambda device, _: device.async_set_siren(1), + turn_off_fn=lambda device: device.async_set_siren(0), + ), ) diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index b81bf233ce8..f3a7d9a1252 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -16,6 +16,7 @@ import homeassistant.util.dt as dt_util from . import RingConfigEntry from .coordinator import RingDataCoordinator from .entity import ( + DeprecatedInfo, RingDeviceT, RingEntity, RingEntityDescription, @@ -49,6 +50,9 @@ SWITCHES: Sequence[RingSwitchEntityDescription[Any]] = ( is_on_fn=lambda device: device.siren > 0, turn_on_fn=lambda device: device.async_set_siren(1), turn_off_fn=lambda device: device.async_set_siren(0), + deprecated_info=DeprecatedInfo( + new_platform=Platform.SIREN, breaks_in_ha_version="2025.4.0" + ), ), ) diff --git a/tests/components/ring/snapshots/test_siren.ambr b/tests/components/ring/snapshots/test_siren.ambr index 14fdf63db7b..c49ab2cb30f 100644 --- a/tests/components/ring/snapshots/test_siren.ambr +++ b/tests/components/ring/snapshots/test_siren.ambr @@ -56,3 +56,99 @@ 'state': 'unknown', }) # --- +# name: test_states[siren.front_siren-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'siren', + 'entity_category': None, + 'entity_id': 'siren.front_siren', + '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': 'Siren', + 'platform': 'ring', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'siren', + 'unique_id': '765432', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[siren.front_siren-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Ring.com', + 'friendly_name': 'Front Siren', + 'supported_features': , + }), + 'context': , + 'entity_id': 'siren.front_siren', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_states[siren.internal_siren-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'siren', + 'entity_category': None, + 'entity_id': 'siren.internal_siren', + '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': 'Siren', + 'platform': 'ring', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'siren', + 'unique_id': '345678', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[siren.internal_siren-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Ring.com', + 'friendly_name': 'Internal Siren', + 'supported_features': , + }), + 'context': , + 'entity_id': 'siren.internal_siren', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/ring/test_siren.py b/tests/components/ring/test_siren.py index 6ab1ef0bdf1..6cfe8aecd57 100644 --- a/tests/components/ring/test_siren.py +++ b/tests/components/ring/test_siren.py @@ -6,8 +6,16 @@ import pytest import ring_doorbell from syrupy.assertion import SnapshotAssertion +from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH -from homeassistant.const import Platform +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -184,3 +192,44 @@ async def test_siren_errors_when_turned_on( ) == reauth_expected ) + + +async def test_camera_siren_on_off( + hass: HomeAssistant, mock_ring_client, mock_ring_devices +) -> None: + """Tests siren on a ring camera turns on and off.""" + await setup_platform(hass, Platform.SIREN) + + entity_id = "siren.front_siren" + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + await hass.services.async_call( + SIREN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + downstairs_chime_mock = mock_ring_devices.get_device(765432) + downstairs_chime_mock.async_set_siren.assert_called_once_with(1) + + downstairs_chime_mock.async_set_siren.reset_mock() + + await hass.services.async_call( + SIREN_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + downstairs_chime_mock.async_set_siren.assert_called_once_with(0) + + assert state.state == STATE_OFF diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index 7b10ea0f23d..c0d49ad2896 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -6,8 +6,17 @@ import pytest import ring_doorbell from syrupy.assertion import SnapshotAssertion -from homeassistant.config_entries import SOURCE_REAUTH -from homeassistant.const import Platform +from homeassistant.components.ring.const import DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -17,10 +26,35 @@ from .common import MockConfigEntry, setup_platform from tests.common import snapshot_platform +@pytest.fixture +def create_deprecated_siren_entity( + hass: HomeAssistant, + mock_config_entry: ConfigEntry, + entity_registry: er.EntityRegistry, +): + """Create the entity so it is not ignored by the deprecation check.""" + mock_config_entry.add_to_hass(hass) + + def create_entry(device_name, device_id): + unique_id = f"{device_id}-siren" + + entity_registry.async_get_or_create( + domain=SWITCH_DOMAIN, + platform=DOMAIN, + unique_id=unique_id, + suggested_object_id=f"{device_name}_siren", + config_entry=mock_config_entry, + ) + + create_entry("front", 765432) + create_entry("internal", 345678) + + async def test_entity_registry( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_ring_client, + create_deprecated_siren_entity, ) -> None: """Tests that the devices are registered in the entity registry.""" await setup_platform(hass, Platform.SWITCH) @@ -38,6 +72,7 @@ async def test_states( mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, + create_deprecated_siren_entity, ) -> None: """Test states.""" @@ -47,7 +82,7 @@ async def test_states( async def test_siren_off_reports_correctly( - hass: HomeAssistant, mock_ring_client + hass: HomeAssistant, mock_ring_client, create_deprecated_siren_entity ) -> None: """Tests that the initial state of a device that should be off is correct.""" await setup_platform(hass, Platform.SWITCH) @@ -58,7 +93,7 @@ async def test_siren_off_reports_correctly( async def test_siren_on_reports_correctly( - hass: HomeAssistant, mock_ring_client + hass: HomeAssistant, mock_ring_client, create_deprecated_siren_entity ) -> None: """Tests that the initial state of a device that should be on is correct.""" await setup_platform(hass, Platform.SWITCH) @@ -68,20 +103,36 @@ async def test_siren_on_reports_correctly( assert state.attributes.get("friendly_name") == "Internal Siren" -async def test_siren_can_be_turned_on(hass: HomeAssistant, mock_ring_client) -> None: +async def test_siren_can_be_turned_on_and_off( + hass: HomeAssistant, mock_ring_client, create_deprecated_siren_entity +) -> None: """Tests the siren turns on correctly.""" await setup_platform(hass, Platform.SWITCH) state = hass.states.get("switch.front_siren") - assert state.state == "off" + assert state.state == STATE_OFF await hass.services.async_call( - "switch", "turn_on", {"entity_id": "switch.front_siren"}, blocking=True + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.front_siren"}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("switch.front_siren") - assert state.state == "on" + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.front_siren"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.front_siren") + assert state.state == STATE_OFF @pytest.mark.parametrize( @@ -99,6 +150,7 @@ async def test_switch_errors_when_turned_on( mock_ring_devices, exception_type, reauth_expected, + create_deprecated_siren_entity, ) -> None: """Tests the switch turns on correctly.""" await setup_platform(hass, Platform.SWITCH)