diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index c00388d0f2c3..a7d82a38fce3 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -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 diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index b31751f26753..531b19f96404 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "loggers": ["uiprotect"], "quality_scale": "platinum", - "requirements": ["uiprotect==16.16.1"] + "requirements": ["uiprotect==17.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 02c092a77ebc..86cde6f6b334 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 58223014cb67..6ba2e7e68ae1 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -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.""" diff --git a/tests/components/unifiprotect/fixtures/sample_aiport.json b/tests/components/unifiprotect/fixtures/sample_aiport.json deleted file mode 100644 index a53c77a91692..000000000000 --- a/tests/components/unifiprotect/fixtures/sample_aiport.json +++ /dev/null @@ -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" -} diff --git a/tests/components/unifiprotect/snapshots/test_diagnostics.ambr b/tests/components/unifiprotect/snapshots/test_diagnostics.ambr index 8447f02c2318..b48b6b05e183 100644 --- a/tests/components/unifiprotect/snapshots/test_diagnostics.ambr +++ b/tests/components/unifiprotect/snapshots/test_diagnostics.ambr @@ -3,8 +3,6 @@ dict({ 'bootstrap': dict({ 'accessKey': '**REDACTED**', - 'aiports': list([ - ]), 'authUserId': '**REDACTED_ID**', 'bridges': list([ dict({ diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index a1ff0af14aa9..ca7126aa041f 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -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, diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index 027f559f43d3..7fc5132e9bc5 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -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 diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 53c080665126..d52561aaa2b2 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -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, diff --git a/tests/components/unifiprotect/test_event.py b/tests/components/unifiprotect/test_event.py index f2a87c4d41b5..855fa88b8cc0 100644 --- a/tests/components/unifiprotect/test_event.py +++ b/tests/components/unifiprotect/test_event.py @@ -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, diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index e4bee1225e93..b9dee4dc2d7c 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -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) diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 3f187c902828..9a54c7b598cf 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -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: diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index d2c69f75e8c0..0a20a79e4eaf 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -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 diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 8f16f1cca999..07966bfb2a12 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -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()