Skip to content

Commit 269ef30

Browse files
committed
Fix sync-less doorbell discovery on shared network IDs.
Use homescreen-aware detection so doorbells and minis only attach to sync-module camera lists when a real sync module exists for that network, and add fallback parsing/tests for alternate homescreen device structures.
1 parent 77f7fa1 commit 269ef30

File tree

3 files changed

+119
-8
lines changed

3 files changed

+119
-8
lines changed

blinkpy/blinkpy.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,51 @@ def __init__(
7575
self.homescreen = {}
7676
self.no_owls = no_owls
7777

78+
def _iter_device_dicts(self, payload):
79+
"""Yield device-like dictionaries from potentially nested payloads."""
80+
if isinstance(payload, list):
81+
for item in payload:
82+
yield from self._iter_device_dicts(item)
83+
return
84+
if isinstance(payload, dict):
85+
# Treat this as a device entry when it has the minimum fields.
86+
if "name" in payload and "network_id" in payload:
87+
yield payload
88+
for value in payload.values():
89+
yield from self._iter_device_dicts(value)
90+
91+
def get_homescreen_devices(self, kind):
92+
"""Return homescreen devices for a given kind."""
93+
key_candidates = {
94+
"mini": ["owls", "mini_cameras", "minis"],
95+
"doorbell": ["doorbells", "lotus", "doorbell_cameras"],
96+
}
97+
devices = []
98+
seen = set()
99+
for key in key_candidates.get(kind, []):
100+
payload = self.homescreen.get(key)
101+
for device in self._iter_device_dicts(payload):
102+
signature = (
103+
str(device.get("id")),
104+
str(device.get("network_id")),
105+
str(device.get("name")),
106+
)
107+
if signature in seen:
108+
continue
109+
seen.add(signature)
110+
devices.append(device)
111+
return devices
112+
113+
def has_sync_module_for_network(self, network_id):
114+
"""Check whether homescreen reports a real sync module for network."""
115+
try:
116+
for sync in self.homescreen.get("sync_modules", []):
117+
if str(sync.get("network_id")) == str(network_id):
118+
return True
119+
except AttributeError:
120+
return False
121+
return False
122+
78123
@property
79124
def client_id(self):
80125
"""Return the client id."""
@@ -220,10 +265,12 @@ async def setup_owls(self):
220265
network_list = []
221266
camera_list = []
222267
try:
223-
for owl in self.homescreen["owls"]:
268+
for owl in self.get_homescreen_devices("mini"):
224269
name = owl["name"]
225270
network_id = str(owl["network_id"])
226-
if network_id in self.network_ids:
271+
if network_id in self.network_ids and self.has_sync_module_for_network(
272+
network_id
273+
):
227274
camera_list.append(
228275
{network_id: {"name": name, "id": network_id, "type": "mini"}}
229276
)
@@ -244,10 +291,12 @@ async def setup_lotus(self):
244291
network_list = []
245292
camera_list = []
246293
try:
247-
for lotus in self.homescreen["doorbells"]:
294+
for lotus in self.get_homescreen_devices("doorbell"):
248295
name = lotus["name"]
249296
network_id = str(lotus["network_id"])
250-
if network_id in self.network_ids:
297+
if network_id in self.network_ids and self.has_sync_module_for_network(
298+
network_id
299+
):
251300
camera_list.append(
252301
{
253302
network_id: {
@@ -287,9 +336,11 @@ async def setup_camera_list(self):
287336
lotus_cameras = await self.setup_lotus()
288337
for camera in mini_cameras:
289338
for network, camera_info in camera.items():
339+
all_cameras.setdefault(network, [])
290340
all_cameras[network].append(camera_info)
291341
for camera in lotus_cameras:
292342
for network, camera_info in camera.items():
343+
all_cameras.setdefault(network, [])
293344
all_cameras[network].append(camera_info)
294345
return all_cameras
295346
except (KeyError, TypeError) as ex:

blinkpy/sync_module.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,8 @@ async def update_cameras(self, camera_type=BlinkCamera):
225225
def get_unique_info(self, name):
226226
"""Extract unique information for Minis and Doorbells."""
227227
try:
228-
for type_key in self.type_key_map.values():
229-
for device in self.blink.homescreen[type_key]:
228+
for type_name in self.type_key_map:
229+
for device in self.blink.get_homescreen_devices(type_name):
230230
_LOGGER.debug("checking device %s", device)
231231
if device["name"] == name:
232232
_LOGGER.debug("Found unique_info %s", device)
@@ -551,7 +551,7 @@ async def update_cameras(self, camera_type=BlinkCameraMini):
551551
async def get_camera_info(self, camera_id, **kwargs):
552552
"""Retrieve camera information."""
553553
try:
554-
for owl in self.blink.homescreen["owls"]:
554+
for owl in self.blink.get_homescreen_devices("mini"):
555555
if owl["name"] == self.name:
556556
self.status = owl["enabled"]
557557
return owl
@@ -614,7 +614,7 @@ async def update_cameras(self, camera_type=BlinkDoorbell):
614614
async def get_camera_info(self, camera_id, **kwargs):
615615
"""Retrieve camera information."""
616616
try:
617-
for doorbell in self.blink.homescreen["doorbells"]:
617+
for doorbell in self.blink.get_homescreen_devices("doorbell"):
618618
if doorbell["name"] == self.name:
619619
self.status = doorbell["enabled"]
620620
return doorbell

tests/test_blinkpy.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ async def test_blink_mini_cameras_returned(self):
230230
"""Test that blink mini cameras are found if attached to sync module."""
231231
self.blink.network_ids = ["1234"]
232232
self.blink.homescreen = {
233+
"sync_modules": [{"network_id": 1234}],
233234
"owls": [
234235
{
235236
"id": 1,
@@ -261,6 +262,7 @@ async def test_blink_mini_attached_to_sync(self, mock_usage):
261262
"""Test that blink mini cameras are properly attached to sync module."""
262263
self.blink.network_ids = ["1234"]
263264
self.blink.homescreen = {
265+
"sync_modules": [{"network_id": 1234}],
264266
"owls": [
265267
{
266268
"id": 1,
@@ -322,6 +324,7 @@ async def test_blink_doorbell_attached_to_sync(self, mock_usage):
322324
"""Test that blink doorbell cameras are properly attached to sync module."""
323325
self.blink.network_ids = ["1234"]
324326
self.blink.homescreen = {
327+
"sync_modules": [{"network_id": 1234}],
325328
"doorbells": [
326329
{
327330
"id": 1,
@@ -341,11 +344,66 @@ async def test_blink_doorbell_attached_to_sync(self, mock_usage):
341344
result, {"1234": [{"name": "foo", "id": "1234", "type": "doorbell"}]}
342345
)
343346

347+
@mock.patch("blinkpy.api.request_camera_usage")
348+
async def test_blink_doorbell_with_alt_homescreen_key(self, mock_usage):
349+
"""Test that doorbells are discovered from alternate homescreen keys."""
350+
self.blink.network_ids = ["1234"]
351+
self.blink.homescreen = {
352+
"sync_modules": [{"network_id": 1234}],
353+
"lotus": {
354+
"devices": [
355+
{
356+
"id": 1,
357+
"name": "foo",
358+
"network_id": 1234,
359+
"onboarded": True,
360+
"enabled": True,
361+
"status": "online",
362+
"thumbnail": "/foo/bar",
363+
"serial": "abc123",
364+
}
365+
]
366+
}
367+
}
368+
mock_usage.return_value = {"networks": [{"cameras": [], "network_id": 1234}]}
369+
result = await self.blink.setup_camera_list()
370+
self.assertEqual(
371+
result, {"1234": [{"name": "foo", "id": "1234", "type": "doorbell"}]}
372+
)
373+
374+
@mock.patch("blinkpy.blinkpy.BlinkLotus.start")
375+
@mock.patch("blinkpy.api.request_camera_usage")
376+
async def test_blink_syncless_doorbell_not_in_camera_usage(
377+
self, mock_usage, mock_lotus_start
378+
):
379+
"""Test that sync-less doorbells initialize even without camera_usage network."""
380+
mock_lotus_start.return_value = True
381+
self.blink.network_ids = []
382+
self.blink.homescreen = {
383+
"doorbells": [
384+
{
385+
"id": 1,
386+
"name": "foo",
387+
"network_id": 1234,
388+
"onboarded": True,
389+
"enabled": True,
390+
"status": "online",
391+
"thumbnail": "/foo/bar",
392+
"serial": "abc123",
393+
}
394+
]
395+
}
396+
mock_usage.return_value = {"networks": []}
397+
result = await self.blink.setup_camera_list()
398+
self.assertEqual(result, {})
399+
self.assertEqual(self.blink.sync["foo"].__class__, BlinkLotus)
400+
344401
@mock.patch("blinkpy.api.request_camera_usage")
345402
async def test_blink_multi_doorbell(self, mock_usage):
346403
"""Test that multiple doorbells are properly attached to sync module."""
347404
self.blink.network_ids = ["1234"]
348405
self.blink.homescreen = {
406+
"sync_modules": [{"network_id": 1234}],
349407
"doorbells": [
350408
{
351409
"id": 1,
@@ -384,6 +442,7 @@ async def test_blink_multi_mini(self, mock_usage):
384442
"""Test that multiple minis are properly attached to sync module."""
385443
self.blink.network_ids = ["1234"]
386444
self.blink.homescreen = {
445+
"sync_modules": [{"network_id": 1234}],
387446
"owls": [
388447
{
389448
"id": 1,
@@ -422,6 +481,7 @@ async def test_blink_camera_mix(self, mock_usage):
422481
"""Test that a mix of cameras are properly attached to sync module."""
423482
self.blink.network_ids = ["1234"]
424483
self.blink.homescreen = {
484+
"sync_modules": [{"network_id": 1234}],
425485
"doorbells": [
426486
{
427487
"id": 1,

0 commit comments

Comments
 (0)