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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user