Skip to content

Commit 35e6f50

Browse files
ptarjanbdraco
andauthored
Fix doorbird duplicate unique ID generation (home-assistant#158013)
Co-authored-by: J. Nick Koston <[email protected]>
1 parent 1f68809 commit 35e6f50

File tree

3 files changed

+142
-8
lines changed

3 files changed

+142
-8
lines changed

homeassistant/components/doorbird/device.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ def token(self) -> str:
102102
"""Get token for device."""
103103
return self._token
104104

105+
def _get_hass_url(self) -> str:
106+
"""Get the Home Assistant URL for this device."""
107+
if custom_url := self.custom_url:
108+
return custom_url
109+
return get_url(self._hass, prefer_external=False)
110+
105111
async def async_register_events(self) -> None:
106112
"""Register events on device."""
107113
if not self.door_station_events:
@@ -146,13 +152,7 @@ async def _configure_unconfigured_favorites(
146152

147153
async def _async_register_events(self) -> dict[str, Any]:
148154
"""Register events on device."""
149-
# Override url if another is specified in the configuration
150-
if custom_url := self.custom_url:
151-
hass_url = custom_url
152-
else:
153-
# Get the URL of this server
154-
hass_url = get_url(self._hass, prefer_external=False)
155-
155+
hass_url = self._get_hass_url()
156156
http_fav = await self._async_get_http_favorites()
157157
if any(
158158
# Note that a list comp is used here to ensure all
@@ -191,10 +191,14 @@ async def _async_get_event_config(
191191
self._get_event_name(event): event_type
192192
for event, event_type in DEFAULT_EVENT_TYPES
193193
}
194+
hass_url = self._get_hass_url()
194195
for identifier, data in http_fav.items():
195196
title: str | None = data.get("title")
196197
if not title or not title.startswith("Home Assistant"):
197198
continue
199+
value: str | None = data.get("value")
200+
if not value or not value.startswith(hass_url):
201+
continue # Not our favorite - different HA instance or stale
198202
event = title.partition("(")[2].strip(")")
199203
if input_type := favorite_input_type.get(identifier):
200204
events.append(DoorbirdEvent(event, input_type))

tests/components/doorbird/conftest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ def patch_doorbird_api_entry_points(api: MagicMock) -> Generator[DoorBird]:
8282
"homeassistant.components.doorbird.config_flow.DoorBird",
8383
return_value=api,
8484
),
85+
patch(
86+
"homeassistant.components.doorbird.device.get_url",
87+
return_value="http://127.0.0.1:8123",
88+
),
8589
):
8690
yield api
8791

tests/components/doorbird/test_device.py

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,141 @@
22

33
from copy import deepcopy
44
from http import HTTPStatus
5+
from typing import Any
56

67
from doorbirdpy import DoorBirdScheduleEntry
78
import pytest
89

9-
from homeassistant.components.doorbird.const import CONF_EVENTS
10+
from homeassistant.components.doorbird.const import (
11+
CONF_EVENTS,
12+
DEFAULT_DOORBELL_EVENT,
13+
DEFAULT_MOTION_EVENT,
14+
DOMAIN,
15+
)
1016
from homeassistant.core import HomeAssistant
1117

18+
from . import VALID_CONFIG
1219
from .conftest import DoorbirdMockerType
1320

21+
from tests.common import MockConfigEntry
22+
23+
24+
@pytest.fixture
25+
def doorbird_favorites_with_stale() -> dict[str, dict[str, Any]]:
26+
"""Return favorites fixture with stale favorites from another HA instance.
27+
28+
Creates favorites where identifier "2" has the same event name as "0"
29+
(mydoorbird_doorbell) but points to a different HA instance URL.
30+
These stale favorites should be filtered out.
31+
"""
32+
return {
33+
"http": {
34+
"0": {
35+
"title": "Home Assistant (mydoorbird_doorbell)",
36+
"value": "http://127.0.0.1:8123/api/doorbird/mydoorbird_doorbell?token=test-token",
37+
},
38+
# Stale favorite from a different HA instance - should be filtered out
39+
"2": {
40+
"title": "Home Assistant (mydoorbird_doorbell)",
41+
"value": "http://old-ha-instance:8123/api/doorbird/mydoorbird_doorbell?token=old-token",
42+
},
43+
"5": {
44+
"title": "Home Assistant (mydoorbird_motion)",
45+
"value": "http://127.0.0.1:8123/api/doorbird/mydoorbird_motion?token=test-token",
46+
},
47+
}
48+
}
49+
50+
51+
@pytest.fixture
52+
def doorbird_schedule_with_stale() -> list[DoorBirdScheduleEntry]:
53+
"""Return schedule fixture with outputs referencing stale favorites.
54+
55+
Both param "0" and "2" map to doorbell input, but "2" is a stale favorite.
56+
"""
57+
schedule_data = [
58+
{
59+
"input": "doorbell",
60+
"param": "1",
61+
"output": [
62+
{
63+
"event": "http",
64+
"param": "0",
65+
"schedule": {"weekdays": [{"to": "107999", "from": "108000"}]},
66+
},
67+
{
68+
"event": "http",
69+
"param": "2",
70+
"schedule": {"weekdays": [{"to": "107999", "from": "108000"}]},
71+
},
72+
],
73+
},
74+
{
75+
"input": "motion",
76+
"param": "",
77+
"output": [
78+
{
79+
"event": "http",
80+
"param": "5",
81+
"schedule": {"weekdays": [{"to": "107999", "from": "108000"}]},
82+
},
83+
],
84+
},
85+
]
86+
return DoorBirdScheduleEntry.parse_all(schedule_data)
87+
88+
89+
async def test_stale_favorites_filtered_by_url(
90+
hass: HomeAssistant,
91+
doorbird_mocker: DoorbirdMockerType,
92+
doorbird_favorites_with_stale: dict[str, dict[str, Any]],
93+
doorbird_schedule_with_stale: list[DoorBirdScheduleEntry],
94+
) -> None:
95+
"""Test that stale favorites from other HA instances are filtered out."""
96+
await doorbird_mocker(
97+
favorites=doorbird_favorites_with_stale,
98+
schedule=doorbird_schedule_with_stale,
99+
)
100+
# Should have 2 event entities - stale favorite "2" is filtered out
101+
# because its URL doesn't match the current HA instance
102+
event_entities = hass.states.async_all("event")
103+
assert len(event_entities) == 2
104+
105+
106+
async def test_custom_url_used_for_favorites(
107+
hass: HomeAssistant,
108+
doorbird_mocker: DoorbirdMockerType,
109+
) -> None:
110+
"""Test that custom URL override is used instead of get_url."""
111+
custom_url = "https://my-custom-url.example.com:8443"
112+
favorites = {
113+
"http": {
114+
"1": {
115+
"title": "Home Assistant (mydoorbird_doorbell)",
116+
"value": f"{custom_url}/api/doorbird/mydoorbird_doorbell?token=test-token",
117+
},
118+
"2": {
119+
"title": "Home Assistant (mydoorbird_motion)",
120+
"value": f"{custom_url}/api/doorbird/mydoorbird_motion?token=test-token",
121+
},
122+
}
123+
}
124+
config_with_custom_url = {
125+
**VALID_CONFIG,
126+
"hass_url_override": custom_url,
127+
}
128+
entry = MockConfigEntry(
129+
domain=DOMAIN,
130+
unique_id="1CCAE3AAAAAA",
131+
data=config_with_custom_url,
132+
options={CONF_EVENTS: [DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT]},
133+
)
134+
await doorbird_mocker(entry=entry, favorites=favorites)
135+
136+
# Should have 2 event entities using the custom URL
137+
event_entities = hass.states.async_all("event")
138+
assert len(event_entities) == 2
139+
14140

15141
async def test_no_configured_events(
16142
hass: HomeAssistant,

0 commit comments

Comments
 (0)