1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Exclude AI Port from camera entities and RTSP issues (#161188)

Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
This commit is contained in:
Raphael Hehl
2026-01-28 19:54:15 +01:00
committed by GitHub
parent 360af74519
commit 0c9834e4ca
13 changed files with 213 additions and 14 deletions
@@ -755,7 +755,8 @@ async def async_setup_entry(
entities += async_all_device_entities(
data, klass, model_descriptions=model_descriptions, ufp_device=device
)
if device.is_adopted and isinstance(device, Camera):
# AiPort inherits from Camera but should not create camera-specific entities
if device.is_adopted and device.model is ModelType.CAMERA:
entities += _async_event_entities(data, ufp_device=device)
async_add_entities(entities)
@@ -8,6 +8,7 @@ import logging
from uiprotect.data import (
Camera as UFPCamera,
CameraChannel,
ModelType,
ProtectAdoptableDeviceModel,
StateType,
)
@@ -150,7 +151,8 @@ async def async_setup_entry(
@callback
def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
if not isinstance(device, UFPCamera):
# AiPort inherits from Camera but should not create camera entities
if not isinstance(device, UFPCamera) or device.model is ModelType.AIPORT:
return
async_add_entities(_async_camera_entities(hass, entry, data, ufp_device=device))
@@ -158,6 +160,11 @@ async def async_setup_entry(
entry.async_on_unload(
async_dispatcher_connect(hass, data.channels_signal, _add_new_device)
)
# Clean up any erroneously created RTSP issues for AI Ports
for device in data.get_by_types({ModelType.AIPORT}):
ir.async_delete_issue(hass, DOMAIN, f"rtsp_disabled_{device.id}")
async_add_entities(_async_camera_entities(hass, entry, data))
@@ -5,6 +5,7 @@ from __future__ import annotations
import dataclasses
from typing import Any
from uiprotect.data import ModelType
from uiprotect.data.nvr import Event, EventDetectedThumbnail
from homeassistant.components.event import (
@@ -31,7 +32,6 @@ from .const import (
VEHICLE_EVENT_DELAY_SECONDS,
)
from .data import (
Camera,
EventType,
ProtectAdoptableDeviceModel,
ProtectData,
@@ -423,7 +423,8 @@ async def async_setup_entry(
@callback
def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
if device.is_adopted and isinstance(device, Camera):
# AiPort inherits from Camera but should not create camera-specific entities
if device.is_adopted and device.model is ModelType.CAMERA:
async_add_entities(_async_event_entities(data, ufp_device=device))
data.async_subscribe_adopt(_add_new_device)
@@ -5,7 +5,7 @@ from __future__ import annotations
import logging
from typing import Any
from uiprotect.data import Camera, ProtectAdoptableDeviceModel, StateType
from uiprotect.data import Camera, ModelType, ProtectAdoptableDeviceModel, StateType
from uiprotect.exceptions import StreamError
from homeassistant.components import media_source
@@ -47,8 +47,11 @@ async def async_setup_entry(
@callback
def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
if isinstance(device, Camera) and (
device.has_speaker or device.has_removable_speaker
# AiPort inherits from Camera but should not create camera-specific entities
if (
device.model is ModelType.CAMERA
and isinstance(device, Camera)
and (device.has_speaker or device.has_removable_speaker)
):
async_add_entities([ProtectMediaPlayer(data, device)])
@@ -606,7 +606,12 @@ async def async_setup_entry(
model_descriptions=_MODEL_DESCRIPTIONS,
ufp_device=device,
)
if device.is_adopted_by_us and isinstance(device, Camera):
# AiPort inherits from Camera but should not create camera-specific entities
if (
device.model is ModelType.CAMERA
and isinstance(device, Camera)
and device.is_adopted_by_us
):
entities += _async_event_entities(data, ufp_device=device)
async_add_entities(entities)
+14
View File
@@ -15,6 +15,7 @@ import pytest
from uiprotect import ProtectApiClient
from uiprotect.data import (
NVR,
AiPort,
Bootstrap,
Camera,
Chime,
@@ -397,6 +398,19 @@ def chime():
Chime.model_config["validate_assignment"] = True
@pytest.fixture(name="aiport")
def aiport_fixture():
"""Mock UniFi Protect AI Port device."""
# disable pydantic validation so mocking can happen
AiPort.model_config["validate_assignment"] = False
data = load_json_object_fixture("sample_aiport.json", DOMAIN)
yield AiPort.from_unifi_dict(**data)
AiPort.model_config["validate_assignment"] = True
@pytest.fixture(name="fixed_now")
def fixed_now_fixture():
"""Return datetime object that will be consistent throughout test."""
@@ -0,0 +1,29 @@
{
"id": "696a16ac001e0b03e4008cec",
"mac": "696A16AC001E",
"host": "192.168.1.100",
"connectionHost": "192.168.1.1",
"type": "AI Port",
"name": "Test AI Port",
"upSince": 1640020678036,
"uptime": 3203,
"lastSeen": 1640023881036,
"connectedSince": 1640020710448,
"state": "CONNECTED",
"hardwareRevision": "1",
"firmwareVersion": "1.0.0",
"latestFirmwareVersion": "1.0.0",
"firmwareBuild": "build123",
"isUpdating": false,
"isAdopting": false,
"isAdopted": true,
"isAdoptedByOther": false,
"isProvisioned": true,
"isRebooting": false,
"isSshEnabled": false,
"canAdopt": false,
"isAttemptingToConnect": false,
"isConnected": true,
"marketName": "AI Port",
"modelKey": "aiport"
}
@@ -7,6 +7,7 @@ from unittest.mock import Mock
import pytest
from uiprotect.data import (
AiPort,
Camera,
Event,
EventType,
@@ -723,6 +724,25 @@ async def test_binary_sensor_person_detected(
assert len(state_changes) == 2
async def test_aiport_no_binary_sensor_entities(
hass: HomeAssistant,
ufp: MockUFPFixture,
aiport: AiPort,
) -> None:
"""Test that AI Port devices do not create camera-specific binary sensor entities."""
await init_entry(hass, ufp, [aiport])
# AI Port should not create any camera-specific binary sensors (motion, smart detection, etc.)
# NVR HDD sensors will still be created, but no AI Port-specific entities
entity_registry = er.async_get(hass)
entities = er.async_entries_for_config_entry(entity_registry, ufp.entry.entry_id)
for entity in entities:
if entity.domain == Platform.BINARY_SENSOR:
# No entities should contain the AI Port's device id
assert aiport.id not in entity.unique_id
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_binary_sensor_simultaneous_person_and_vehicle_detection(
hass: HomeAssistant,
+69 -2
View File
@@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, Mock
import pytest
from uiprotect.api import DEVICE_UPDATE_INTERVAL
from uiprotect.data import Camera as ProtectCamera, CameraChannel, StateType
from uiprotect.data import AiPort, Camera as ProtectCamera, CameraChannel, StateType
from uiprotect.exceptions import NvrError
from uiprotect.websocket import WebsocketState
from webrtc_models import RTCIceCandidateInit
@@ -41,7 +41,11 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.setup import async_setup_component
from . import patch_ufp_method
@@ -627,3 +631,66 @@ async def test_camera_motion_detection(
)
mock_method.assert_called_once_with(expected_value)
async def test_aiport_no_camera_entities(
hass: HomeAssistant,
ufp: MockUFPFixture,
aiport: AiPort,
) -> None:
"""Test that AI Port devices do not create camera entities."""
await init_entry(hass, ufp, [aiport])
# AI Port should not create any camera entities
assert_entity_counts(hass, Platform.CAMERA, 0, 0)
async def test_aiport_rtsp_issue_cleanup(
hass: HomeAssistant,
ufp: MockUFPFixture,
aiport: AiPort,
) -> None:
"""Test that RTSP disabled issues for AI Ports are cleaned up on setup."""
# Set up the integration with the AI Port first
# (init_entry regenerates IDs, so we need to get the new ID)
await init_entry(hass, ufp, [aiport])
# Now get the actual AI Port ID after regeneration
actual_aiport_id = aiport.id
# Create an RTSP disabled issue for the AI Port
# (simulating an issue that might have been created by a previous buggy version)
issue_id = f"rtsp_disabled_{actual_aiport_id}"
# Get the issue registry and create the issue directly via internal method
# to avoid translation validation (as we're simulating a legacy issue)
issue_registry = ir.async_get(hass)
issue_registry.issues[(DOMAIN, issue_id)] = ir.IssueEntry(
active=True,
breaks_in_ha_version=None,
created=None,
data=None,
dismissed_version=None,
domain=DOMAIN,
is_fixable=True,
is_persistent=False,
issue_domain=None,
issue_id=issue_id,
learn_more_url=None,
severity=ir.IssueSeverity.WARNING,
translation_key="rtsp_disabled",
translation_placeholders=None,
)
# Verify the issue exists
assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None
# Reload the integration - this should clean up the issue
await hass.config_entries.async_reload(ufp.entry.entry_id)
await hass.async_block_till_done()
# The issue should be cleaned up since AI Ports can't have RTSP
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
# Verify no camera entities were created
assert_entity_counts(hass, Platform.CAMERA, 0, 0)
+20 -1
View File
@@ -7,7 +7,14 @@ from datetime import datetime, timedelta
from unittest.mock import Mock, patch
import pytest
from uiprotect.data import Camera, Event, EventType, ModelType, SmartDetectObjectType
from uiprotect.data import (
AiPort,
Camera,
Event,
EventType,
ModelType,
SmartDetectObjectType,
)
from homeassistant.components.unifiprotect.const import (
ATTR_EVENT_ID,
@@ -1590,3 +1597,15 @@ async def test_vehicle_detection_no_refire_same_data(
assert len(events) == 1
unsub()
async def test_aiport_no_event_entities(
hass: HomeAssistant,
ufp: MockUFPFixture,
aiport: AiPort,
) -> None:
"""Test that AI Port devices do not create camera-specific event entities."""
await init_entry(hass, ufp, [aiport])
# AI Port should not create any camera-specific event entities (doorbell, motion, etc.)
assert_entity_counts(hass, Platform.EVENT, 0, 0)
@@ -5,7 +5,7 @@ from __future__ import annotations
from unittest.mock import AsyncMock, Mock, patch
import pytest
from uiprotect.data import Camera
from uiprotect.data import AiPort, Camera
from uiprotect.exceptions import StreamError
from homeassistant.components.media_player import (
@@ -295,3 +295,15 @@ async def test_media_player_play_error(
assert mock_play.called
assert not mock_wait.called
async def test_aiport_no_media_player_entities(
hass: HomeAssistant,
ufp: MockUFPFixture,
aiport: AiPort,
) -> None:
"""Test that AI Port devices do not create camera-specific media player entities."""
await init_entry(hass, ufp, [aiport])
# AI Port should not create any media player entities (speaker)
assert_entity_counts(hass, Platform.MEDIA_PLAYER, 0, 0)
+22 -1
View File
@@ -6,7 +6,7 @@ from datetime import datetime, timedelta
from unittest.mock import Mock
import pytest
from uiprotect.data import NVR, Camera, Event, EventType, ModelType, Sensor
from uiprotect.data import NVR, AiPort, Camera, Event, EventType, ModelType, Sensor
from uiprotect.data.nvr import EventMetadata
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
@@ -542,3 +542,24 @@ async def test_sensor_precision(
)
assert hass.states.get(entity_id).state == "17.49"
async def test_aiport_no_camera_sensor_entities(
hass: HomeAssistant,
ufp: MockUFPFixture,
aiport: AiPort,
) -> None:
"""Test that AI Port devices do not create camera-specific sensor entities."""
await init_entry(hass, ufp, [aiport])
# AI Port should only create base device sensors, not camera-specific sensors
# The exact count may vary, but camera motion/detection sensors should not exist
entity_registry = er.async_get(hass)
entities = er.async_entries_for_config_entry(entity_registry, ufp.entry.entry_id)
# Check no camera-specific sensors like motion detection exist
for entity in entities:
if entity.domain == Platform.SENSOR:
# Camera-specific sensors should not exist for AI Port
assert "detected_object" not in entity.unique_id
assert "last_motion" not in entity.unique_id
+1 -1
View File
@@ -177,7 +177,7 @@ def add_device(
return
device._api = bootstrap.api
if isinstance(device, Camera):
if isinstance(device, Camera) and device.model is ModelType.CAMERA:
for channel in device.channels:
channel._api = bootstrap.api