1
0
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:
Raphael Hehl
2026-07-10 22:04:14 +02:00
committed by GitHub
parent b34412d10f
commit eb09af591e
7 changed files with 99 additions and 55 deletions
@@ -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:
+36 -1
View File
@@ -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,
+21 -11
View File
@@ -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}_")]