mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 09:23:17 -04:00
Fix Ring switch setup crash on unknown doorbell chime type (#182375)
This commit is contained in:
@@ -156,6 +156,9 @@
|
||||
"api_timeout": {
|
||||
"message": "Timeout communicating with Ring API"
|
||||
},
|
||||
"chime_type_unknown": {
|
||||
"message": "The in-home chime type changed and is no longer supported. Reload the Ring integration."
|
||||
},
|
||||
"no_subscription": {
|
||||
"message": "Ring Protect subscription required for snapshots"
|
||||
},
|
||||
|
||||
@@ -11,10 +11,12 @@ from ring_doorbell.const import DOORBELL_EXISTING_TYPE
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import RingConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RingDataCoordinator
|
||||
from .entity import (
|
||||
DeprecatedInfo,
|
||||
@@ -34,6 +36,40 @@ PARALLEL_UPDATES = 1
|
||||
IN_HOME_CHIME_IS_PRESENT = {v for k, v in DOORBELL_EXISTING_TYPE.items() if k != 2}
|
||||
|
||||
|
||||
def _in_home_chime_exists(device: RingDoorBell) -> bool:
|
||||
"""Return True if the doorbell has an in-home chime."""
|
||||
if device.family != "doorbots":
|
||||
return False
|
||||
try:
|
||||
return device.existing_doorbell_type in IN_HOME_CHIME_IS_PRESENT
|
||||
except KeyError as ex:
|
||||
_LOGGER.debug(
|
||||
"Unknown doorbell chime type %s; skipping in-home chime for %s",
|
||||
ex,
|
||||
device.device_api_id,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _in_home_chime_is_on(device: RingDoorBell) -> bool | None:
|
||||
"""Return if the in-home chime is enabled; None when the chime type is unknown."""
|
||||
try:
|
||||
return device.existing_doorbell_type_enabled or False
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
async def _async_set_in_home_chime_enabled(device: RingDoorBell, enabled: bool) -> None:
|
||||
"""Enable or disable the in-home chime."""
|
||||
try:
|
||||
await device.async_set_existing_doorbell_type_enabled(enabled)
|
||||
except KeyError as ex:
|
||||
_LOGGER.debug("In-home chime type unknown for %s", device.device_api_id)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="chime_type_unknown"
|
||||
) from ex
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class RingSwitchEntityDescription(
|
||||
SwitchEntityDescription,
|
||||
@@ -46,7 +82,7 @@ class RingSwitchEntityDescription(
|
||||
unique_id_fn: Callable[[Self, RingDeviceT], str] = lambda self, device: (
|
||||
f"{device.device_api_id}-{self.key}"
|
||||
)
|
||||
is_on_fn: Callable[[RingDeviceT], bool]
|
||||
is_on_fn: Callable[[RingDeviceT], bool | None]
|
||||
turn_on_fn: Callable[[RingDeviceT], Coroutine[Any, Any, None]]
|
||||
turn_off_fn: Callable[[RingDeviceT], Coroutine[Any, Any, None]]
|
||||
|
||||
@@ -66,15 +102,10 @@ SWITCHES: Sequence[RingSwitchEntityDescription[Any]] = (
|
||||
RingSwitchEntityDescription[RingDoorBell](
|
||||
key="in_home_chime",
|
||||
translation_key="in_home_chime",
|
||||
exists_fn=lambda device: (
|
||||
device.family == "doorbots"
|
||||
and device.existing_doorbell_type in IN_HOME_CHIME_IS_PRESENT
|
||||
),
|
||||
is_on_fn=lambda device: device.existing_doorbell_type_enabled or False,
|
||||
turn_on_fn=lambda device: device.async_set_existing_doorbell_type_enabled(True),
|
||||
turn_off_fn=lambda device: device.async_set_existing_doorbell_type_enabled(
|
||||
False
|
||||
),
|
||||
exists_fn=_in_home_chime_exists,
|
||||
is_on_fn=_in_home_chime_is_on,
|
||||
turn_on_fn=lambda device: _async_set_in_home_chime_enabled(device, True),
|
||||
turn_off_fn=lambda device: _async_set_in_home_chime_enabled(device, False),
|
||||
),
|
||||
RingSwitchEntityDescription[RingDoorBell](
|
||||
key="motion_detection",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""The tests for the Ring switch platform."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
import ring_doorbell
|
||||
from ring_doorbell import RingCapability
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.ring.const import DOMAIN
|
||||
@@ -15,6 +17,7 @@ from homeassistant.const import (
|
||||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -26,6 +29,50 @@ from .common import MockConfigEntry, setup_platform
|
||||
from tests.common import snapshot_platform
|
||||
|
||||
|
||||
class BadChimeDoorbell:
|
||||
"""Doorbell whose chime type is unknown to ring_doorbell."""
|
||||
|
||||
family = "doorbots"
|
||||
id = 987654321
|
||||
device_api_id = 987654321
|
||||
device_id = "aa:bb:cc:dd:ee:ff"
|
||||
name = "Bad Chime Doorbell"
|
||||
|
||||
@property
|
||||
def existing_doorbell_type(self) -> str:
|
||||
"""Mimic ring_doorbell raising for an unknown chime type."""
|
||||
raise KeyError(3)
|
||||
|
||||
def has_capability(self, capability: RingCapability) -> bool:
|
||||
"""Return False for all capabilities."""
|
||||
return False
|
||||
|
||||
|
||||
class ChimeCapabilityLostDoorbell:
|
||||
"""Doorbell whose chime enabled-state read raises after setup."""
|
||||
|
||||
family = "doorbots"
|
||||
id = 987654322
|
||||
device_api_id = 987654322
|
||||
device_id = "aa:bb:cc:dd:ee:00"
|
||||
name = "Chime Capability Lost Doorbell"
|
||||
model = "doorbots"
|
||||
existing_doorbell_type = "Mechanical"
|
||||
|
||||
@property
|
||||
def existing_doorbell_type_enabled(self) -> bool:
|
||||
"""Mimic ring_doorbell raising when the chime type becomes unknown."""
|
||||
raise KeyError(3)
|
||||
|
||||
async def async_set_existing_doorbell_type_enabled(self, value: bool) -> None:
|
||||
"""Mimic ring_doorbell raising when the chime type becomes unknown."""
|
||||
raise KeyError(3)
|
||||
|
||||
def has_capability(self, capability: RingCapability) -> bool:
|
||||
"""Return False for all capabilities."""
|
||||
return False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def create_deprecated_siren_entity(
|
||||
hass: HomeAssistant,
|
||||
@@ -169,3 +216,56 @@ async def test_switch_errors_when_turned_on(
|
||||
)
|
||||
== reauth_expected
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_ring_client", "create_deprecated_siren_entity")
|
||||
async def test_switch_setup_succeeds_with_unknown_chime_type(
|
||||
hass: HomeAssistant, mock_ring_devices: Any
|
||||
) -> None:
|
||||
"""Test that an unknown doorbell chime type does not abort switch setup."""
|
||||
mock_ring_devices.all_devices.append(BadChimeDoorbell())
|
||||
|
||||
await setup_platform(hass, Platform.SWITCH)
|
||||
|
||||
assert hass.states.get("switch.front_siren")
|
||||
assert hass.states.get("switch.front_motion_detection")
|
||||
assert not hass.states.get("switch.bad_chime_doorbell_in_home_chime")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_ring_client", "create_deprecated_siren_entity")
|
||||
async def test_in_home_chime_unknown_when_type_becomes_unreadable(
|
||||
hass: HomeAssistant, mock_ring_devices: Any
|
||||
) -> None:
|
||||
"""Test that an in-home chime switch reads as unknown when its type becomes unreadable."""
|
||||
mock_ring_devices.all_devices.append(ChimeCapabilityLostDoorbell())
|
||||
|
||||
await setup_platform(hass, Platform.SWITCH)
|
||||
|
||||
state = hass.states.get("switch.chime_capability_lost_doorbell_in_home_chime")
|
||||
assert state
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_ring_client", "create_deprecated_siren_entity")
|
||||
async def test_in_home_chime_toggle_errors_when_type_unreadable(
|
||||
hass: HomeAssistant, mock_ring_devices: Any
|
||||
) -> None:
|
||||
"""Test that toggling raises a translated error when the chime type becomes unreadable."""
|
||||
mock_ring_devices.all_devices.append(ChimeCapabilityLostDoorbell())
|
||||
|
||||
await setup_platform(hass, Platform.SWITCH)
|
||||
|
||||
state = hass.states.get("switch.chime_capability_lost_doorbell_in_home_chime")
|
||||
assert state
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
with pytest.raises(HomeAssistantError) as err:
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{"entity_id": state.entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
assert err.value.translation_key == "chime_type_unknown"
|
||||
assert err.value.translation_domain == DOMAIN
|
||||
await hass.async_block_till_done()
|
||||
|
||||
Reference in New Issue
Block a user