mirror of
https://github.com/home-assistant/core.git
synced 2026-07-12 00:57:36 +01:00
Remove UniFi Protect AI Port support (#174378)
This commit is contained in:
@@ -149,10 +149,6 @@ async def async_setup_entry(
|
||||
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))
|
||||
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ DEFAULT_VERIFY_SSL = False
|
||||
DEFAULT_MAX_MEDIA = 1000
|
||||
|
||||
DEVICES_THAT_ADOPT = {
|
||||
ModelType.AIPORT,
|
||||
ModelType.CAMERA,
|
||||
ModelType.LIGHT,
|
||||
ModelType.VIEWPORT,
|
||||
|
||||
@@ -164,7 +164,6 @@ def _async_device_entities(
|
||||
|
||||
|
||||
_ALL_MODEL_TYPES = (
|
||||
ModelType.AIPORT,
|
||||
ModelType.CAMERA,
|
||||
ModelType.LIGHT,
|
||||
ModelType.SENSOR,
|
||||
@@ -208,6 +207,10 @@ def async_all_device_entities(
|
||||
|
||||
device_model_type = ufp_device.model
|
||||
assert device_model_type is not None
|
||||
# Runtime adoption must honor the same model-type allowlist as initial setup,
|
||||
# so unsupported devices (e.g. AI Port) get no entities when adopted live.
|
||||
if device_model_type not in _ALL_MODEL_TYPES:
|
||||
return []
|
||||
descs = _combine_model_descs(device_model_type, model_descriptions, all_descs)
|
||||
return _async_device_entities(
|
||||
data, klass, device_model_type, descs, unadopted_descs, ufp_device
|
||||
|
||||
@@ -11,7 +11,11 @@ from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -109,6 +113,10 @@ async def async_migrate_data(
|
||||
async_deprecate_hdr(hass, entry)
|
||||
_LOGGER.debug("Completed Migrate: async_deprecate_hdr")
|
||||
|
||||
_LOGGER.debug("Start Migrate: async_remove_aiport_devices")
|
||||
async_remove_aiport_devices(hass, entry)
|
||||
_LOGGER.debug("Completed Migrate: async_remove_aiport_devices")
|
||||
|
||||
_LOGGER.debug("Start Migrate: async_migrate_insecure_cameras")
|
||||
async_migrate_insecure_cameras(hass, entry)
|
||||
_LOGGER.debug("Completed Migrate: async_migrate_insecure_cameras")
|
||||
@@ -118,6 +126,35 @@ async def async_migrate_data(
|
||||
_LOGGER.debug("Completed Migrate: async_remove_package_binary_sensor")
|
||||
|
||||
|
||||
# Device type (``ProtectAdoptableDeviceModel.type``) reported by AI Ports. Matched
|
||||
# in the registry so cleanup does not depend on the bundled library still exposing
|
||||
# the AI Port model.
|
||||
_AIPORT_DEVICE_TYPE = "AI Port"
|
||||
|
||||
|
||||
@callback
|
||||
def async_remove_aiport_devices(hass: HomeAssistant, entry: UFPConfigEntry) -> None:
|
||||
"""Remove AI Port devices and their diagnostic-only entities.
|
||||
|
||||
AI Ports only ever exposed diagnostic sensors (no automation-relevant
|
||||
functionality) and behave transparently, extending the camera they back.
|
||||
They have no public API representation, so support is dropped. Devices are
|
||||
matched from the registry (by device type) rather than the live bootstrap, so
|
||||
cleanup works even once the library drops the AI Port model.
|
||||
|
||||
Added in 2026.7.0
|
||||
"""
|
||||
device_registry = dr.async_get(hass)
|
||||
for device in dr.async_entries_for_config_entry(device_registry, entry.entry_id):
|
||||
if device.model_id != _AIPORT_DEVICE_TYPE:
|
||||
continue
|
||||
# Detaching the config entry removes the device (it has no other entry)
|
||||
# and its entities along with it.
|
||||
device_registry.async_update_device(
|
||||
device.id, remove_config_entry_id=entry.entry_id
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_migrate_insecure_cameras(hass: HomeAssistant, entry: UFPConfigEntry) -> None:
|
||||
"""Migrate the legacy plain-RTSP "(insecure)" camera entities.
|
||||
|
||||
@@ -383,42 +383,6 @@ async def test_aiport_no_camera_entities(
|
||||
assert_entity_counts(hass, Platform.CAMERA, 0, 0)
|
||||
|
||||
|
||||
async def test_aiport_stream_issue_cleanup(
|
||||
hass: HomeAssistant,
|
||||
ufp: MockUFPFixture,
|
||||
aiport: AiPort,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Stale public-stream issues for AI Ports are cleaned up on setup."""
|
||||
await init_entry(hass, ufp, [aiport])
|
||||
|
||||
issue_id = f"rtsp_disabled_{aiport.id}"
|
||||
# Simulate a legacy issue created directly (bypass translation validation).
|
||||
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,
|
||||
)
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None
|
||||
|
||||
await hass.config_entries.async_reload(ufp.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
|
||||
assert_entity_counts(hass, Platform.CAMERA, 0, 0)
|
||||
|
||||
|
||||
async def test_snapshot_low_quality_without_stream(
|
||||
hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera
|
||||
) -> None:
|
||||
|
||||
@@ -9,7 +9,11 @@ from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN
|
||||
from homeassistant.components.unifiprotect.const import DOMAIN
|
||||
from homeassistant.const import SERVICE_RELOAD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .utils import MockUFPFixture, init_entry
|
||||
@@ -220,6 +224,37 @@ async def test_deprecate_entity_script(
|
||||
assert issue is None
|
||||
|
||||
|
||||
async def test_migrate_remove_aiport_device(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
ufp: MockUFPFixture,
|
||||
) -> None:
|
||||
"""A leftover AI Port device/entity is removed by type, bootstrap-independent."""
|
||||
mac = "AABBCCDDEEFF"
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=ufp.entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, mac)},
|
||||
model_id="AI Port",
|
||||
)
|
||||
entity = entity_registry.async_get_or_create(
|
||||
Platform.SENSOR,
|
||||
DOMAIN,
|
||||
f"{mac}_uptime",
|
||||
config_entry=ufp.entry,
|
||||
device_id=device.id,
|
||||
)
|
||||
|
||||
# AI Port deliberately absent from the bootstrap — cleanup is registry-based
|
||||
await init_entry(hass, ufp, [])
|
||||
|
||||
assert entity_registry.async_get(entity.entity_id) is None
|
||||
assert (
|
||||
device_registry.async_get_device(connections={(dr.CONNECTION_NETWORK_MAC, mac)})
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_migrate_insecure_camera_redirected(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
|
||||
@@ -692,22 +692,32 @@ async def test_sensor_precision(
|
||||
assert hass.states.get(entity_id).state == "17.49"
|
||||
|
||||
|
||||
async def test_aiport_no_camera_sensor_entities(
|
||||
async def test_aiport_no_sensor_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
ufp: MockUFPFixture,
|
||||
aiport: AiPort,
|
||||
) -> None:
|
||||
"""Test that AI Port devices do not create camera-specific sensor entities."""
|
||||
"""AI Port devices create no entities (support dropped)."""
|
||||
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)
|
||||
assert not [e for e in entities if e.unique_id.startswith(f"{aiport.mac}_")]
|
||||
|
||||
# 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
|
||||
|
||||
async def test_aiport_no_sensor_entities_on_runtime_adopt(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
ufp: MockUFPFixture,
|
||||
sensor_all: Sensor,
|
||||
aiport: AiPort,
|
||||
) -> None:
|
||||
"""An AI Port adopted while running still creates no entities."""
|
||||
await init_entry(hass, ufp, [sensor_all])
|
||||
|
||||
aiport._api = ufp.api
|
||||
aiport.feature_flags = Mock(is_ptz=False)
|
||||
await adopt_devices(hass, ufp, [aiport])
|
||||
|
||||
entities = er.async_entries_for_config_entry(entity_registry, ufp.entry.entry_id)
|
||||
assert not [e for e in entities if e.unique_id.startswith(f"{aiport.mac}_")]
|
||||
|
||||
Reference in New Issue
Block a user