Add metadata for UniFi Protect Media Source (#109389)
parent
f0f3773858
commit
ae60f59bed
|
@ -47,6 +47,7 @@ class SimpleEventType(str, Enum):
|
|||
RING = "ring"
|
||||
MOTION = "motion"
|
||||
SMART = "smart"
|
||||
AUDIO = "audio"
|
||||
|
||||
|
||||
class IdentifierType(str, Enum):
|
||||
|
@ -64,21 +65,29 @@ class IdentifierTimeType(str, Enum):
|
|||
RANGE = "range"
|
||||
|
||||
|
||||
EVENT_MAP = {
|
||||
SimpleEventType.ALL: None,
|
||||
SimpleEventType.RING: EventType.RING,
|
||||
SimpleEventType.MOTION: EventType.MOTION,
|
||||
SimpleEventType.SMART: EventType.SMART_DETECT,
|
||||
EVENT_MAP: dict[SimpleEventType, set[EventType]] = {
|
||||
SimpleEventType.ALL: {
|
||||
EventType.RING,
|
||||
EventType.MOTION,
|
||||
EventType.SMART_DETECT,
|
||||
EventType.SMART_DETECT_LINE,
|
||||
EventType.SMART_AUDIO_DETECT,
|
||||
},
|
||||
SimpleEventType.RING: {EventType.RING},
|
||||
SimpleEventType.MOTION: {EventType.MOTION},
|
||||
SimpleEventType.SMART: {EventType.SMART_DETECT, EventType.SMART_DETECT_LINE},
|
||||
SimpleEventType.AUDIO: {EventType.SMART_AUDIO_DETECT},
|
||||
}
|
||||
EVENT_NAME_MAP = {
|
||||
SimpleEventType.ALL: "All Events",
|
||||
SimpleEventType.RING: "Ring Events",
|
||||
SimpleEventType.MOTION: "Motion Events",
|
||||
SimpleEventType.SMART: "Smart Detections",
|
||||
SimpleEventType.SMART: "Object Detections",
|
||||
SimpleEventType.AUDIO: "Audio Detections",
|
||||
}
|
||||
|
||||
|
||||
def get_ufp_event(event_type: SimpleEventType) -> EventType | None:
|
||||
def get_ufp_event(event_type: SimpleEventType) -> set[EventType]:
|
||||
"""Get UniFi Protect event type from SimpleEventType."""
|
||||
|
||||
return EVENT_MAP[event_type]
|
||||
|
@ -132,6 +141,51 @@ def _format_duration(duration: timedelta) -> str:
|
|||
return formatted.strip()
|
||||
|
||||
|
||||
@callback
|
||||
def _get_object_name(event: Event | dict[str, Any]) -> str:
|
||||
if isinstance(event, Event):
|
||||
event = event.unifi_dict()
|
||||
|
||||
names = []
|
||||
types = set(event["smartDetectTypes"])
|
||||
metadata = event.get("metadata") or {}
|
||||
for thumb in metadata.get("detectedThumbnails", []):
|
||||
thumb_type = thumb.get("type")
|
||||
if thumb_type not in types:
|
||||
continue
|
||||
|
||||
types.remove(thumb_type)
|
||||
if thumb_type == SmartDetectObjectType.VEHICLE.value:
|
||||
attributes = thumb.get("attributes") or {}
|
||||
color = attributes.get("color", {}).get("val", "")
|
||||
vehicle_type = attributes.get("vehicleType", {}).get("val", "vehicle")
|
||||
license_plate = metadata.get("licensePlate", {}).get("name")
|
||||
|
||||
name = f"{color} {vehicle_type}".strip().title()
|
||||
if license_plate:
|
||||
types.remove(SmartDetectObjectType.LICENSE_PLATE.value)
|
||||
name = f"{name}: {license_plate}"
|
||||
names.append(name)
|
||||
else:
|
||||
smart_type = SmartDetectObjectType(thumb_type)
|
||||
names.append(smart_type.name.title().replace("_", " "))
|
||||
|
||||
for raw in types:
|
||||
smart_type = SmartDetectObjectType(raw)
|
||||
names.append(smart_type.name.title().replace("_", " "))
|
||||
|
||||
return ", ".join(sorted(names))
|
||||
|
||||
|
||||
@callback
|
||||
def _get_audio_name(event: Event | dict[str, Any]) -> str:
|
||||
if isinstance(event, Event):
|
||||
event = event.unifi_dict()
|
||||
|
||||
smart_types = [SmartDetectObjectType(e) for e in event["smartDetectTypes"]]
|
||||
return ", ".join([s.name.title().replace("_", " ") for s in smart_types])
|
||||
|
||||
|
||||
class ProtectMediaSource(MediaSource):
|
||||
"""Represents all UniFi Protect NVRs."""
|
||||
|
||||
|
@ -384,7 +438,7 @@ class ProtectMediaSource(MediaSource):
|
|||
end = event.end
|
||||
else:
|
||||
event_id = event["id"]
|
||||
event_type = event["type"]
|
||||
event_type = EventType(event["type"])
|
||||
start = from_js_time(event["start"])
|
||||
end = from_js_time(event["end"])
|
||||
|
||||
|
@ -393,19 +447,14 @@ class ProtectMediaSource(MediaSource):
|
|||
title = dt_util.as_local(start).strftime("%x %X")
|
||||
duration = end - start
|
||||
title += f" {_format_duration(duration)}"
|
||||
if event_type == EventType.RING.value:
|
||||
if event_type in EVENT_MAP[SimpleEventType.RING]:
|
||||
event_text = "Ring Event"
|
||||
elif event_type == EventType.MOTION.value:
|
||||
elif event_type in EVENT_MAP[SimpleEventType.MOTION]:
|
||||
event_text = "Motion Event"
|
||||
elif event_type == EventType.SMART_DETECT.value:
|
||||
if isinstance(event, Event):
|
||||
smart_types = event.smart_detect_types
|
||||
else:
|
||||
smart_types = [
|
||||
SmartDetectObjectType(e) for e in event["smartDetectTypes"]
|
||||
]
|
||||
smart_type_names = [s.name.title().replace("_", " ") for s in smart_types]
|
||||
event_text = f"Smart Detection - {','.join(smart_type_names)}"
|
||||
elif event_type in EVENT_MAP[SimpleEventType.SMART]:
|
||||
event_text = f"Object Detection - {_get_object_name(event)}"
|
||||
elif event_type in EVENT_MAP[SimpleEventType.AUDIO]:
|
||||
event_text = f"Audio Detection - {_get_audio_name(event)}"
|
||||
title += f" {event_text}"
|
||||
|
||||
nvr = data.api.bootstrap.nvr
|
||||
|
@ -442,20 +491,13 @@ class ProtectMediaSource(MediaSource):
|
|||
start: datetime,
|
||||
end: datetime,
|
||||
camera_id: str | None = None,
|
||||
event_type: EventType | None = None,
|
||||
event_types: set[EventType] | None = None,
|
||||
reserve: bool = False,
|
||||
) -> list[BrowseMediaSource]:
|
||||
"""Build media source for a given range of time and event type."""
|
||||
|
||||
if event_type is None:
|
||||
types = [
|
||||
EventType.RING,
|
||||
EventType.MOTION,
|
||||
EventType.SMART_DETECT,
|
||||
]
|
||||
else:
|
||||
types = [event_type]
|
||||
|
||||
event_types = event_types or get_ufp_event(SimpleEventType.ALL)
|
||||
types = list(event_types)
|
||||
sources: list[BrowseMediaSource] = []
|
||||
events = await data.api.get_events_raw(
|
||||
start=start, end=end, types=types, limit=data.max_events
|
||||
|
@ -515,9 +557,8 @@ class ProtectMediaSource(MediaSource):
|
|||
"start": now - timedelta(days=days),
|
||||
"end": now,
|
||||
"reserve": True,
|
||||
"event_types": get_ufp_event(event_type),
|
||||
}
|
||||
if event_type != SimpleEventType.ALL:
|
||||
args["event_type"] = get_ufp_event(event_type)
|
||||
|
||||
camera: Camera | None = None
|
||||
if camera_id != "all":
|
||||
|
@ -646,9 +687,8 @@ class ProtectMediaSource(MediaSource):
|
|||
"start": start_dt,
|
||||
"end": end_dt,
|
||||
"reserve": False,
|
||||
"event_types": get_ufp_event(event_type),
|
||||
}
|
||||
if event_type != SimpleEventType.ALL:
|
||||
args["event_type"] = get_ufp_event(event_type)
|
||||
|
||||
camera: Camera | None = None
|
||||
if camera_id != "all":
|
||||
|
@ -798,6 +838,9 @@ class ProtectMediaSource(MediaSource):
|
|||
source.children.append(
|
||||
await self._build_events_type(data, camera_id, SimpleEventType.SMART)
|
||||
)
|
||||
source.children.append(
|
||||
await self._build_events_type(data, camera_id, SimpleEventType.AUDIO)
|
||||
)
|
||||
|
||||
if is_doorbell or has_smart:
|
||||
source.children.insert(
|
||||
|
|
|
@ -421,15 +421,17 @@ async def test_browse_media_event_type(
|
|||
|
||||
assert browse.title == "UnifiProtect > All Cameras"
|
||||
assert browse.identifier == "test_id:browse:all"
|
||||
assert len(browse.children) == 4
|
||||
assert len(browse.children) == 5
|
||||
assert browse.children[0].title == "All Events"
|
||||
assert browse.children[0].identifier == "test_id:browse:all:all"
|
||||
assert browse.children[1].title == "Ring Events"
|
||||
assert browse.children[1].identifier == "test_id:browse:all:ring"
|
||||
assert browse.children[2].title == "Motion Events"
|
||||
assert browse.children[2].identifier == "test_id:browse:all:motion"
|
||||
assert browse.children[3].title == "Smart Detections"
|
||||
assert browse.children[3].title == "Object Detections"
|
||||
assert browse.children[3].identifier == "test_id:browse:all:smart"
|
||||
assert browse.children[4].title == "Audio Detections"
|
||||
assert browse.children[4].identifier == "test_id:browse:all:audio"
|
||||
|
||||
|
||||
ONE_MONTH_SIMPLE = (
|
||||
|
@ -649,24 +651,232 @@ async def test_browse_media_recent_truncated(
|
|||
assert browse.children[0].identifier == "test_id:event:test_event_id"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("event", "expected_title"),
|
||||
[
|
||||
(
|
||||
Event(
|
||||
id="test_event_id",
|
||||
type=EventType.RING,
|
||||
start=datetime(1000, 1, 1, 0, 0, 0),
|
||||
end=None,
|
||||
score=100,
|
||||
smart_detect_types=[],
|
||||
smart_detect_event_ids=[],
|
||||
camera_id="test",
|
||||
),
|
||||
"Ring Event",
|
||||
),
|
||||
(
|
||||
Event(
|
||||
id="test_event_id",
|
||||
type=EventType.MOTION,
|
||||
start=datetime(1000, 1, 1, 0, 0, 0),
|
||||
end=None,
|
||||
score=100,
|
||||
smart_detect_types=[],
|
||||
smart_detect_event_ids=[],
|
||||
camera_id="test",
|
||||
),
|
||||
"Motion Event",
|
||||
),
|
||||
(
|
||||
Event(
|
||||
id="test_event_id",
|
||||
type=EventType.SMART_DETECT,
|
||||
start=datetime(1000, 1, 1, 0, 0, 0),
|
||||
end=None,
|
||||
score=100,
|
||||
smart_detect_types=["person"],
|
||||
smart_detect_event_ids=[],
|
||||
camera_id="test",
|
||||
metadata={
|
||||
"detected_thumbnails": [
|
||||
{
|
||||
"clock_best_wall": datetime(1000, 1, 1, 0, 0, 0),
|
||||
"type": "person",
|
||||
"cropped_id": "event_id",
|
||||
}
|
||||
],
|
||||
},
|
||||
),
|
||||
"Object Detection - Person",
|
||||
),
|
||||
(
|
||||
Event(
|
||||
id="test_event_id",
|
||||
type=EventType.SMART_DETECT,
|
||||
start=datetime(1000, 1, 1, 0, 0, 0),
|
||||
end=None,
|
||||
score=100,
|
||||
smart_detect_types=["vehicle", "person"],
|
||||
smart_detect_event_ids=[],
|
||||
camera_id="test",
|
||||
),
|
||||
"Object Detection - Person, Vehicle",
|
||||
),
|
||||
(
|
||||
Event(
|
||||
id="test_event_id",
|
||||
type=EventType.SMART_DETECT,
|
||||
start=datetime(1000, 1, 1, 0, 0, 0),
|
||||
end=None,
|
||||
score=100,
|
||||
smart_detect_types=["vehicle", "licensePlate"],
|
||||
smart_detect_event_ids=[],
|
||||
camera_id="test",
|
||||
),
|
||||
"Object Detection - License Plate, Vehicle",
|
||||
),
|
||||
(
|
||||
Event(
|
||||
id="test_event_id",
|
||||
type=EventType.SMART_DETECT,
|
||||
start=datetime(1000, 1, 1, 0, 0, 0),
|
||||
end=None,
|
||||
score=100,
|
||||
smart_detect_types=["vehicle", "licensePlate"],
|
||||
smart_detect_event_ids=[],
|
||||
camera_id="test",
|
||||
metadata={
|
||||
"license_plate": {"name": "ABC1234", "confidence_level": 95},
|
||||
"detected_thumbnails": [
|
||||
{
|
||||
"clock_best_wall": datetime(1000, 1, 1, 0, 0, 0),
|
||||
"type": "vehicle",
|
||||
"cropped_id": "event_id",
|
||||
}
|
||||
],
|
||||
},
|
||||
),
|
||||
"Object Detection - Vehicle: ABC1234",
|
||||
),
|
||||
(
|
||||
Event(
|
||||
id="test_event_id",
|
||||
type=EventType.SMART_DETECT,
|
||||
start=datetime(1000, 1, 1, 0, 0, 0),
|
||||
end=None,
|
||||
score=100,
|
||||
smart_detect_types=["vehicle", "licensePlate"],
|
||||
smart_detect_event_ids=[],
|
||||
camera_id="test",
|
||||
metadata={
|
||||
"license_plate": {"name": "ABC1234", "confidence_level": 95},
|
||||
"detected_thumbnails": [
|
||||
{
|
||||
"clock_best_wall": datetime(1000, 1, 1, 0, 0, 0),
|
||||
"type": "vehicle",
|
||||
"cropped_id": "event_id",
|
||||
"attributes": {
|
||||
"vehicle_type": {
|
||||
"val": "car",
|
||||
"confidence": 95,
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
),
|
||||
"Object Detection - Car: ABC1234",
|
||||
),
|
||||
(
|
||||
Event(
|
||||
id="test_event_id",
|
||||
type=EventType.SMART_DETECT,
|
||||
start=datetime(1000, 1, 1, 0, 0, 0),
|
||||
end=None,
|
||||
score=100,
|
||||
smart_detect_types=["vehicle", "licensePlate"],
|
||||
smart_detect_event_ids=[],
|
||||
camera_id="test",
|
||||
metadata={
|
||||
"license_plate": {"name": "ABC1234", "confidence_level": 95},
|
||||
"detected_thumbnails": [
|
||||
{
|
||||
"clock_best_wall": datetime(1000, 1, 1, 0, 0, 0),
|
||||
"type": "vehicle",
|
||||
"cropped_id": "event_id",
|
||||
"attributes": {
|
||||
"color": {
|
||||
"val": "black",
|
||||
"confidence": 95,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"clock_best_wall": datetime(1000, 1, 1, 0, 0, 0),
|
||||
"type": "person",
|
||||
"cropped_id": "event_id",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
"Object Detection - Black Vehicle: ABC1234",
|
||||
),
|
||||
(
|
||||
Event(
|
||||
id="test_event_id",
|
||||
type=EventType.SMART_DETECT,
|
||||
start=datetime(1000, 1, 1, 0, 0, 0),
|
||||
end=None,
|
||||
score=100,
|
||||
smart_detect_types=["vehicle"],
|
||||
smart_detect_event_ids=[],
|
||||
camera_id="test",
|
||||
metadata={
|
||||
"detected_thumbnails": [
|
||||
{
|
||||
"clock_best_wall": datetime(1000, 1, 1, 0, 0, 0),
|
||||
"type": "vehicle",
|
||||
"cropped_id": "event_id",
|
||||
"attributes": {
|
||||
"color": {
|
||||
"val": "black",
|
||||
"confidence": 95,
|
||||
},
|
||||
"vehicle_type": {
|
||||
"val": "car",
|
||||
"confidence": 95,
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
"Object Detection - Black Car",
|
||||
),
|
||||
(
|
||||
Event(
|
||||
id="test_event_id",
|
||||
type=EventType.SMART_AUDIO_DETECT,
|
||||
start=datetime(1000, 1, 1, 0, 0, 0),
|
||||
end=None,
|
||||
score=100,
|
||||
smart_detect_types=["alrmSpeak"],
|
||||
smart_detect_event_ids=[],
|
||||
camera_id="test",
|
||||
),
|
||||
"Audio Detection - Speak",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_browse_media_event(
|
||||
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime
|
||||
hass: HomeAssistant,
|
||||
ufp: MockUFPFixture,
|
||||
doorbell: Camera,
|
||||
fixed_now: datetime,
|
||||
event: Event,
|
||||
expected_title: str,
|
||||
) -> None:
|
||||
"""Test browsing specific event."""
|
||||
|
||||
ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap)
|
||||
await init_entry(hass, ufp, [doorbell], regenerate_ids=False)
|
||||
|
||||
event = Event(
|
||||
id="test_event_id",
|
||||
type=EventType.RING,
|
||||
start=fixed_now - timedelta(seconds=20),
|
||||
end=fixed_now,
|
||||
score=100,
|
||||
smart_detect_types=[],
|
||||
smart_detect_event_ids=[],
|
||||
camera_id=doorbell.id,
|
||||
)
|
||||
event.start = fixed_now - timedelta(seconds=20)
|
||||
event.end = fixed_now
|
||||
event.camera_id = doorbell.id
|
||||
event._api = ufp.api
|
||||
ufp.api.get_event = AsyncMock(return_value=event)
|
||||
|
||||
|
@ -674,10 +884,13 @@ async def test_browse_media_event(
|
|||
media_item = MediaSourceItem(hass, DOMAIN, "test_id:event:test_event_id", None)
|
||||
|
||||
browse = await source.async_browse_media(media_item)
|
||||
# chop off the datetime/duration
|
||||
title = " ".join(browse.title.split(" ")[3:])
|
||||
|
||||
assert browse.identifier == "test_id:event:test_event_id"
|
||||
assert browse.children is None
|
||||
assert browse.media_class == MediaClass.VIDEO
|
||||
assert title == expected_title
|
||||
|
||||
|
||||
async def test_browse_media_eventthumb(
|
||||
|
|
Loading…
Reference in New Issue