Compare commits

...

3 Commits

Author SHA1 Message Date
abmantis
d0bd5329fb Ensure events get processed 2026-04-08 16:59:35 +01:00
abmantis
86229e6da0 Add test 2026-04-07 22:00:30 +01:00
abmantis
d070400936 Add standard event type for doorbell event entities 2026-04-07 21:53:27 +01:00
8 changed files with 174 additions and 5 deletions

View File

@@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from homeassistant.util.hass_dict import HassKey
from .const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN
from .const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN, DoorbellEventType
_LOGGER = logging.getLogger(__name__)
DATA_COMPONENT: HassKey[EntityComponent[EventEntity]] = HassKey(DOMAIN)
@@ -44,6 +44,7 @@ __all__ = [
"DOMAIN",
"PLATFORM_SCHEMA",
"PLATFORM_SCHEMA_BASE",
"DoorbellEventType",
"EventDeviceClass",
"EventEntity",
"EventEntityDescription",
@@ -189,6 +190,21 @@ class EventEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_)
async def async_internal_added_to_hass(self) -> None:
"""Call when the event entity is added to hass."""
await super().async_internal_added_to_hass()
if (
self.device_class == EventDeviceClass.DOORBELL
and DoorbellEventType.RING not in self.event_types
):
report_issue = self._suggest_report_issue()
_LOGGER.warning(
"Entity %s is a doorbell event entity but does not support "
"the '%s' event type. This will stop working in "
"Home Assistant 2027.4, please %s",
self.entity_id,
DoorbellEventType.RING,
report_issue,
)
if (
(state := await self.async_get_last_state())
and state.state is not None

View File

@@ -1,5 +1,13 @@
"""Provides the constants needed for the component."""
from enum import StrEnum
DOMAIN = "event"
ATTR_EVENT_TYPE = "event_type"
ATTR_EVENT_TYPES = "event_types"
class DoorbellEventType(StrEnum):
"""Standard event types for doorbell device class."""
RING = "ring"

View File

@@ -15,7 +15,14 @@
"name": "Button"
},
"doorbell": {
"name": "Doorbell"
"name": "Doorbell",
"state_attributes": {
"event_type": {
"state": {
"ring": "Ring"
}
}
}
},
"motion": {
"name": "Motion"

View File

@@ -7,6 +7,7 @@ from ring_doorbell import RingCapability, RingEvent as RingAlert
from ring_doorbell.const import KIND_DING, KIND_INTERCOM_UNLOCK, KIND_MOTION
from homeassistant.components.event import (
DoorbellEventType,
EventDeviceClass,
EventEntity,
EventEntityDescription,
@@ -34,7 +35,7 @@ EVENT_DESCRIPTIONS: tuple[RingEventEntityDescription, ...] = (
key=KIND_DING,
translation_key=KIND_DING,
device_class=EventDeviceClass.DOORBELL,
event_types=[KIND_DING],
event_types=[DoorbellEventType.RING, KIND_DING],
capability=RingCapability.DING,
),
RingEventEntityDescription(
@@ -100,6 +101,11 @@ class RingEvent(RingBaseEntity[RingListenCoordinator, RingDeviceT], EventEntity)
@callback
def _handle_coordinator_update(self) -> None:
if (alert := self._get_coordinator_alert()) and not alert.is_update:
if alert.kind == KIND_DING:
# Fire both the standard "ring" event and the legacy "ding"
# event for backward compatibility.
self._trigger_event(DoorbellEventType.RING)
self.async_write_ha_state()
self._async_handle_event(alert.kind)
super()._handle_coordinator_update()

View File

@@ -73,7 +73,14 @@
},
"event": {
"ding": {
"name": "Ding"
"name": "Ding",
"state_attributes": {
"event_type": {
"state": {
"ring": "[%key:component::event::entity_component::doorbell::state_attributes::event_type::state::ring%]"
}
}
}
},
"intercom_unlock": {
"name": "Intercom unlock"

View File

@@ -10,6 +10,7 @@ from homeassistant.components.event import (
ATTR_EVENT_TYPE,
ATTR_EVENT_TYPES,
DOMAIN,
DoorbellEventType,
EventDeviceClass,
EventEntity,
EventEntityDescription,
@@ -344,3 +345,76 @@ async def test_name(hass: HomeAssistant) -> None:
"device_class": "doorbell",
"friendly_name": "Doorbell",
}
@pytest.mark.usefixtures("config_flow_fixture")
async def test_doorbell_missing_ring_event_type(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test warning when a doorbell entity does not include the standard ring event type."""
async def async_setup_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry
) -> bool:
"""Set up test config entry."""
await hass.config_entries.async_forward_entry_setups(
config_entry, [Platform.EVENT]
)
return True
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
mock_integration(
hass,
MockModule(
TEST_DOMAIN,
async_setup_entry=async_setup_entry_init,
),
)
# Doorbell entity WITHOUT the standard "ring" event type
entity_without_ring = EventEntity()
entity_without_ring._attr_event_types = ["ding"]
entity_without_ring._attr_device_class = EventDeviceClass.DOORBELL
entity_without_ring._attr_has_entity_name = True
entity_without_ring.entity_id = "event.doorbell_without_ring"
# Doorbell entity WITH the standard "ring" event type
entity_with_ring = EventEntity()
entity_with_ring._attr_event_types = [DoorbellEventType.RING, "ding"]
entity_with_ring._attr_device_class = EventDeviceClass.DOORBELL
entity_with_ring._attr_has_entity_name = True
entity_with_ring.entity_id = "event.doorbell_with_ring"
# Non-doorbell entity should not warn
entity_button = EventEntity()
entity_button._attr_event_types = ["press"]
entity_button._attr_device_class = EventDeviceClass.BUTTON
entity_button._attr_has_entity_name = True
entity_button.entity_id = "event.button"
async def async_setup_entry_platform(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up test event platform via config entry."""
async_add_entities([entity_without_ring, entity_with_ring, entity_button])
mock_platform(
hass,
f"{TEST_DOMAIN}.{DOMAIN}",
MockPlatform(async_setup_entry=async_setup_entry_platform),
)
config_entry = MockConfigEntry(domain=TEST_DOMAIN)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert (
"Entity event.doorbell_without_ring is a doorbell event entity "
"but does not support the 'ring' event type"
) in caplog.text
assert "event.doorbell_with_ring" not in caplog.text
assert "event.button" not in caplog.text

View File

@@ -7,6 +7,7 @@
'area_id': None,
'capabilities': dict({
'event_types': list([
<DoorbellEventType.RING: 'ring'>,
'ding',
]),
}),
@@ -47,6 +48,7 @@
'device_class': 'doorbell',
'event_type': None,
'event_types': list([
<DoorbellEventType.RING: 'ring'>,
'ding',
]),
'friendly_name': 'Front Door Ding',
@@ -187,6 +189,7 @@
'area_id': None,
'capabilities': dict({
'event_types': list([
<DoorbellEventType.RING: 'ring'>,
'ding',
]),
}),
@@ -227,6 +230,7 @@
'device_class': 'doorbell',
'event_type': None,
'event_types': list([
<DoorbellEventType.RING: 'ring'>,
'ding',
]),
'friendly_name': 'Ingress Ding',

View File

@@ -9,10 +9,11 @@ import pytest
from ring_doorbell import Ring
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.event import DoorbellEventType
from homeassistant.components.ring.binary_sensor import RingEvent
from homeassistant.components.ring.coordinator import RingEventListener
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from .common import MockConfigEntry, setup_platform
@@ -96,3 +97,49 @@ async def test_event(
state = hass.states.get(entity_id)
assert state is not None
assert state.state == start_time_str
async def test_doorbell_ding_fires_standard_ring_event(
hass: HomeAssistant,
mock_ring_client: Ring,
mock_ring_event_listener_class: RingEventListener,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test that a doorbell ding fires both 'ring' and 'ding' events."""
await setup_platform(hass, Platform.EVENT)
start_time_str = "2024-09-04T15:32:53.892+00:00"
start_time = datetime.strptime(start_time_str, "%Y-%m-%dT%H:%M:%S.%f%z")
freezer.move_to(start_time)
on_event_cb = mock_ring_event_listener_class.return_value.add_notification_callback.call_args.args[
0
]
entity_id = "event.front_door_ding"
event_types: list[str] = []
@callback
def state_listener(event: object) -> None:
state = hass.states.get(entity_id)
assert state
event_types.append(state.attributes["event_type"])
hass.bus.async_listen("state_changed", state_listener)
event = RingEvent(
1234546,
FRONT_DOOR_DEVICE_ID,
"Foo",
"Bar",
time.time(),
180,
kind="ding",
state=None,
)
mock_ring_client.active_alerts.return_value = [event]
on_event_cb(event)
await hass.async_block_till_done()
# Both the standard "ring" and legacy "ding" events should have fired
assert DoorbellEventType.RING in event_types
assert "ding" in event_types