Fix Ring switch setup crash on unknown doorbell chime type (#182375)

This commit is contained in:
Rasad Regmi
2026-09-19 14:16:18 +02:00
committed by GitHub
parent 7ca2d9b733
commit a7b90b5cd4
3 changed files with 144 additions and 10 deletions
@@ -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"
},
+41 -10
View File
@@ -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",
+100
View File
@@ -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()