mirror of
https://github.com/home-assistant/core.git
synced 2026-10-06 22:38:02 -04:00
Bump uiprotect to 17.0.0 (#183178)
This commit is contained in:
@@ -73,7 +73,7 @@ def _async_capability_supported(
|
||||
|
||||
Smart-detect capabilities are answered by the master object (the private
|
||||
camera in hybrid, the public one otherwise). Sensor capabilities come from
|
||||
the public capability map; without one every description is created.
|
||||
the public capability map; a sensor without one supports none.
|
||||
"""
|
||||
if (capability := description.ufp_capability) is None:
|
||||
return True
|
||||
@@ -82,7 +82,7 @@ def _async_capability_supported(
|
||||
"Camera | PublicCamera", private if private is not None else public
|
||||
)
|
||||
return camera.can_detect(capability)
|
||||
if not isinstance(public, PublicSensor) or not public.has_feature_flags:
|
||||
if not isinstance(public, PublicSensor):
|
||||
return True
|
||||
return public.supports(capability)
|
||||
|
||||
@@ -96,8 +96,8 @@ def async_remove_unsupported_sense_entities(
|
||||
) -> None:
|
||||
"""Remove registry entries for sense entities the device cannot support.
|
||||
|
||||
Only acts when a public capability map is present (newer firmware); a console
|
||||
upgrade then drops the never-functional entities created before the map existed.
|
||||
Drops the never-functional entities created before the capability map
|
||||
existed, and those of a sensor that reports no map.
|
||||
"""
|
||||
entity_registry = er.async_get(hass)
|
||||
is_public_only = data.api.is_public_only
|
||||
@@ -720,8 +720,8 @@ class ProtectEntityDescription(EntityDescription, Generic[T]): # noqa: UP046
|
||||
# is often compound (e.g. mount type plus a settings flag).
|
||||
ufp_public_enabled_fn: Callable[[PublicDeviceModel], bool] | None = None
|
||||
# Capability required to create the entity: a sensor capability is checked
|
||||
# against the public capability map (without one every description is
|
||||
# created), a smart-detect type against the camera's advertised types.
|
||||
# against the public capability map, a smart-detect type against the
|
||||
# camera's advertised types.
|
||||
ufp_capability: SensorFeatureCapability | SmartDetectObjectType | None = None
|
||||
ufp_perm: PermRequired | None = None
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["uiprotect==16.16.1"]
|
||||
"requirements": ["uiprotect==17.0.0"]
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -3341,7 +3341,7 @@ uasiren==0.0.1
|
||||
uhooapi==1.2.8
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==16.16.1
|
||||
uiprotect==17.0.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.6.1
|
||||
|
||||
@@ -14,7 +14,6 @@ from uiprotect import ProtectApiClient
|
||||
from uiprotect.api import RTSPSStreams
|
||||
from uiprotect.data import (
|
||||
NVR,
|
||||
AiPort,
|
||||
Bootstrap,
|
||||
Camera,
|
||||
Chime,
|
||||
@@ -151,7 +150,6 @@ def bootstrap_fixture(nvr: NVR):
|
||||
data["liveviews"] = []
|
||||
data["events"] = []
|
||||
data["chimes"] = []
|
||||
data["aiports"] = []
|
||||
|
||||
return Bootstrap.from_unifi_dict(**data)
|
||||
|
||||
@@ -516,19 +514,6 @@ 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."""
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -3,8 +3,6 @@
|
||||
dict({
|
||||
'bootstrap': dict({
|
||||
'accessKey': '**REDACTED**',
|
||||
'aiports': list([
|
||||
]),
|
||||
'authUserId': '**REDACTED_ID**',
|
||||
'bridges': list([
|
||||
dict({
|
||||
|
||||
@@ -6,7 +6,6 @@ from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from uiprotect.data import (
|
||||
AiPort,
|
||||
Camera,
|
||||
Event,
|
||||
EventType,
|
||||
@@ -496,14 +495,6 @@ async def test_binary_sensor_sense_leak_public_value(
|
||||
STATE_OFF,
|
||||
id="capability-external",
|
||||
),
|
||||
pytest.param(
|
||||
MountType.NONE,
|
||||
None,
|
||||
True,
|
||||
True,
|
||||
STATE_UNAVAILABLE,
|
||||
id="settings-without-capability-map",
|
||||
),
|
||||
pytest.param(
|
||||
MountType.NONE,
|
||||
set(),
|
||||
@@ -601,21 +592,23 @@ async def test_binary_sensor_sense_capability_registry_cleanup(
|
||||
assert entity_registry.async_get(stale.entity_id) is None
|
||||
|
||||
|
||||
async def test_binary_sensor_sense_no_capability_map_creates_all(
|
||||
async def test_binary_sensor_sense_no_capability_map_creates_none(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
ufp: MockUFPFixture,
|
||||
sensor_all: Sensor,
|
||||
) -> None:
|
||||
"""Without a capability map (older firmware) every sense entity is created."""
|
||||
setup_public_sensor(ufp)
|
||||
"""A sensor without a capability map gets no capability-gated entity."""
|
||||
setup_public_sensor(ufp, capabilities=set())
|
||||
await init_entry(hass, ufp, [sensor_all])
|
||||
|
||||
for description in (SENSE_DOOR, SENSE_TAMPERING, SENSE_MOTION, SENSE_LEAK):
|
||||
_, entity_id = await ids_from_device_description(
|
||||
hass, Platform.BINARY_SENSOR, sensor_all, description
|
||||
)
|
||||
assert entity_registry.async_get(entity_id) is not None
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(
|
||||
Platform.BINARY_SENSOR, DOMAIN, f"{sensor_all.mac}_{description.key}"
|
||||
)
|
||||
is None
|
||||
), description.key
|
||||
|
||||
|
||||
async def test_binary_sensor_sense_tampering_public_value(
|
||||
@@ -979,26 +972,6 @@ async def test_binary_sensor_doorbell_ring(
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
|
||||
|
||||
async def test_aiport_no_binary_sensor_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
ufp: MockUFPFixture,
|
||||
aiport: AiPort,
|
||||
) -> None:
|
||||
"""Test AI Port devices do not create camera-specific binary sensors."""
|
||||
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
|
||||
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,
|
||||
|
||||
@@ -188,17 +188,19 @@ async def test_button_sense_capability_registry_cleanup(
|
||||
assert entity_registry.async_get(stale.entity_id) is None
|
||||
|
||||
|
||||
async def test_button_sense_no_capability_map_creates_clear_tamper(
|
||||
async def test_button_sense_no_capability_map_skips_clear_tamper(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
ufp: MockUFPFixture,
|
||||
sensor_all: Sensor,
|
||||
) -> None:
|
||||
"""Without a capability map (Protect below 7.2) the button is still created."""
|
||||
setup_public_sensor(ufp)
|
||||
"""A sensor without a capability map gets no clear tamper button."""
|
||||
setup_public_sensor(ufp, capabilities=set())
|
||||
await init_entry(hass, ufp, [sensor_all])
|
||||
|
||||
_, entity_id = await ids_from_device_description(
|
||||
hass, Platform.BUTTON, sensor_all, CLEAR_TAMPER
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(
|
||||
Platform.BUTTON, DOMAIN, f"{sensor_all.mac}_{CLEAR_TAMPER.key}"
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert entity_registry.async_get(entity_id) is not None
|
||||
|
||||
@@ -8,7 +8,6 @@ from aiohttp.client_exceptions import ServerDisconnectedError
|
||||
import pytest
|
||||
from uiprotect.api import RTSPSStreams
|
||||
from uiprotect.data import (
|
||||
AiPort,
|
||||
Camera as ProtectCamera,
|
||||
ChannelQuality,
|
||||
DeviceState,
|
||||
@@ -399,14 +398,6 @@ 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:
|
||||
"""AI Port devices do not create camera entities."""
|
||||
await init_entry(hass, ufp, [aiport])
|
||||
assert_entity_counts(hass, Platform.CAMERA, 0, 0)
|
||||
|
||||
|
||||
async def test_public_only_camera(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
|
||||
@@ -11,7 +11,6 @@ from unittest.mock import Mock, patch
|
||||
import pytest
|
||||
from uiprotect import EventChange, ProtectEvent, ProtectEventChannel
|
||||
from uiprotect.data import (
|
||||
AiPort,
|
||||
Camera,
|
||||
Event,
|
||||
EventType,
|
||||
@@ -1770,19 +1769,6 @@ async def test_vehicle_detection_no_refire_same_data(
|
||||
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)
|
||||
|
||||
|
||||
async def test_motion_detection_event(
|
||||
hass: HomeAssistant,
|
||||
ufp: MockUFPFixture,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from uiprotect.data import AiPort, Camera
|
||||
from uiprotect.data import Camera
|
||||
from uiprotect.exceptions import StreamError
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -293,15 +293,3 @@ 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)
|
||||
|
||||
@@ -8,7 +8,6 @@ from unittest.mock import Mock
|
||||
import pytest
|
||||
from uiprotect.data import (
|
||||
NVR,
|
||||
AiPort,
|
||||
Camera,
|
||||
DeviceState,
|
||||
Event,
|
||||
@@ -754,44 +753,6 @@ async def test_sensor_precision(
|
||||
assert hass.states.get(entity_id).state == "17.49"
|
||||
|
||||
|
||||
async def test_aiport_no_sensor_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
ufp: MockUFPFixture,
|
||||
aiport: AiPort,
|
||||
) -> None:
|
||||
"""AI Port devices create no entities (support dropped)."""
|
||||
await init_entry(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}_")]
|
||||
|
||||
# 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}_")]
|
||||
|
||||
|
||||
async def test_sensor_light_last_motion_public(
|
||||
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
|
||||
) -> None:
|
||||
|
||||
@@ -996,44 +996,44 @@ async def test_switch_sense_capability_registry_cleanup(
|
||||
assert entity_registry.async_get(stale.entity_id) is None
|
||||
|
||||
|
||||
async def test_switch_sense_no_capability_map_creates_all(
|
||||
async def test_switch_sense_no_capability_map_creates_none(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
ufp: MockUFPFixture,
|
||||
sensor_all: Sensor,
|
||||
) -> None:
|
||||
"""Without a capability map (Protect below 7.2) every config switch is created."""
|
||||
setup_public_sensor(ufp)
|
||||
"""A sensor without a capability map gets no capability-gated config switch."""
|
||||
setup_public_sensor(ufp, capabilities=set())
|
||||
await init_entry(hass, ufp, [sensor_all])
|
||||
|
||||
for description in SENSE_SWITCHES:
|
||||
_, entity_id = await ids_from_device_description(
|
||||
hass, Platform.SWITCH, sensor_all, description
|
||||
)
|
||||
assert entity_registry.async_get(entity_id) is not None, description.key
|
||||
gated = [desc for desc in SENSE_SWITCHES if desc.ufp_capability is not None]
|
||||
assert gated
|
||||
for description in gated:
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(
|
||||
Platform.SWITCH, DOMAIN, f"{sensor_all.mac}_{description.key}"
|
||||
)
|
||||
is None
|
||||
), description.key
|
||||
|
||||
|
||||
async def test_switch_sense_no_capability_map_keeps_existing(
|
||||
async def test_switch_sense_no_capability_map_removes_existing(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
ufp: MockUFPFixture,
|
||||
sensor_all: Sensor,
|
||||
) -> None:
|
||||
"""Without a capability map (Protect below 7.2) nothing is removed.
|
||||
|
||||
The console cannot say which capabilities it lacks, so an existing entity
|
||||
must survive setup instead of being deleted on a guess.
|
||||
"""
|
||||
"""A switch created before its sensor reported no capability map is removed."""
|
||||
existing = entity_registry.async_get_or_create(
|
||||
Platform.SWITCH,
|
||||
DOMAIN,
|
||||
f"{sensor_all.mac}_motion",
|
||||
config_entry=ufp.entry,
|
||||
)
|
||||
setup_public_sensor(ufp)
|
||||
setup_public_sensor(ufp, capabilities=set())
|
||||
await init_entry(hass, ufp, [sensor_all], regenerate_ids=False)
|
||||
|
||||
assert entity_registry.async_get(existing.entity_id) is not None
|
||||
assert entity_registry.async_get(existing.entity_id) is None
|
||||
|
||||
|
||||
# The five sense settings the public API exposes, with the public-mock override
|
||||
|
||||
@@ -370,8 +370,8 @@ def make_public_sensor(
|
||||
never from real capture data. Each ``*`` override lets a test diverge from
|
||||
the private value. The mount-derived enablement properties are computed from
|
||||
the resolved mount type so a ``mount_type`` override stays consistent.
|
||||
``capabilities`` mimics the capability map of newer firmware; ``None`` (the
|
||||
default) models older firmware without a map, where every entity is created.
|
||||
``capabilities`` is the sensor's capability map; ``None`` (the default)
|
||||
advertises every capability.
|
||||
"""
|
||||
public = Mock(spec=PublicSensor)
|
||||
public.id = sensor.id
|
||||
@@ -391,12 +391,8 @@ def make_public_sensor(
|
||||
if is_tampering_detected is None
|
||||
else is_tampering_detected
|
||||
)
|
||||
public.has_feature_flags = capabilities is not None
|
||||
public.supports = Mock(
|
||||
side_effect=lambda capability: (
|
||||
capabilities is not None and capability in capabilities
|
||||
)
|
||||
)
|
||||
caps = set(SensorFeatureCapability) if capabilities is None else capabilities
|
||||
public.supports = Mock(side_effect=lambda capability: capability in caps)
|
||||
public.leak_settings = PublicSensorLeakSettings(
|
||||
is_internal_enabled=leak_internal_enabled,
|
||||
is_external_enabled=leak_external_enabled,
|
||||
@@ -715,8 +711,8 @@ def setup_public_sensor(
|
||||
|
||||
Lookups go through the real ``PublicBootstrap.get``; the mirror resolves
|
||||
against the private bootstrap at call time, so it is robust to ``init_entry``
|
||||
regenerating device ids. ``capabilities`` is forwarded to the mirror to model
|
||||
newer firmware with a capability map. Further keyword arguments are handed
|
||||
regenerating device ids. ``capabilities`` is forwarded to the mirror as the
|
||||
sensor's capability map. Further keyword arguments are handed
|
||||
to ``make_public_sensor``, so a test can diverge a mirrored value.
|
||||
"""
|
||||
public_bootstrap = PublicBootstrap()
|
||||
|
||||
Reference in New Issue
Block a user