Refactor UniFi Protect tests (#73971)

Co-authored-by: J. Nick Koston <nick@koston.org>
pull/73984/head
Christopher Bailey 2022-06-25 11:15:38 -04:00 committed by GitHub
parent 85fdc56240
commit e67f8720e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1535 additions and 2294 deletions

View File

@ -3,14 +3,14 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from datetime import datetime, timedelta
from datetime import timedelta
from ipaddress import IPv4Address from ipaddress import IPv4Address
import json import json
from typing import Any from typing import Any
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
import pytest import pytest
from pyunifiprotect import ProtectApiClient
from pyunifiprotect.data import ( from pyunifiprotect.data import (
NVR, NVR,
Bootstrap, Bootstrap,
@ -19,37 +19,27 @@ from pyunifiprotect.data import (
Doorlock, Doorlock,
Light, Light,
Liveview, Liveview,
ProtectAdoptableDeviceModel,
Sensor, Sensor,
SmartDetectObjectType,
VideoMode,
Viewer, Viewer,
WSSubscriptionMessage, WSSubscriptionMessage,
) )
from pyunifiprotect.test_util.anonymize import random_hex
from homeassistant.components.unifiprotect.const import DOMAIN from homeassistant.components.unifiprotect.const import DOMAIN
from homeassistant.const import Platform from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity import EntityDescription
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from . import _patch_discovery from . import _patch_discovery
from .utils import MockUFPFixture
from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.common import MockConfigEntry, load_fixture
MAC_ADDR = "aa:bb:cc:dd:ee:ff" MAC_ADDR = "aa:bb:cc:dd:ee:ff"
@dataclass @pytest.fixture(name="nvr")
class MockEntityFixture: def mock_nvr():
"""Mock for NVR."""
entry: MockConfigEntry
api: Mock
@pytest.fixture(name="mock_nvr")
def mock_nvr_fixture():
"""Mock UniFi Protect Camera device.""" """Mock UniFi Protect Camera device."""
data = json.loads(load_fixture("sample_nvr.json", integration=DOMAIN)) data = json.loads(load_fixture("sample_nvr.json", integration=DOMAIN))
@ -63,7 +53,7 @@ def mock_nvr_fixture():
NVR.__config__.validate_assignment = True NVR.__config__.validate_assignment = True
@pytest.fixture(name="mock_ufp_config_entry") @pytest.fixture(name="ufp_config_entry")
def mock_ufp_config_entry(): def mock_ufp_config_entry():
"""Mock the unifiprotect config entry.""" """Mock the unifiprotect config entry."""
@ -81,8 +71,8 @@ def mock_ufp_config_entry():
) )
@pytest.fixture(name="mock_old_nvr") @pytest.fixture(name="old_nvr")
def mock_old_nvr_fixture(): def old_nvr():
"""Mock UniFi Protect Camera device.""" """Mock UniFi Protect Camera device."""
data = json.loads(load_fixture("sample_nvr.json", integration=DOMAIN)) data = json.loads(load_fixture("sample_nvr.json", integration=DOMAIN))
@ -90,11 +80,11 @@ def mock_old_nvr_fixture():
return NVR.from_unifi_dict(**data) return NVR.from_unifi_dict(**data)
@pytest.fixture(name="mock_bootstrap") @pytest.fixture(name="bootstrap")
def mock_bootstrap_fixture(mock_nvr: NVR): def bootstrap_fixture(nvr: NVR):
"""Mock Bootstrap fixture.""" """Mock Bootstrap fixture."""
data = json.loads(load_fixture("sample_bootstrap.json", integration=DOMAIN)) data = json.loads(load_fixture("sample_bootstrap.json", integration=DOMAIN))
data["nvr"] = mock_nvr data["nvr"] = nvr
data["cameras"] = [] data["cameras"] = []
data["lights"] = [] data["lights"] = []
data["sensors"] = [] data["sensors"] = []
@ -107,24 +97,11 @@ def mock_bootstrap_fixture(mock_nvr: NVR):
return Bootstrap.from_unifi_dict(**data) return Bootstrap.from_unifi_dict(**data)
def reset_objects(bootstrap: Bootstrap): @pytest.fixture(name="ufp_client")
"""Reset bootstrap objects.""" def mock_ufp_client(bootstrap: Bootstrap):
bootstrap.cameras = {}
bootstrap.lights = {}
bootstrap.sensors = {}
bootstrap.viewers = {}
bootstrap.liveviews = {}
bootstrap.events = {}
bootstrap.doorlocks = {}
bootstrap.chimes = {}
@pytest.fixture
def mock_client(mock_bootstrap: Bootstrap):
"""Mock ProtectApiClient for testing.""" """Mock ProtectApiClient for testing."""
client = Mock() client = Mock()
client.bootstrap = mock_bootstrap client.bootstrap = bootstrap
nvr = client.bootstrap.nvr nvr = client.bootstrap.nvr
nvr._api = client nvr._api = client
@ -133,161 +110,227 @@ def mock_client(mock_bootstrap: Bootstrap):
client.base_url = "https://127.0.0.1" client.base_url = "https://127.0.0.1"
client.connection_host = IPv4Address("127.0.0.1") client.connection_host = IPv4Address("127.0.0.1")
client.get_nvr = AsyncMock(return_value=nvr) client.get_nvr = AsyncMock(return_value=nvr)
client.update = AsyncMock(return_value=mock_bootstrap) client.update = AsyncMock(return_value=bootstrap)
client.async_disconnect_ws = AsyncMock() client.async_disconnect_ws = AsyncMock()
def subscribe(ws_callback: Callable[[WSSubscriptionMessage], None]) -> Any:
client.ws_subscription = ws_callback
return Mock()
client.subscribe_websocket = subscribe
return client return client
@pytest.fixture @pytest.fixture(name="ufp")
def mock_entry( def mock_entry(
hass: HomeAssistant, hass: HomeAssistant, ufp_config_entry: MockConfigEntry, ufp_client: ProtectApiClient
mock_ufp_config_entry: MockConfigEntry,
mock_client, # pylint: disable=redefined-outer-name
): ):
"""Mock ProtectApiClient for testing.""" """Mock ProtectApiClient for testing."""
with _patch_discovery(no_device=True), patch( with _patch_discovery(no_device=True), patch(
"homeassistant.components.unifiprotect.ProtectApiClient" "homeassistant.components.unifiprotect.ProtectApiClient"
) as mock_api: ) as mock_api:
mock_ufp_config_entry.add_to_hass(hass) ufp_config_entry.add_to_hass(hass)
mock_api.return_value = mock_client mock_api.return_value = ufp_client
yield MockEntityFixture(mock_ufp_config_entry, mock_client) ufp = MockUFPFixture(ufp_config_entry, ufp_client)
def subscribe(ws_callback: Callable[[WSSubscriptionMessage], None]) -> Any:
ufp.ws_subscription = ws_callback
return Mock()
ufp_client.subscribe_websocket = subscribe
yield ufp
@pytest.fixture @pytest.fixture
def mock_liveview(): def liveview():
"""Mock UniFi Protect Liveview.""" """Mock UniFi Protect Liveview."""
data = json.loads(load_fixture("sample_liveview.json", integration=DOMAIN)) data = json.loads(load_fixture("sample_liveview.json", integration=DOMAIN))
return Liveview.from_unifi_dict(**data) return Liveview.from_unifi_dict(**data)
@pytest.fixture @pytest.fixture(name="camera")
def mock_camera(): def camera_fixture(fixed_now: datetime):
"""Mock UniFi Protect Camera device.""" """Mock UniFi Protect Camera device."""
# disable pydantic validation so mocking can happen
Camera.__config__.validate_assignment = False
data = json.loads(load_fixture("sample_camera.json", integration=DOMAIN)) data = json.loads(load_fixture("sample_camera.json", integration=DOMAIN))
return Camera.from_unifi_dict(**data) camera = Camera.from_unifi_dict(**data)
camera.last_motion = fixed_now - timedelta(hours=1)
yield camera
Camera.__config__.validate_assignment = True
@pytest.fixture(name="camera_all")
def camera_all_fixture(camera: Camera):
"""Mock UniFi Protect Camera device."""
all_camera = camera.copy()
all_camera.channels = [all_camera.channels[0].copy()]
medium_channel = all_camera.channels[0].copy()
medium_channel.name = "Medium"
medium_channel.id = 1
medium_channel.rtsp_alias = "test_medium_alias"
all_camera.channels.append(medium_channel)
low_channel = all_camera.channels[0].copy()
low_channel.name = "Low"
low_channel.id = 2
low_channel.rtsp_alias = "test_medium_alias"
all_camera.channels.append(low_channel)
return all_camera
@pytest.fixture(name="doorbell")
def doorbell_fixture(camera: Camera, fixed_now: datetime):
"""Mock UniFi Protect Camera device (with chime)."""
doorbell = camera.copy()
doorbell.channels = [c.copy() for c in doorbell.channels]
package_channel = doorbell.channels[0].copy()
package_channel.name = "Package Camera"
package_channel.id = 3
package_channel.fps = 2
package_channel.rtsp_alias = "test_package_alias"
doorbell.channels.append(package_channel)
doorbell.feature_flags.video_modes = [VideoMode.DEFAULT, VideoMode.HIGH_FPS]
doorbell.feature_flags.smart_detect_types = [
SmartDetectObjectType.PERSON,
SmartDetectObjectType.VEHICLE,
]
doorbell.feature_flags.has_hdr = True
doorbell.feature_flags.has_lcd_screen = True
doorbell.feature_flags.has_speaker = True
doorbell.feature_flags.has_privacy_mask = True
doorbell.feature_flags.has_chime = True
doorbell.feature_flags.has_smart_detect = True
doorbell.feature_flags.has_package_camera = True
doorbell.feature_flags.has_led_status = True
doorbell.last_ring = fixed_now - timedelta(hours=1)
return doorbell
@pytest.fixture @pytest.fixture
def mock_light(): def unadopted_camera(camera: Camera):
"""Mock UniFi Protect Camera device (unadopted)."""
no_camera = camera.copy()
no_camera.channels = [c.copy() for c in no_camera.channels]
no_camera.name = "Unadopted Camera"
no_camera.is_adopted = False
return no_camera
@pytest.fixture(name="light")
def light_fixture():
"""Mock UniFi Protect Light device.""" """Mock UniFi Protect Light device."""
# disable pydantic validation so mocking can happen
Light.__config__.validate_assignment = False
data = json.loads(load_fixture("sample_light.json", integration=DOMAIN)) data = json.loads(load_fixture("sample_light.json", integration=DOMAIN))
return Light.from_unifi_dict(**data) yield Light.from_unifi_dict(**data)
Light.__config__.validate_assignment = True
@pytest.fixture @pytest.fixture
def mock_viewer(): def unadopted_light(light: Light):
"""Mock UniFi Protect Light device (unadopted)."""
no_light = light.copy()
no_light.name = "Unadopted Light"
no_light.is_adopted = False
return no_light
@pytest.fixture
def viewer():
"""Mock UniFi Protect Viewport device.""" """Mock UniFi Protect Viewport device."""
# disable pydantic validation so mocking can happen
Viewer.__config__.validate_assignment = False
data = json.loads(load_fixture("sample_viewport.json", integration=DOMAIN)) data = json.loads(load_fixture("sample_viewport.json", integration=DOMAIN))
return Viewer.from_unifi_dict(**data) yield Viewer.from_unifi_dict(**data)
Viewer.__config__.validate_assignment = True
@pytest.fixture @pytest.fixture(name="sensor")
def mock_sensor(): def sensor_fixture(fixed_now: datetime):
"""Mock UniFi Protect Sensor device.""" """Mock UniFi Protect Sensor device."""
# disable pydantic validation so mocking can happen
Sensor.__config__.validate_assignment = False
data = json.loads(load_fixture("sample_sensor.json", integration=DOMAIN)) data = json.loads(load_fixture("sample_sensor.json", integration=DOMAIN))
return Sensor.from_unifi_dict(**data) sensor: Sensor = Sensor.from_unifi_dict(**data)
sensor.motion_detected_at = fixed_now - timedelta(hours=1)
sensor.open_status_changed_at = fixed_now - timedelta(hours=1)
sensor.alarm_triggered_at = fixed_now - timedelta(hours=1)
yield sensor
Sensor.__config__.validate_assignment = True
@pytest.fixture @pytest.fixture(name="sensor_all")
def mock_doorlock(): def csensor_all_fixture(sensor: Sensor):
"""Mock UniFi Protect Sensor device."""
all_sensor = sensor.copy()
all_sensor.light_settings.is_enabled = True
all_sensor.humidity_settings.is_enabled = True
all_sensor.temperature_settings.is_enabled = True
all_sensor.alarm_settings.is_enabled = True
all_sensor.led_settings.is_enabled = True
all_sensor.motion_settings.is_enabled = True
return all_sensor
@pytest.fixture(name="doorlock")
def doorlock_fixture():
"""Mock UniFi Protect Doorlock device.""" """Mock UniFi Protect Doorlock device."""
# disable pydantic validation so mocking can happen
Doorlock.__config__.validate_assignment = False
data = json.loads(load_fixture("sample_doorlock.json", integration=DOMAIN)) data = json.loads(load_fixture("sample_doorlock.json", integration=DOMAIN))
return Doorlock.from_unifi_dict(**data) yield Doorlock.from_unifi_dict(**data)
Doorlock.__config__.validate_assignment = True
@pytest.fixture @pytest.fixture
def mock_chime(): def unadopted_doorlock(doorlock: Doorlock):
"""Mock UniFi Protect Light device (unadopted)."""
no_doorlock = doorlock.copy()
no_doorlock.name = "Unadopted Lock"
no_doorlock.is_adopted = False
return no_doorlock
@pytest.fixture
def chime():
"""Mock UniFi Protect Chime device.""" """Mock UniFi Protect Chime device."""
# disable pydantic validation so mocking can happen
Chime.__config__.validate_assignment = False
data = json.loads(load_fixture("sample_chime.json", integration=DOMAIN)) data = json.loads(load_fixture("sample_chime.json", integration=DOMAIN))
return Chime.from_unifi_dict(**data) yield Chime.from_unifi_dict(**data)
Chime.__config__.validate_assignment = True
@pytest.fixture @pytest.fixture(name="fixed_now")
def now(): def fixed_now_fixture():
"""Return datetime object that will be consistent throughout test.""" """Return datetime object that will be consistent throughout test."""
return dt_util.utcnow() return dt_util.utcnow()
async def time_changed(hass: HomeAssistant, seconds: int) -> None:
"""Trigger time changed."""
next_update = dt_util.utcnow() + timedelta(seconds)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
async def enable_entity(
hass: HomeAssistant, entry_id: str, entity_id: str
) -> er.RegistryEntry:
"""Enable a disabled entity."""
entity_registry = er.async_get(hass)
updated_entity = entity_registry.async_update_entity(entity_id, disabled_by=None)
assert not updated_entity.disabled
await hass.config_entries.async_reload(entry_id)
await hass.async_block_till_done()
return updated_entity
def assert_entity_counts(
hass: HomeAssistant, platform: Platform, total: int, enabled: int
) -> None:
"""Assert entity counts for a given platform."""
entity_registry = er.async_get(hass)
entities = [
e for e in entity_registry.entities if split_entity_id(e)[0] == platform.value
]
assert len(entities) == total
assert len(hass.states.async_all(platform.value)) == enabled
def ids_from_device_description(
platform: Platform,
device: ProtectAdoptableDeviceModel,
description: EntityDescription,
) -> tuple[str, str]:
"""Return expected unique_id and entity_id for a give platform/device/description combination."""
entity_name = (
device.name.lower().replace(":", "").replace(" ", "_").replace("-", "_")
)
description_entity_name = (
description.name.lower().replace(":", "").replace(" ", "_").replace("-", "_")
)
unique_id = f"{device.mac}_{description.key}"
entity_id = f"{platform.value}.{entity_name}_{description_entity_name}"
return unique_id, entity_id
def generate_random_ids() -> tuple[str, str]:
"""Generate random IDs for device."""
return random_hex(24).lower(), random_hex(12).upper()
def regenerate_device_ids(device: ProtectAdoptableDeviceModel) -> None:
"""Regenerate the IDs on UFP device."""
device.id, device.mac = generate_random_ids()

View File

@ -4,7 +4,7 @@
"host": "192.168.6.90", "host": "192.168.6.90",
"connectionHost": "192.168.178.217", "connectionHost": "192.168.178.217",
"type": "UVC G4 Instant", "type": "UVC G4 Instant",
"name": "Fufail Qqjx", "name": "Test Camera",
"upSince": 1640020678036, "upSince": 1640020678036,
"uptime": 3203, "uptime": 3203,
"lastSeen": 1640023881036, "lastSeen": 1640023881036,
@ -20,18 +20,18 @@
"isAdoptedByOther": false, "isAdoptedByOther": false,
"isProvisioned": true, "isProvisioned": true,
"isRebooting": false, "isRebooting": false,
"isSshEnabled": true, "isSshEnabled": false,
"canAdopt": false, "canAdopt": false,
"isAttemptingToConnect": false, "isAttemptingToConnect": false,
"lastMotion": 1640021213927, "lastMotion": 1640021213927,
"micVolume": 100, "micVolume": 0,
"isMicEnabled": true, "isMicEnabled": true,
"isRecording": false, "isRecording": false,
"isWirelessUplinkEnabled": true, "isWirelessUplinkEnabled": true,
"isMotionDetected": false, "isMotionDetected": false,
"isSmartDetected": false, "isSmartDetected": false,
"phyRate": 72, "phyRate": 72,
"hdrMode": true, "hdrMode": false,
"videoMode": "default", "videoMode": "default",
"isProbingForWifi": false, "isProbingForWifi": false,
"apMac": null, "apMac": null,
@ -57,18 +57,18 @@
} }
}, },
"videoReconfigurationInProgress": false, "videoReconfigurationInProgress": false,
"voltage": null, "voltage": 20.0,
"wiredConnectionState": { "wiredConnectionState": {
"phyRate": null "phyRate": 1000
}, },
"channels": [ "channels": [
{ {
"id": 0, "id": 0,
"videoId": "video1", "videoId": "video1",
"name": "Jzi Bftu", "name": "High",
"enabled": true, "enabled": true,
"isRtspEnabled": true, "isRtspEnabled": true,
"rtspAlias": "ANOAPfoKMW7VixG1", "rtspAlias": "test_high_alias",
"width": 2688, "width": 2688,
"height": 1512, "height": 1512,
"fps": 30, "fps": 30,
@ -83,10 +83,10 @@
{ {
"id": 1, "id": 1,
"videoId": "video2", "videoId": "video2",
"name": "Rgcpxsf Xfwt", "name": "Medium",
"enabled": true, "enabled": true,
"isRtspEnabled": true, "isRtspEnabled": false,
"rtspAlias": "XHXAdHVKGVEzMNTP", "rtspAlias": null,
"width": 1280, "width": 1280,
"height": 720, "height": 720,
"fps": 30, "fps": 30,
@ -101,7 +101,7 @@
{ {
"id": 2, "id": 2,
"videoId": "video3", "videoId": "video3",
"name": "Umefvk Fug", "name": "Low",
"enabled": true, "enabled": true,
"isRtspEnabled": false, "isRtspEnabled": false,
"rtspAlias": null, "rtspAlias": null,
@ -121,7 +121,7 @@
"aeMode": "auto", "aeMode": "auto",
"irLedMode": "auto", "irLedMode": "auto",
"irLedLevel": 255, "irLedLevel": 255,
"wdr": 1, "wdr": 0,
"icrSensitivity": 0, "icrSensitivity": 0,
"brightness": 50, "brightness": 50,
"contrast": 50, "contrast": 50,
@ -161,8 +161,8 @@
"quality": 100 "quality": 100
}, },
"osdSettings": { "osdSettings": {
"isNameEnabled": true, "isNameEnabled": false,
"isDateEnabled": true, "isDateEnabled": false,
"isLogoEnabled": false, "isLogoEnabled": false,
"isDebugEnabled": false "isDebugEnabled": false
}, },
@ -181,7 +181,7 @@
"minMotionEventTrigger": 1000, "minMotionEventTrigger": 1000,
"endMotionEventDelay": 3000, "endMotionEventDelay": 3000,
"suppressIlluminationSurge": false, "suppressIlluminationSurge": false,
"mode": "detections", "mode": "always",
"geofencing": "off", "geofencing": "off",
"motionAlgorithm": "enhanced", "motionAlgorithm": "enhanced",
"enablePirTimelapse": false, "enablePirTimelapse": false,
@ -223,8 +223,8 @@
], ],
"smartDetectLines": [], "smartDetectLines": [],
"stats": { "stats": {
"rxBytes": 33684237, "rxBytes": 100,
"txBytes": 1208318620, "txBytes": 100,
"wifi": { "wifi": {
"channel": 6, "channel": 6,
"frequency": 2437, "frequency": 2437,
@ -248,8 +248,8 @@
"timelapseEndLQ": 1640021765237 "timelapseEndLQ": 1640021765237
}, },
"storage": { "storage": {
"used": 20401094656, "used": 100,
"rate": 693.424269097809 "rate": 0.1
}, },
"wifiQuality": 100, "wifiQuality": 100,
"wifiStrength": -35 "wifiStrength": -35
@ -257,7 +257,7 @@
"featureFlags": { "featureFlags": {
"canAdjustIrLedLevel": false, "canAdjustIrLedLevel": false,
"canMagicZoom": false, "canMagicZoom": false,
"canOpticalZoom": false, "canOpticalZoom": true,
"canTouchFocus": false, "canTouchFocus": false,
"hasAccelerometer": true, "hasAccelerometer": true,
"hasAec": true, "hasAec": true,
@ -268,15 +268,15 @@
"hasIcrSensitivity": true, "hasIcrSensitivity": true,
"hasLdc": false, "hasLdc": false,
"hasLedIr": true, "hasLedIr": true,
"hasLedStatus": true, "hasLedStatus": false,
"hasLineIn": false, "hasLineIn": false,
"hasMic": true, "hasMic": true,
"hasPrivacyMask": true, "hasPrivacyMask": false,
"hasRtc": false, "hasRtc": false,
"hasSdCard": false, "hasSdCard": false,
"hasSpeaker": true, "hasSpeaker": false,
"hasWifi": true, "hasWifi": true,
"hasHdr": true, "hasHdr": false,
"hasAutoICROnly": true, "hasAutoICROnly": true,
"videoModes": ["default"], "videoModes": ["default"],
"videoModeMaxFps": [], "videoModeMaxFps": [],
@ -353,14 +353,14 @@
"frequency": 2437, "frequency": 2437,
"phyRate": 72, "phyRate": 72,
"signalQuality": 100, "signalQuality": 100,
"signalStrength": -35, "signalStrength": -50,
"ssid": "Mortis Camera" "ssid": "Mortis Camera"
}, },
"lenses": [], "lenses": [],
"id": "0de062b4f6922d489d3b312d", "id": "0de062b4f6922d489d3b312d",
"isConnected": true, "isConnected": true,
"platform": "sav530q", "platform": "sav530q",
"hasSpeaker": true, "hasSpeaker": false,
"hasWifi": true, "hasWifi": true,
"audioBitrate": 64000, "audioBitrate": 64000,
"canManage": false, "canManage": false,

View File

@ -3,7 +3,7 @@
"host": "192.168.144.146", "host": "192.168.144.146",
"connectionHost": "192.168.234.27", "connectionHost": "192.168.234.27",
"type": "UP Chime", "type": "UP Chime",
"name": "Xaorvu Tvsv", "name": "Test Chime",
"upSince": 1651882870009, "upSince": 1651882870009,
"uptime": 567870, "uptime": 567870,
"lastSeen": 1652450740009, "lastSeen": 1652450740009,

View File

@ -3,7 +3,7 @@
"host": null, "host": null,
"connectionHost": "192.168.102.63", "connectionHost": "192.168.102.63",
"type": "UFP-LOCK-R", "type": "UFP-LOCK-R",
"name": "Wkltg Qcjxv", "name": "Test Lock",
"upSince": 1643050461849, "upSince": 1643050461849,
"uptime": null, "uptime": null,
"lastSeen": 1643052750858, "lastSeen": 1643052750858,
@ -23,9 +23,9 @@
"canAdopt": false, "canAdopt": false,
"isAttemptingToConnect": false, "isAttemptingToConnect": false,
"credentials": "955756200c7f43936df9d5f7865f058e1528945aac0f0cb27cef960eb58f17db", "credentials": "955756200c7f43936df9d5f7865f058e1528945aac0f0cb27cef960eb58f17db",
"lockStatus": "CLOSING", "lockStatus": "OPEN",
"enableHomekit": false, "enableHomekit": false,
"autoCloseTimeMs": 15000, "autoCloseTimeMs": 45000,
"wiredConnectionState": { "wiredConnectionState": {
"phyRate": null "phyRate": null
}, },

View File

@ -3,7 +3,7 @@
"host": "192.168.10.86", "host": "192.168.10.86",
"connectionHost": "192.168.178.217", "connectionHost": "192.168.178.217",
"type": "UP FloodLight", "type": "UP FloodLight",
"name": "Byyfbpe Ufoka", "name": "Test Light",
"upSince": 1638128991022, "upSince": 1638128991022,
"uptime": 1894890, "uptime": 1894890,
"lastSeen": 1640023881022, "lastSeen": 1640023881022,
@ -19,7 +19,7 @@
"isAdoptedByOther": false, "isAdoptedByOther": false,
"isProvisioned": false, "isProvisioned": false,
"isRebooting": false, "isRebooting": false,
"isSshEnabled": true, "isSshEnabled": false,
"canAdopt": false, "canAdopt": false,
"isAttemptingToConnect": false, "isAttemptingToConnect": false,
"isPirMotionDetected": false, "isPirMotionDetected": false,
@ -31,20 +31,20 @@
"phyRate": 100 "phyRate": 100
}, },
"lightDeviceSettings": { "lightDeviceSettings": {
"isIndicatorEnabled": true, "isIndicatorEnabled": false,
"ledLevel": 6, "ledLevel": 6,
"luxSensitivity": "medium", "luxSensitivity": "medium",
"pirDuration": 120000, "pirDuration": 45000,
"pirSensitivity": 46 "pirSensitivity": 45
}, },
"lightOnSettings": { "lightOnSettings": {
"isLedForceOn": false "isLedForceOn": false
}, },
"lightModeSettings": { "lightModeSettings": {
"mode": "off", "mode": "motion",
"enableAt": "fulltime" "enableAt": "fulltime"
}, },
"camera": "193be66559c03ec5629f54cd", "camera": null,
"id": "37dd610720816cfb5c547967", "id": "37dd610720816cfb5c547967",
"isConnected": true, "isConnected": true,
"isCameraPaired": true, "isCameraPaired": true,

View File

@ -2,7 +2,7 @@
"mac": "26DBAFF133A4", "mac": "26DBAFF133A4",
"connectionHost": "192.168.216.198", "connectionHost": "192.168.216.198",
"type": "UFP-SENSE", "type": "UFP-SENSE",
"name": "Egdczv Urg", "name": "Test Sensor",
"upSince": 1641256963255, "upSince": 1641256963255,
"uptime": null, "uptime": null,
"lastSeen": 1641259127934, "lastSeen": 1641259127934,
@ -25,7 +25,7 @@
"mountType": "door", "mountType": "door",
"leakDetectedAt": null, "leakDetectedAt": null,
"tamperingDetectedAt": null, "tamperingDetectedAt": null,
"isOpened": true, "isOpened": false,
"openStatusChangedAt": 1641269036582, "openStatusChangedAt": 1641269036582,
"alarmTriggeredAt": null, "alarmTriggeredAt": null,
"motionDetectedAt": 1641269044824, "motionDetectedAt": 1641269044824,
@ -34,53 +34,53 @@
}, },
"stats": { "stats": {
"light": { "light": {
"value": 0, "value": 10.0,
"status": "neutral" "status": "neutral"
}, },
"humidity": { "humidity": {
"value": 35, "value": 10.0,
"status": "neutral" "status": "neutral"
}, },
"temperature": { "temperature": {
"value": 17.23, "value": 10.0,
"status": "neutral" "status": "neutral"
} }
}, },
"bluetoothConnectionState": { "bluetoothConnectionState": {
"signalQuality": 15, "signalQuality": 15,
"signalStrength": -84 "signalStrength": -50
}, },
"batteryStatus": { "batteryStatus": {
"percentage": 100, "percentage": 10,
"isLow": false "isLow": false
}, },
"alarmSettings": { "alarmSettings": {
"isEnabled": false "isEnabled": false
}, },
"lightSettings": { "lightSettings": {
"isEnabled": true, "isEnabled": false,
"lowThreshold": null, "lowThreshold": null,
"highThreshold": null, "highThreshold": null,
"margin": 10 "margin": 10
}, },
"motionSettings": { "motionSettings": {
"isEnabled": true, "isEnabled": false,
"sensitivity": 100 "sensitivity": 100
}, },
"temperatureSettings": { "temperatureSettings": {
"isEnabled": true, "isEnabled": false,
"lowThreshold": null, "lowThreshold": null,
"highThreshold": null, "highThreshold": null,
"margin": 0.1 "margin": 0.1
}, },
"humiditySettings": { "humiditySettings": {
"isEnabled": true, "isEnabled": false,
"lowThreshold": null, "lowThreshold": null,
"highThreshold": null, "highThreshold": null,
"margin": 1 "margin": 1
}, },
"ledSettings": { "ledSettings": {
"isEnabled": true "isEnabled": false
}, },
"bridge": "61b3f5c90050a703e700042a", "bridge": "61b3f5c90050a703e700042a",
"camera": "2f9beb2e6f79af3c32c22d49", "camera": "2f9beb2e6f79af3c32c22d49",

View File

@ -2,11 +2,9 @@
# pylint: disable=protected-access # pylint: disable=protected-access
from __future__ import annotations from __future__ import annotations
from copy import copy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import Mock from unittest.mock import Mock
import pytest
from pyunifiprotect.data import Camera, Event, EventType, Light, MountType, Sensor from pyunifiprotect.data import Camera, Event, EventType, Light, MountType, Sensor
from pyunifiprotect.data.nvr import EventMetadata from pyunifiprotect.data.nvr import EventMetadata
@ -32,218 +30,25 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .conftest import ( from .utils import (
MockEntityFixture, MockUFPFixture,
assert_entity_counts, assert_entity_counts,
ids_from_device_description, ids_from_device_description,
regenerate_device_ids, init_entry,
reset_objects,
) )
LIGHT_SENSOR_WRITE = LIGHT_SENSORS[:2] LIGHT_SENSOR_WRITE = LIGHT_SENSORS[:2]
SENSE_SENSORS_WRITE = SENSE_SENSORS[:4] SENSE_SENSORS_WRITE = SENSE_SENSORS[:4]
@pytest.fixture(name="camera")
async def camera_fixture(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
mock_camera: Camera,
now: datetime,
):
"""Fixture for a single camera for testing the binary_sensor platform."""
# disable pydantic validation so mocking can happen
Camera.__config__.validate_assignment = False
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.feature_flags.has_chime = True
camera_obj.last_ring = now - timedelta(hours=1)
camera_obj.is_dark = False
camera_obj.is_motion_detected = False
regenerate_device_ids(camera_obj)
no_camera_obj = mock_camera.copy()
no_camera_obj._api = mock_entry.api
no_camera_obj.channels[0]._api = mock_entry.api
no_camera_obj.channels[1]._api = mock_entry.api
no_camera_obj.channels[2]._api = mock_entry.api
no_camera_obj.name = "Unadopted Camera"
no_camera_obj.is_adopted = False
regenerate_device_ids(no_camera_obj)
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
no_camera_obj.id: no_camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 9)
yield camera_obj
Camera.__config__.validate_assignment = True
@pytest.fixture(name="light")
async def light_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light, now: datetime
):
"""Fixture for a single light for testing the binary_sensor platform."""
# disable pydantic validation so mocking can happen
Light.__config__.validate_assignment = False
light_obj = mock_light.copy()
light_obj._api = mock_entry.api
light_obj.name = "Test Light"
light_obj.is_dark = False
light_obj.is_pir_motion_detected = False
light_obj.last_motion = now - timedelta(hours=1)
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
mock_entry.api.bootstrap.lights = {
light_obj.id: light_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8)
yield light_obj
Light.__config__.validate_assignment = True
@pytest.fixture(name="camera_none")
async def camera_none_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera
):
"""Fixture for a single camera for testing the binary_sensor platform."""
# disable pydantic validation so mocking can happen
Camera.__config__.validate_assignment = False
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.feature_flags.has_chime = False
camera_obj.is_dark = False
camera_obj.is_motion_detected = False
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
mock_entry.api.bootstrap.nvr.system_info.ustorage = None
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2)
yield camera_obj
Camera.__config__.validate_assignment = True
@pytest.fixture(name="sensor")
async def sensor_fixture(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
mock_sensor: Sensor,
now: datetime,
):
"""Fixture for a single sensor for testing the binary_sensor platform."""
# disable pydantic validation so mocking can happen
Sensor.__config__.validate_assignment = False
sensor_obj = mock_sensor.copy()
sensor_obj._api = mock_entry.api
sensor_obj.name = "Test Sensor"
sensor_obj.mount_type = MountType.DOOR
sensor_obj.is_opened = False
sensor_obj.battery_status.is_low = False
sensor_obj.is_motion_detected = False
sensor_obj.alarm_settings.is_enabled = True
sensor_obj.motion_detected_at = now - timedelta(hours=1)
sensor_obj.open_status_changed_at = now - timedelta(hours=1)
sensor_obj.alarm_triggered_at = now - timedelta(hours=1)
sensor_obj.tampering_detected_at = None
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
mock_entry.api.bootstrap.sensors = {
sensor_obj.id: sensor_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10)
yield sensor_obj
Sensor.__config__.validate_assignment = True
@pytest.fixture(name="sensor_none")
async def sensor_none_fixture(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
mock_sensor: Sensor,
now: datetime,
):
"""Fixture for a single sensor for testing the binary_sensor platform."""
# disable pydantic validation so mocking can happen
Sensor.__config__.validate_assignment = False
sensor_obj = mock_sensor.copy()
sensor_obj._api = mock_entry.api
sensor_obj.name = "Test Sensor"
sensor_obj.mount_type = MountType.LEAK
sensor_obj.battery_status.is_low = False
sensor_obj.alarm_settings.is_enabled = False
sensor_obj.tampering_detected_at = None
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
mock_entry.api.bootstrap.sensors = {
sensor_obj.id: sensor_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10)
yield sensor_obj
Sensor.__config__.validate_assignment = True
async def test_binary_sensor_setup_light( async def test_binary_sensor_setup_light(
hass: HomeAssistant, light: Light, now: datetime hass: HomeAssistant, ufp: MockUFPFixture, light: Light
): ):
"""Test binary_sensor entity setup for light devices.""" """Test binary_sensor entity setup for light devices."""
await init_entry(hass, ufp, [light])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
for description in LIGHT_SENSOR_WRITE: for description in LIGHT_SENSOR_WRITE:
@ -262,15 +67,22 @@ async def test_binary_sensor_setup_light(
async def test_binary_sensor_setup_camera_all( async def test_binary_sensor_setup_camera_all(
hass: HomeAssistant, camera: Camera, now: datetime hass: HomeAssistant,
ufp: MockUFPFixture,
doorbell: Camera,
unadopted_camera: Camera,
): ):
"""Test binary_sensor entity setup for camera devices (all features).""" """Test binary_sensor entity setup for camera devices (all features)."""
ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
description = CAMERA_SENSORS[0] description = CAMERA_SENSORS[0]
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, camera, description Platform.BINARY_SENSOR, doorbell, description
) )
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -285,7 +97,7 @@ async def test_binary_sensor_setup_camera_all(
# Is Dark # Is Dark
description = CAMERA_SENSORS[1] description = CAMERA_SENSORS[1]
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, camera, description Platform.BINARY_SENSOR, doorbell, description
) )
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -300,7 +112,7 @@ async def test_binary_sensor_setup_camera_all(
# Motion # Motion
description = MOTION_SENSORS[0] description = MOTION_SENSORS[0]
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, camera, description Platform.BINARY_SENSOR, doorbell, description
) )
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -315,16 +127,19 @@ async def test_binary_sensor_setup_camera_all(
async def test_binary_sensor_setup_camera_none( async def test_binary_sensor_setup_camera_none(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera
camera_none: Camera,
): ):
"""Test binary_sensor entity setup for camera devices (no features).""" """Test binary_sensor entity setup for camera devices (no features)."""
ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
description = CAMERA_SENSORS[1] description = CAMERA_SENSORS[1]
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, camera_none, description Platform.BINARY_SENSOR, camera, description
) )
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -338,15 +153,18 @@ async def test_binary_sensor_setup_camera_none(
async def test_binary_sensor_setup_sensor( async def test_binary_sensor_setup_sensor(
hass: HomeAssistant, sensor: Sensor, now: datetime hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor
): ):
"""Test binary_sensor entity setup for sensor devices.""" """Test binary_sensor entity setup for sensor devices."""
await init_entry(hass, ufp, [sensor_all])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
for description in SENSE_SENSORS_WRITE: for description in SENSE_SENSORS_WRITE:
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, sensor, description Platform.BINARY_SENSOR, sensor_all, description
) )
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -360,10 +178,14 @@ async def test_binary_sensor_setup_sensor(
async def test_binary_sensor_setup_sensor_none( async def test_binary_sensor_setup_sensor_none(
hass: HomeAssistant, sensor_none: Sensor hass: HomeAssistant, ufp: MockUFPFixture, sensor: Sensor
): ):
"""Test binary_sensor entity setup for sensor with most sensors disabled.""" """Test binary_sensor entity setup for sensor with most sensors disabled."""
sensor.mount_type = MountType.LEAK
await init_entry(hass, ufp, [sensor])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
expected = [ expected = [
@ -374,7 +196,7 @@ async def test_binary_sensor_setup_sensor_none(
] ]
for index, description in enumerate(SENSE_SENSORS_WRITE): for index, description in enumerate(SENSE_SENSORS_WRITE):
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, sensor_none, description Platform.BINARY_SENSOR, sensor, description
) )
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -388,27 +210,33 @@ async def test_binary_sensor_setup_sensor_none(
async def test_binary_sensor_update_motion( async def test_binary_sensor_update_motion(
hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime hass: HomeAssistant,
ufp: MockUFPFixture,
doorbell: Camera,
unadopted_camera: Camera,
fixed_now: datetime,
): ):
"""Test binary_sensor motion entity.""" """Test binary_sensor motion entity."""
await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 9)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, camera, MOTION_SENSORS[0] Platform.BINARY_SENSOR, doorbell, MOTION_SENSORS[0]
) )
event = Event( event = Event(
id="test_event_id", id="test_event_id",
type=EventType.MOTION, type=EventType.MOTION,
start=now - timedelta(seconds=1), start=fixed_now - timedelta(seconds=1),
end=None, end=None,
score=100, score=100,
smart_detect_types=[], smart_detect_types=[],
smart_detect_event_ids=[], smart_detect_event_ids=[],
camera_id=camera.id, camera_id=doorbell.id,
) )
new_bootstrap = copy(mock_entry.api.bootstrap) new_camera = doorbell.copy()
new_camera = camera.copy()
new_camera.is_motion_detected = True new_camera.is_motion_detected = True
new_camera.last_motion_event_id = event.id new_camera.last_motion_event_id = event.id
@ -416,10 +244,9 @@ async def test_binary_sensor_update_motion(
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_camera mock_msg.new_obj = new_camera
new_bootstrap.cameras = {new_camera.id: new_camera} ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
new_bootstrap.events = {event.id: event} ufp.api.bootstrap.events = {event.id: event}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
@ -430,10 +257,13 @@ async def test_binary_sensor_update_motion(
async def test_binary_sensor_update_light_motion( async def test_binary_sensor_update_light_motion(
hass: HomeAssistant, mock_entry: MockEntityFixture, light: Light, now: datetime hass: HomeAssistant, ufp: MockUFPFixture, light: Light, fixed_now: datetime
): ):
"""Test binary_sensor motion entity.""" """Test binary_sensor motion entity."""
await init_entry(hass, ufp, [light])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, light, LIGHT_SENSOR_WRITE[1] Platform.BINARY_SENSOR, light, LIGHT_SENSOR_WRITE[1]
) )
@ -442,16 +272,15 @@ async def test_binary_sensor_update_light_motion(
event = Event( event = Event(
id="test_event_id", id="test_event_id",
type=EventType.MOTION_LIGHT, type=EventType.MOTION_LIGHT,
start=now - timedelta(seconds=1), start=fixed_now - timedelta(seconds=1),
end=None, end=None,
score=100, score=100,
smart_detect_types=[], smart_detect_types=[],
smart_detect_event_ids=[], smart_detect_event_ids=[],
metadata=event_metadata, metadata=event_metadata,
api=mock_entry.api, api=ufp.api,
) )
new_bootstrap = copy(mock_entry.api.bootstrap)
new_light = light.copy() new_light = light.copy()
new_light.is_pir_motion_detected = True new_light.is_pir_motion_detected = True
new_light.last_motion_event_id = event.id new_light.last_motion_event_id = event.id
@ -460,10 +289,9 @@ async def test_binary_sensor_update_light_motion(
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = event mock_msg.new_obj = event
new_bootstrap.lights = {new_light.id: new_light} ufp.api.bootstrap.lights = {new_light.id: new_light}
new_bootstrap.events = {event.id: event} ufp.api.bootstrap.events = {event.id: event}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
@ -472,29 +300,30 @@ async def test_binary_sensor_update_light_motion(
async def test_binary_sensor_update_mount_type_window( async def test_binary_sensor_update_mount_type_window(
hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor
): ):
"""Test binary_sensor motion entity.""" """Test binary_sensor motion entity."""
await init_entry(hass, ufp, [sensor_all])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, sensor, SENSE_SENSORS_WRITE[0] Platform.BINARY_SENSOR, sensor_all, SENSE_SENSORS_WRITE[0]
) )
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR.value assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR.value
new_bootstrap = copy(mock_entry.api.bootstrap) new_sensor = sensor_all.copy()
new_sensor = sensor.copy()
new_sensor.mount_type = MountType.WINDOW new_sensor.mount_type = MountType.WINDOW
mock_msg = Mock() mock_msg = Mock()
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_sensor mock_msg.new_obj = new_sensor
new_bootstrap.sensors = {new_sensor.id: new_sensor} ufp.api.bootstrap.sensors = {new_sensor.id: new_sensor}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
@ -503,29 +332,30 @@ async def test_binary_sensor_update_mount_type_window(
async def test_binary_sensor_update_mount_type_garage( async def test_binary_sensor_update_mount_type_garage(
hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor
): ):
"""Test binary_sensor motion entity.""" """Test binary_sensor motion entity."""
await init_entry(hass, ufp, [sensor_all])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, sensor, SENSE_SENSORS_WRITE[0] Platform.BINARY_SENSOR, sensor_all, SENSE_SENSORS_WRITE[0]
) )
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR.value assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR.value
new_bootstrap = copy(mock_entry.api.bootstrap) new_sensor = sensor_all.copy()
new_sensor = sensor.copy()
new_sensor.mount_type = MountType.GARAGE new_sensor.mount_type = MountType.GARAGE
mock_msg = Mock() mock_msg = Mock()
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_sensor mock_msg.new_obj = new_sensor
new_bootstrap.sensors = {new_sensor.id: new_sensor} ufp.api.bootstrap.sensors = {new_sensor.id: new_sensor}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)

View File

@ -4,7 +4,6 @@ from __future__ import annotations
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
import pytest
from pyunifiprotect.data.devices import Chime from pyunifiprotect.data.devices import Chime
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
@ -12,39 +11,20 @@ from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .conftest import MockEntityFixture, assert_entity_counts, enable_entity from .utils import MockUFPFixture, assert_entity_counts, enable_entity, init_entry
@pytest.fixture(name="chime")
async def chime_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_chime: Chime
):
"""Fixture for a single camera for testing the button platform."""
chime_obj = mock_chime.copy()
chime_obj._api = mock_entry.api
chime_obj.name = "Test Chime"
mock_entry.api.bootstrap.chimes = {
chime_obj.id: chime_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.BUTTON, 3, 2)
return chime_obj
async def test_reboot_button( async def test_reboot_button(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
chime: Chime, chime: Chime,
): ):
"""Test button entity.""" """Test button entity."""
mock_entry.api.reboot_device = AsyncMock() await init_entry(hass, ufp, [chime])
assert_entity_counts(hass, Platform.BUTTON, 3, 2)
ufp.api.reboot_device = AsyncMock()
unique_id = f"{chime.mac}_reboot" unique_id = f"{chime.mac}_reboot"
entity_id = "button.test_chime_reboot_device" entity_id = "button.test_chime_reboot_device"
@ -55,7 +35,7 @@ async def test_reboot_button(
assert entity.disabled assert entity.disabled
assert entity.unique_id == unique_id assert entity.unique_id == unique_id
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
@ -63,17 +43,20 @@ async def test_reboot_button(
await hass.services.async_call( await hass.services.async_call(
"button", "press", {ATTR_ENTITY_ID: entity_id}, blocking=True "button", "press", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
mock_entry.api.reboot_device.assert_called_once() ufp.api.reboot_device.assert_called_once()
async def test_chime_button( async def test_chime_button(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
chime: Chime, chime: Chime,
): ):
"""Test button entity.""" """Test button entity."""
mock_entry.api.play_speaker = AsyncMock() await init_entry(hass, ufp, [chime])
assert_entity_counts(hass, Platform.BUTTON, 3, 2)
ufp.api.play_speaker = AsyncMock()
unique_id = f"{chime.mac}_play" unique_id = f"{chime.mac}_play"
entity_id = "button.test_chime_play_chime" entity_id = "button.test_chime_play_chime"
@ -91,4 +74,4 @@ async def test_chime_button(
await hass.services.async_call( await hass.services.async_call(
"button", "press", {ATTR_ENTITY_ID: entity_id}, blocking=True "button", "press", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
mock_entry.api.play_speaker.assert_called_once() ufp.api.play_speaker.assert_called_once()

View File

@ -2,16 +2,13 @@
# pylint: disable=protected-access # pylint: disable=protected-access
from __future__ import annotations from __future__ import annotations
from copy import copy
from unittest.mock import AsyncMock, Mock from unittest.mock import AsyncMock, Mock
import pytest
from pyunifiprotect.data import Camera as ProtectCamera, CameraChannel, StateType from pyunifiprotect.data import Camera as ProtectCamera, CameraChannel, StateType
from pyunifiprotect.exceptions import NvrError from pyunifiprotect.exceptions import NvrError
from homeassistant.components.camera import ( from homeassistant.components.camera import (
SUPPORT_STREAM, SUPPORT_STREAM,
Camera,
async_get_image, async_get_image,
async_get_stream_source, async_get_stream_source,
) )
@ -34,87 +31,15 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .conftest import ( from .utils import (
MockEntityFixture, MockUFPFixture,
assert_entity_counts, assert_entity_counts,
enable_entity, enable_entity,
regenerate_device_ids, init_entry,
time_changed, time_changed,
) )
@pytest.fixture(name="camera")
async def camera_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera
):
"""Fixture for a single camera for testing the camera platform."""
# disable pydantic validation so mocking can happen
ProtectCamera.__config__.validate_assignment = False
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.channels[0].is_rtsp_enabled = True
camera_obj.channels[0].name = "High"
camera_obj.channels[1].is_rtsp_enabled = False
camera_obj.channels[2].is_rtsp_enabled = False
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
yield (camera_obj, "camera.test_camera_high")
ProtectCamera.__config__.validate_assignment = True
@pytest.fixture(name="camera_package")
async def camera_package_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera
):
"""Fixture for a single camera for testing the camera platform."""
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.feature_flags.has_package_camera = True
camera_obj.channels[0].is_rtsp_enabled = True
camera_obj.channels[0].name = "High"
camera_obj.channels[0].rtsp_alias = "test_high_alias"
camera_obj.channels[1].is_rtsp_enabled = False
camera_obj.channels[2].is_rtsp_enabled = False
package_channel = camera_obj.channels[0].copy()
package_channel.is_rtsp_enabled = False
package_channel.name = "Package Camera"
package_channel.id = 3
package_channel.fps = 2
package_channel.rtsp_alias = "test_package_alias"
camera_obj.channels.append(package_channel)
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.CAMERA, 3, 2)
return (camera_obj, "camera.test_camera_package_camera")
def validate_default_camera_entity( def validate_default_camera_entity(
hass: HomeAssistant, hass: HomeAssistant,
camera_obj: ProtectCamera, camera_obj: ProtectCamera,
@ -242,99 +167,46 @@ async def validate_no_stream_camera_state(
async def test_basic_setup( async def test_basic_setup(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: ProtectCamera hass: HomeAssistant,
ufp: MockUFPFixture,
camera_all: ProtectCamera,
doorbell: ProtectCamera,
): ):
"""Test working setup of unifiprotect entry.""" """Test working setup of unifiprotect entry."""
camera_high_only = mock_camera.copy() camera_high_only = camera_all.copy()
camera_high_only._api = mock_entry.api camera_high_only.channels = [c.copy() for c in camera_all.channels]
camera_high_only.channels = [c.copy() for c in mock_camera.channels]
camera_high_only.channels[0]._api = mock_entry.api
camera_high_only.channels[1]._api = mock_entry.api
camera_high_only.channels[2]._api = mock_entry.api
camera_high_only.name = "Test Camera 1" camera_high_only.name = "Test Camera 1"
camera_high_only.channels[0].is_rtsp_enabled = True camera_high_only.channels[0].is_rtsp_enabled = True
camera_high_only.channels[0].name = "High"
camera_high_only.channels[0].rtsp_alias = "test_high_alias"
camera_high_only.channels[1].is_rtsp_enabled = False camera_high_only.channels[1].is_rtsp_enabled = False
camera_high_only.channels[2].is_rtsp_enabled = False camera_high_only.channels[2].is_rtsp_enabled = False
regenerate_device_ids(camera_high_only)
camera_medium_only = mock_camera.copy() camera_medium_only = camera_all.copy()
camera_medium_only._api = mock_entry.api camera_medium_only.channels = [c.copy() for c in camera_all.channels]
camera_medium_only.channels = [c.copy() for c in mock_camera.channels]
camera_medium_only.channels[0]._api = mock_entry.api
camera_medium_only.channels[1]._api = mock_entry.api
camera_medium_only.channels[2]._api = mock_entry.api
camera_medium_only.name = "Test Camera 2" camera_medium_only.name = "Test Camera 2"
camera_medium_only.channels[0].is_rtsp_enabled = False camera_medium_only.channels[0].is_rtsp_enabled = False
camera_medium_only.channels[1].is_rtsp_enabled = True camera_medium_only.channels[1].is_rtsp_enabled = True
camera_medium_only.channels[1].name = "Medium"
camera_medium_only.channels[1].rtsp_alias = "test_medium_alias"
camera_medium_only.channels[2].is_rtsp_enabled = False camera_medium_only.channels[2].is_rtsp_enabled = False
regenerate_device_ids(camera_medium_only)
camera_all_channels = mock_camera.copy() camera_all.name = "Test Camera 3"
camera_all_channels._api = mock_entry.api
camera_all_channels.channels = [c.copy() for c in mock_camera.channels]
camera_all_channels.channels[0]._api = mock_entry.api
camera_all_channels.channels[1]._api = mock_entry.api
camera_all_channels.channels[2]._api = mock_entry.api
camera_all_channels.name = "Test Camera 3"
camera_all_channels.channels[0].is_rtsp_enabled = True
camera_all_channels.channels[0].name = "High"
camera_all_channels.channels[0].rtsp_alias = "test_high_alias"
camera_all_channels.channels[1].is_rtsp_enabled = True
camera_all_channels.channels[1].name = "Medium"
camera_all_channels.channels[1].rtsp_alias = "test_medium_alias"
camera_all_channels.channels[2].is_rtsp_enabled = True
camera_all_channels.channels[2].name = "Low"
camera_all_channels.channels[2].rtsp_alias = "test_low_alias"
regenerate_device_ids(camera_all_channels)
camera_no_channels = mock_camera.copy() camera_no_channels = camera_all.copy()
camera_no_channels._api = mock_entry.api camera_no_channels.channels = [c.copy() for c in camera_all.channels]
camera_no_channels.channels = [c.copy() for c in camera_no_channels.channels]
camera_no_channels.channels[0]._api = mock_entry.api
camera_no_channels.channels[1]._api = mock_entry.api
camera_no_channels.channels[2]._api = mock_entry.api
camera_no_channels.name = "Test Camera 4" camera_no_channels.name = "Test Camera 4"
camera_no_channels.channels[0].is_rtsp_enabled = False camera_no_channels.channels[0].is_rtsp_enabled = False
camera_no_channels.channels[0].name = "High"
camera_no_channels.channels[1].is_rtsp_enabled = False camera_no_channels.channels[1].is_rtsp_enabled = False
camera_no_channels.channels[2].is_rtsp_enabled = False camera_no_channels.channels[2].is_rtsp_enabled = False
regenerate_device_ids(camera_no_channels)
camera_package = mock_camera.copy() doorbell.name = "Test Camera 5"
camera_package._api = mock_entry.api
camera_package.channels = [c.copy() for c in mock_camera.channels]
camera_package.channels[0]._api = mock_entry.api
camera_package.channels[1]._api = mock_entry.api
camera_package.channels[2]._api = mock_entry.api
camera_package.name = "Test Camera 5"
camera_package.channels[0].is_rtsp_enabled = True
camera_package.channels[0].name = "High"
camera_package.channels[0].rtsp_alias = "test_high_alias"
camera_package.channels[1].is_rtsp_enabled = False
camera_package.channels[2].is_rtsp_enabled = False
regenerate_device_ids(camera_package)
package_channel = camera_package.channels[0].copy()
package_channel.is_rtsp_enabled = False
package_channel.name = "Package Camera"
package_channel.id = 3
package_channel.fps = 2
package_channel.rtsp_alias = "test_package_alias"
camera_package.channels.append(package_channel)
mock_entry.api.bootstrap.cameras = { devices = [
camera_high_only.id: camera_high_only, camera_high_only,
camera_medium_only.id: camera_medium_only, camera_medium_only,
camera_all_channels.id: camera_all_channels, camera_all,
camera_no_channels.id: camera_no_channels, camera_no_channels,
camera_package.id: camera_package, doorbell,
} ]
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await init_entry(hass, ufp, devices)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.CAMERA, 14, 6) assert_entity_counts(hass, Platform.CAMERA, 14, 6)
@ -343,7 +215,7 @@ async def test_basic_setup(
await validate_rtsps_camera_state(hass, camera_high_only, 0, entity_id) await validate_rtsps_camera_state(hass, camera_high_only, 0, entity_id)
entity_id = validate_rtsp_camera_entity(hass, camera_high_only, 0) entity_id = validate_rtsp_camera_entity(hass, camera_high_only, 0)
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
await validate_rtsp_camera_state(hass, camera_high_only, 0, entity_id) await validate_rtsp_camera_state(hass, camera_high_only, 0, entity_id)
# test camera 2 # test camera 2
@ -351,32 +223,32 @@ async def test_basic_setup(
await validate_rtsps_camera_state(hass, camera_medium_only, 1, entity_id) await validate_rtsps_camera_state(hass, camera_medium_only, 1, entity_id)
entity_id = validate_rtsp_camera_entity(hass, camera_medium_only, 1) entity_id = validate_rtsp_camera_entity(hass, camera_medium_only, 1)
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
await validate_rtsp_camera_state(hass, camera_medium_only, 1, entity_id) await validate_rtsp_camera_state(hass, camera_medium_only, 1, entity_id)
# test camera 3 # test camera 3
entity_id = validate_default_camera_entity(hass, camera_all_channels, 0) entity_id = validate_default_camera_entity(hass, camera_all, 0)
await validate_rtsps_camera_state(hass, camera_all_channels, 0, entity_id) await validate_rtsps_camera_state(hass, camera_all, 0, entity_id)
entity_id = validate_rtsp_camera_entity(hass, camera_all_channels, 0) entity_id = validate_rtsp_camera_entity(hass, camera_all, 0)
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
await validate_rtsp_camera_state(hass, camera_all_channels, 0, entity_id) await validate_rtsp_camera_state(hass, camera_all, 0, entity_id)
entity_id = validate_rtsps_camera_entity(hass, camera_all_channels, 1) entity_id = validate_rtsps_camera_entity(hass, camera_all, 1)
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
await validate_rtsps_camera_state(hass, camera_all_channels, 1, entity_id) await validate_rtsps_camera_state(hass, camera_all, 1, entity_id)
entity_id = validate_rtsp_camera_entity(hass, camera_all_channels, 1) entity_id = validate_rtsp_camera_entity(hass, camera_all, 1)
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
await validate_rtsp_camera_state(hass, camera_all_channels, 1, entity_id) await validate_rtsp_camera_state(hass, camera_all, 1, entity_id)
entity_id = validate_rtsps_camera_entity(hass, camera_all_channels, 2) entity_id = validate_rtsps_camera_entity(hass, camera_all, 2)
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
await validate_rtsps_camera_state(hass, camera_all_channels, 2, entity_id) await validate_rtsps_camera_state(hass, camera_all, 2, entity_id)
entity_id = validate_rtsp_camera_entity(hass, camera_all_channels, 2) entity_id = validate_rtsp_camera_entity(hass, camera_all, 2)
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
await validate_rtsp_camera_state(hass, camera_all_channels, 2, entity_id) await validate_rtsp_camera_state(hass, camera_all, 2, entity_id)
# test camera 4 # test camera 4
entity_id = validate_default_camera_entity(hass, camera_no_channels, 0) entity_id = validate_default_camera_entity(hass, camera_no_channels, 0)
@ -385,197 +257,194 @@ async def test_basic_setup(
) )
# test camera 5 # test camera 5
entity_id = validate_default_camera_entity(hass, camera_package, 0) entity_id = validate_default_camera_entity(hass, doorbell, 0)
await validate_rtsps_camera_state(hass, camera_package, 0, entity_id) await validate_rtsps_camera_state(hass, doorbell, 0, entity_id)
entity_id = validate_rtsp_camera_entity(hass, camera_package, 0) entity_id = validate_rtsp_camera_entity(hass, doorbell, 0)
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
await validate_rtsp_camera_state(hass, camera_package, 0, entity_id) await validate_rtsp_camera_state(hass, doorbell, 0, entity_id)
entity_id = validate_default_camera_entity(hass, camera_package, 3) entity_id = validate_default_camera_entity(hass, doorbell, 3)
await validate_no_stream_camera_state( await validate_no_stream_camera_state(hass, doorbell, 3, entity_id, features=0)
hass, camera_package, 3, entity_id, features=0
)
async def test_missing_channels( async def test_missing_channels(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: ProtectCamera hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera
): ):
"""Test setting up camera with no camera channels.""" """Test setting up camera with no camera channels."""
camera = mock_camera.copy() camera1 = camera.copy()
camera.channels = [] camera1.channels = []
mock_entry.api.bootstrap.cameras = {camera.id: camera} await init_entry(hass, ufp, [camera1])
await hass.config_entries.async_setup(mock_entry.entry.entry_id) assert_entity_counts(hass, Platform.CAMERA, 0, 0)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
assert len(hass.states.async_all()) == 0
assert len(entity_registry.entities) == 0
async def test_camera_image( async def test_camera_image(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera
mock_entry: MockEntityFixture,
camera: tuple[Camera, str],
): ):
"""Test retrieving camera image.""" """Test retrieving camera image."""
mock_entry.api.get_camera_snapshot = AsyncMock() await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
await async_get_image(hass, camera[1]) ufp.api.get_camera_snapshot = AsyncMock()
mock_entry.api.get_camera_snapshot.assert_called_once()
await async_get_image(hass, "camera.test_camera_high")
ufp.api.get_camera_snapshot.assert_called_once()
async def test_package_camera_image( async def test_package_camera_image(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, doorbell: ProtectCamera
mock_entry: MockEntityFixture,
camera_package: tuple[Camera, str],
): ):
"""Test retrieving package camera image.""" """Test retrieving package camera image."""
mock_entry.api.get_package_camera_snapshot = AsyncMock() await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.CAMERA, 3, 2)
await async_get_image(hass, camera_package[1]) ufp.api.get_package_camera_snapshot = AsyncMock()
mock_entry.api.get_package_camera_snapshot.assert_called_once()
await async_get_image(hass, "camera.test_camera_package_camera")
ufp.api.get_package_camera_snapshot.assert_called_once()
async def test_camera_generic_update( async def test_camera_generic_update(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera
mock_entry: MockEntityFixture,
camera: tuple[ProtectCamera, str],
): ):
"""Tests generic entity update service.""" """Tests generic entity update service."""
await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
entity_id = "camera.test_camera_high"
assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "homeassistant", {})
state = hass.states.get(camera[1]) state = hass.states.get(entity_id)
assert state and state.state == "idle" assert state and state.state == "idle"
mock_entry.api.update = AsyncMock(return_value=None) ufp.api.update = AsyncMock(return_value=None)
await hass.services.async_call( await hass.services.async_call(
"homeassistant", "homeassistant",
"update_entity", "update_entity",
{ATTR_ENTITY_ID: camera[1]}, {ATTR_ENTITY_ID: entity_id},
blocking=True, blocking=True,
) )
state = hass.states.get(camera[1]) state = hass.states.get(entity_id)
assert state and state.state == "idle" assert state and state.state == "idle"
async def test_camera_interval_update( async def test_camera_interval_update(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera
mock_entry: MockEntityFixture,
camera: tuple[ProtectCamera, str],
): ):
"""Interval updates updates camera entity.""" """Interval updates updates camera entity."""
state = hass.states.get(camera[1]) await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
entity_id = "camera.test_camera_high"
state = hass.states.get(entity_id)
assert state and state.state == "idle" assert state and state.state == "idle"
new_bootstrap = copy(mock_entry.api.bootstrap) new_camera = camera.copy()
new_camera = camera[0].copy()
new_camera.is_recording = True new_camera.is_recording = True
new_bootstrap.cameras = {new_camera.id: new_camera} ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
mock_entry.api.update = AsyncMock(return_value=new_bootstrap) ufp.api.update = AsyncMock(return_value=ufp.api.bootstrap)
mock_entry.api.bootstrap = new_bootstrap
await time_changed(hass, DEFAULT_SCAN_INTERVAL) await time_changed(hass, DEFAULT_SCAN_INTERVAL)
state = hass.states.get(camera[1]) state = hass.states.get(entity_id)
assert state and state.state == "recording" assert state and state.state == "recording"
async def test_camera_bad_interval_update( async def test_camera_bad_interval_update(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera
mock_entry: MockEntityFixture,
camera: tuple[Camera, str],
): ):
"""Interval updates marks camera unavailable.""" """Interval updates marks camera unavailable."""
state = hass.states.get(camera[1]) await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
entity_id = "camera.test_camera_high"
state = hass.states.get(entity_id)
assert state and state.state == "idle" assert state and state.state == "idle"
# update fails # update fails
mock_entry.api.update = AsyncMock(side_effect=NvrError) ufp.api.update = AsyncMock(side_effect=NvrError)
await time_changed(hass, DEFAULT_SCAN_INTERVAL) await time_changed(hass, DEFAULT_SCAN_INTERVAL)
state = hass.states.get(camera[1]) state = hass.states.get(entity_id)
assert state and state.state == "unavailable" assert state and state.state == "unavailable"
# next update succeeds # next update succeeds
mock_entry.api.update = AsyncMock(return_value=mock_entry.api.bootstrap) ufp.api.update = AsyncMock(return_value=ufp.api.bootstrap)
await time_changed(hass, DEFAULT_SCAN_INTERVAL) await time_changed(hass, DEFAULT_SCAN_INTERVAL)
state = hass.states.get(camera[1]) state = hass.states.get(entity_id)
assert state and state.state == "idle" assert state and state.state == "idle"
async def test_camera_ws_update( async def test_camera_ws_update(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera
mock_entry: MockEntityFixture,
camera: tuple[ProtectCamera, str],
): ):
"""WS update updates camera entity.""" """WS update updates camera entity."""
state = hass.states.get(camera[1]) await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
entity_id = "camera.test_camera_high"
state = hass.states.get(entity_id)
assert state and state.state == "idle" assert state and state.state == "idle"
new_bootstrap = copy(mock_entry.api.bootstrap) new_camera = camera.copy()
new_camera = camera[0].copy()
new_camera.is_recording = True new_camera.is_recording = True
no_camera = camera[0].copy() no_camera = camera.copy()
no_camera.is_adopted = False no_camera.is_adopted = False
new_bootstrap.cameras = {new_camera.id: new_camera} ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
mock_entry.api.bootstrap = new_bootstrap
mock_msg = Mock() mock_msg = Mock()
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_camera mock_msg.new_obj = new_camera
mock_entry.api.ws_subscription(mock_msg) ufp.ws_msg(mock_msg)
mock_msg = Mock() mock_msg = Mock()
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = no_camera mock_msg.new_obj = no_camera
mock_entry.api.ws_subscription(mock_msg) ufp.ws_msg(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(camera[1]) state = hass.states.get(entity_id)
assert state and state.state == "recording" assert state and state.state == "recording"
async def test_camera_ws_update_offline( async def test_camera_ws_update_offline(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera
mock_entry: MockEntityFixture,
camera: tuple[ProtectCamera, str],
): ):
"""WS updates marks camera unavailable.""" """WS updates marks camera unavailable."""
state = hass.states.get(camera[1]) await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.CAMERA, 2, 1)
entity_id = "camera.test_camera_high"
state = hass.states.get(entity_id)
assert state and state.state == "idle" assert state and state.state == "idle"
# camera goes offline # camera goes offline
new_bootstrap = copy(mock_entry.api.bootstrap) new_camera = camera.copy()
new_camera = camera[0].copy()
new_camera.state = StateType.DISCONNECTED new_camera.state = StateType.DISCONNECTED
mock_msg = Mock() mock_msg = Mock()
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_camera mock_msg.new_obj = new_camera
new_bootstrap.cameras = {new_camera.id: new_camera} ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(camera[1]) state = hass.states.get(entity_id)
assert state and state.state == "unavailable" assert state and state.state == "unavailable"
# camera comes back online # camera comes back online
@ -585,50 +454,53 @@ async def test_camera_ws_update_offline(
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_camera mock_msg.new_obj = new_camera
new_bootstrap.cameras = {new_camera.id: new_camera} ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(camera[1]) state = hass.states.get(entity_id)
assert state and state.state == "idle" assert state and state.state == "idle"
async def test_camera_enable_motion( async def test_camera_enable_motion(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera
mock_entry: MockEntityFixture,
camera: tuple[ProtectCamera, str],
): ):
"""Tests generic entity update service.""" """Tests generic entity update service."""
camera[0].__fields__["set_motion_detection"] = Mock() await init_entry(hass, ufp, [camera])
camera[0].set_motion_detection = AsyncMock() assert_entity_counts(hass, Platform.CAMERA, 2, 1)
entity_id = "camera.test_camera_high"
camera.__fields__["set_motion_detection"] = Mock()
camera.set_motion_detection = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"camera", "camera",
"enable_motion_detection", "enable_motion_detection",
{ATTR_ENTITY_ID: camera[1]}, {ATTR_ENTITY_ID: entity_id},
blocking=True, blocking=True,
) )
camera[0].set_motion_detection.assert_called_once_with(True) camera.set_motion_detection.assert_called_once_with(True)
async def test_camera_disable_motion( async def test_camera_disable_motion(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera
mock_entry: MockEntityFixture,
camera: tuple[ProtectCamera, str],
): ):
"""Tests generic entity update service.""" """Tests generic entity update service."""
camera[0].__fields__["set_motion_detection"] = Mock() await init_entry(hass, ufp, [camera])
camera[0].set_motion_detection = AsyncMock() assert_entity_counts(hass, Platform.CAMERA, 2, 1)
entity_id = "camera.test_camera_high"
camera.__fields__["set_motion_detection"] = Mock()
camera.set_motion_detection = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"camera", "camera",
"disable_motion_detection", "disable_motion_detection",
{ATTR_ENTITY_ID: camera[1]}, {ATTR_ENTITY_ID: entity_id},
blocking=True, blocking=True,
) )
camera[0].set_motion_detection.assert_called_once_with(False) camera.set_motion_detection.assert_called_once_with(False)

View File

@ -6,7 +6,7 @@ import socket
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from pyunifiprotect import NotAuthorized, NvrError from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
from pyunifiprotect.data import NVR from pyunifiprotect.data import NVR
from homeassistant import config_entries from homeassistant import config_entries
@ -61,7 +61,7 @@ UNIFI_DISCOVERY_DICT = asdict(UNIFI_DISCOVERY)
UNIFI_DISCOVERY_DICT_PARTIAL = asdict(UNIFI_DISCOVERY_PARTIAL) UNIFI_DISCOVERY_DICT_PARTIAL = asdict(UNIFI_DISCOVERY_PARTIAL)
async def test_form(hass: HomeAssistant, mock_nvr: NVR) -> None: async def test_form(hass: HomeAssistant, nvr: NVR) -> None:
"""Test we get the form.""" """Test we get the form."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -71,7 +71,7 @@ async def test_form(hass: HomeAssistant, mock_nvr: NVR) -> None:
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr",
return_value=mock_nvr, return_value=nvr,
), patch( ), patch(
"homeassistant.components.unifiprotect.async_setup_entry", "homeassistant.components.unifiprotect.async_setup_entry",
return_value=True, return_value=True,
@ -99,7 +99,7 @@ async def test_form(hass: HomeAssistant, mock_nvr: NVR) -> None:
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_form_version_too_old(hass: HomeAssistant, mock_old_nvr: NVR) -> None: async def test_form_version_too_old(hass: HomeAssistant, old_nvr: NVR) -> None:
"""Test we handle the version being too old.""" """Test we handle the version being too old."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -107,7 +107,7 @@ async def test_form_version_too_old(hass: HomeAssistant, mock_old_nvr: NVR) -> N
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr",
return_value=mock_old_nvr, return_value=old_nvr,
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@ -168,7 +168,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
assert result2["errors"] == {"base": "cannot_connect"} assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_reauth_auth(hass: HomeAssistant, mock_nvr: NVR) -> None: async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None:
"""Test we handle reauth auth.""" """Test we handle reauth auth."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@ -217,7 +217,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, mock_nvr: NVR) -> None:
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr",
return_value=mock_nvr, return_value=nvr,
): ):
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
@ -231,7 +231,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, mock_nvr: NVR) -> None:
assert result3["reason"] == "reauth_successful" assert result3["reason"] == "reauth_successful"
async def test_form_options(hass: HomeAssistant, mock_client) -> None: async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) -> None:
"""Test we handle options flows.""" """Test we handle options flows."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@ -251,7 +251,7 @@ async def test_form_options(hass: HomeAssistant, mock_client) -> None:
with _patch_discovery(), patch( with _patch_discovery(), patch(
"homeassistant.components.unifiprotect.ProtectApiClient" "homeassistant.components.unifiprotect.ProtectApiClient"
) as mock_api: ) as mock_api:
mock_api.return_value = mock_client mock_api.return_value = ufp_client
await hass.config_entries.async_setup(mock_config.entry_id) await hass.config_entries.async_setup(mock_config.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -300,7 +300,7 @@ async def test_discovered_by_ssdp_or_dhcp(
async def test_discovered_by_unifi_discovery_direct_connect( async def test_discovered_by_unifi_discovery_direct_connect(
hass: HomeAssistant, mock_nvr: NVR hass: HomeAssistant, nvr: NVR
) -> None: ) -> None:
"""Test a discovery from unifi-discovery.""" """Test a discovery from unifi-discovery."""
@ -324,7 +324,7 @@ async def test_discovered_by_unifi_discovery_direct_connect(
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr",
return_value=mock_nvr, return_value=nvr,
), patch( ), patch(
"homeassistant.components.unifiprotect.async_setup_entry", "homeassistant.components.unifiprotect.async_setup_entry",
return_value=True, return_value=True,
@ -352,7 +352,7 @@ async def test_discovered_by_unifi_discovery_direct_connect(
async def test_discovered_by_unifi_discovery_direct_connect_updated( async def test_discovered_by_unifi_discovery_direct_connect_updated(
hass: HomeAssistant, mock_nvr: NVR hass: HomeAssistant,
) -> None: ) -> None:
"""Test a discovery from unifi-discovery updates the direct connect host.""" """Test a discovery from unifi-discovery updates the direct connect host."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
@ -384,7 +384,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated(
async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_using_direct_connect( async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_using_direct_connect(
hass: HomeAssistant, mock_nvr: NVR hass: HomeAssistant,
) -> None: ) -> None:
"""Test a discovery from unifi-discovery updates the host but not direct connect if its not in use.""" """Test a discovery from unifi-discovery updates the host but not direct connect if its not in use."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
@ -419,7 +419,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin
async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_still_online( async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_still_online(
hass: HomeAssistant, mock_nvr: NVR hass: HomeAssistant,
) -> None: ) -> None:
"""Test a discovery from unifi-discovery does not update the ip unless the console at the old ip is offline.""" """Test a discovery from unifi-discovery does not update the ip unless the console at the old ip is offline."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
@ -454,7 +454,7 @@ async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_
async def test_discovered_host_not_updated_if_existing_is_a_hostname( async def test_discovered_host_not_updated_if_existing_is_a_hostname(
hass: HomeAssistant, mock_nvr: NVR hass: HomeAssistant,
) -> None: ) -> None:
"""Test we only update the host if its an ip address from discovery.""" """Test we only update the host if its an ip address from discovery."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
@ -484,9 +484,7 @@ async def test_discovered_host_not_updated_if_existing_is_a_hostname(
assert mock_config.data[CONF_HOST] == "a.hostname" assert mock_config.data[CONF_HOST] == "a.hostname"
async def test_discovered_by_unifi_discovery( async def test_discovered_by_unifi_discovery(hass: HomeAssistant, nvr: NVR) -> None:
hass: HomeAssistant, mock_nvr: NVR
) -> None:
"""Test a discovery from unifi-discovery.""" """Test a discovery from unifi-discovery."""
with _patch_discovery(): with _patch_discovery():
@ -509,7 +507,7 @@ async def test_discovered_by_unifi_discovery(
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr",
side_effect=[NotAuthorized, mock_nvr], side_effect=[NotAuthorized, nvr],
), patch( ), patch(
"homeassistant.components.unifiprotect.async_setup_entry", "homeassistant.components.unifiprotect.async_setup_entry",
return_value=True, return_value=True,
@ -537,7 +535,7 @@ async def test_discovered_by_unifi_discovery(
async def test_discovered_by_unifi_discovery_partial( async def test_discovered_by_unifi_discovery_partial(
hass: HomeAssistant, mock_nvr: NVR hass: HomeAssistant, nvr: NVR
) -> None: ) -> None:
"""Test a discovery from unifi-discovery partial.""" """Test a discovery from unifi-discovery partial."""
@ -561,7 +559,7 @@ async def test_discovered_by_unifi_discovery_partial(
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr",
return_value=mock_nvr, return_value=nvr,
), patch( ), patch(
"homeassistant.components.unifiprotect.async_setup_entry", "homeassistant.components.unifiprotect.async_setup_entry",
return_value=True, return_value=True,
@ -589,7 +587,7 @@ async def test_discovered_by_unifi_discovery_partial(
async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface( async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface(
hass: HomeAssistant, mock_nvr: NVR hass: HomeAssistant,
) -> None: ) -> None:
"""Test a discovery from unifi-discovery from an alternate interface.""" """Test a discovery from unifi-discovery from an alternate interface."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
@ -619,7 +617,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_ip_matches( async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_ip_matches(
hass: HomeAssistant, mock_nvr: NVR hass: HomeAssistant,
) -> None: ) -> None:
"""Test a discovery from unifi-discovery from an alternate interface when the ip matches.""" """Test a discovery from unifi-discovery from an alternate interface when the ip matches."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
@ -649,7 +647,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver( async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver(
hass: HomeAssistant, mock_nvr: NVR hass: HomeAssistant,
) -> None: ) -> None:
"""Test a discovery from unifi-discovery from an alternate interface when direct connect domain resolves to host ip.""" """Test a discovery from unifi-discovery from an alternate interface when direct connect domain resolves to host ip."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
@ -687,7 +685,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver_fails( async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver_fails(
hass: HomeAssistant, mock_nvr: NVR hass: HomeAssistant, nvr: NVR
) -> None: ) -> None:
"""Test we can still configure if the resolver fails.""" """Test we can still configure if the resolver fails."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
@ -730,7 +728,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
with patch( with patch(
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr",
return_value=mock_nvr, return_value=nvr,
), patch( ), patch(
"homeassistant.components.unifiprotect.async_setup_entry", "homeassistant.components.unifiprotect.async_setup_entry",
return_value=True, return_value=True,
@ -758,7 +756,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver_no_result( async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver_no_result(
hass: HomeAssistant, mock_nvr: NVR hass: HomeAssistant,
) -> None: ) -> None:
"""Test a discovery from unifi-discovery from an alternate interface when direct connect domain resolve has no result.""" """Test a discovery from unifi-discovery from an alternate interface when direct connect domain resolve has no result."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
@ -791,7 +789,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
async def test_discovery_can_be_ignored(hass: HomeAssistant, mock_nvr: NVR) -> None: async def test_discovery_can_be_ignored(hass: HomeAssistant) -> None:
"""Test a discovery can be ignored.""" """Test a discovery can be ignored."""
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,

View File

@ -4,53 +4,44 @@ from pyunifiprotect.data import NVR, Light
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .conftest import MockEntityFixture, regenerate_device_ids from .utils import MockUFPFixture, init_entry
from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.components.diagnostics import get_diagnostics_for_config_entry
async def test_diagnostics( async def test_diagnostics(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light, hass_client hass: HomeAssistant, ufp: MockUFPFixture, light: Light, hass_client
): ):
"""Test generating diagnostics for a config entry.""" """Test generating diagnostics for a config entry."""
light1 = mock_light.copy() await init_entry(hass, ufp, [light])
light1._api = mock_entry.api
light1.name = "Test Light 1"
regenerate_device_ids(light1)
mock_entry.api.bootstrap.lights = { diag = await get_diagnostics_for_config_entry(hass, hass_client, ufp.entry)
light1.id: light1,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
diag = await get_diagnostics_for_config_entry(hass, hass_client, mock_entry.entry) nvr: NVR = ufp.api.bootstrap.nvr
nvr_obj: NVR = mock_entry.api.bootstrap.nvr
# validate some of the data # validate some of the data
assert "nvr" in diag and isinstance(diag["nvr"], dict) assert "nvr" in diag and isinstance(diag["nvr"], dict)
nvr = diag["nvr"] nvr_dict = diag["nvr"]
# should have been anonymized # should have been anonymized
assert nvr["id"] != nvr_obj.id assert nvr_dict["id"] != nvr.id
assert nvr["mac"] != nvr_obj.mac assert nvr_dict["mac"] != nvr.mac
assert nvr["host"] != str(nvr_obj.host) assert nvr_dict["host"] != str(nvr.host)
# should have been kept # should have been kept
assert nvr["firmwareVersion"] == nvr_obj.firmware_version assert nvr_dict["firmwareVersion"] == nvr.firmware_version
assert nvr["version"] == str(nvr_obj.version) assert nvr_dict["version"] == str(nvr.version)
assert nvr["type"] == nvr_obj.type assert nvr_dict["type"] == nvr.type
assert ( assert (
"lights" in diag "lights" in diag
and isinstance(diag["lights"], list) and isinstance(diag["lights"], list)
and len(diag["lights"]) == 1 and len(diag["lights"]) == 1
) )
light = diag["lights"][0] light_dict = diag["lights"][0]
# should have been anonymized # should have been anonymized
assert light["id"] != light1.id assert light_dict["id"] != light.id
assert light["name"] != light1.mac assert light_dict["name"] != light.mac
assert light["mac"] != light1.mac assert light_dict["mac"] != light.mac
assert light["host"] != str(light1.host) assert light_dict["host"] != str(light.host)
# should have been kept # should have been kept
assert light["firmwareVersion"] == light1.firmware_version assert light_dict["firmwareVersion"] == light.firmware_version
assert light["type"] == light1.type assert light_dict["type"] == light.type

View File

@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import aiohttp import aiohttp
from pyunifiprotect import NotAuthorized, NvrError from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
from pyunifiprotect.data import NVR, Bootstrap, Light from pyunifiprotect.data import NVR, Bootstrap, Light
from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN
@ -16,7 +16,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from . import _patch_discovery from . import _patch_discovery
from .conftest import MockEntityFixture, regenerate_device_ids from .utils import MockUFPFixture, init_entry
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -37,37 +37,36 @@ async def remove_device(
return response["success"] return response["success"]
async def test_setup(hass: HomeAssistant, mock_entry: MockEntityFixture): async def test_setup(hass: HomeAssistant, ufp: MockUFPFixture):
"""Test working setup of unifiprotect entry.""" """Test working setup of unifiprotect entry."""
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_entry.entry.state == ConfigEntryState.LOADED assert ufp.entry.state == ConfigEntryState.LOADED
assert mock_entry.api.update.called assert ufp.api.update.called
assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
async def test_setup_multiple( async def test_setup_multiple(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
mock_client, bootstrap: Bootstrap,
mock_bootstrap: Bootstrap,
): ):
"""Test working setup of unifiprotect entry.""" """Test working setup of unifiprotect entry."""
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_entry.entry.state == ConfigEntryState.LOADED assert ufp.entry.state == ConfigEntryState.LOADED
assert mock_entry.api.update.called assert ufp.api.update.called
assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
nvr = mock_bootstrap.nvr nvr = bootstrap.nvr
nvr._api = mock_client nvr._api = ufp.api
nvr.mac = "A1E00C826983" nvr.mac = "A1E00C826983"
nvr.id nvr.id
mock_client.get_nvr = AsyncMock(return_value=nvr) ufp.api.get_nvr = AsyncMock(return_value=nvr)
with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api:
mock_config = MockConfigEntry( mock_config = MockConfigEntry(
@ -84,148 +83,134 @@ async def test_setup_multiple(
) )
mock_config.add_to_hass(hass) mock_config.add_to_hass(hass)
mock_api.return_value = mock_client mock_api.return_value = ufp.api
await hass.config_entries.async_setup(mock_config.entry_id) await hass.config_entries.async_setup(mock_config.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_config.state == ConfigEntryState.LOADED assert mock_config.state == ConfigEntryState.LOADED
assert mock_client.update.called assert ufp.api.update.called
assert mock_config.unique_id == mock_client.bootstrap.nvr.mac assert mock_config.unique_id == ufp.api.bootstrap.nvr.mac
async def test_reload(hass: HomeAssistant, mock_entry: MockEntityFixture): async def test_reload(hass: HomeAssistant, ufp: MockUFPFixture):
"""Test updating entry reload entry.""" """Test updating entry reload entry."""
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_entry.entry.state == ConfigEntryState.LOADED assert ufp.entry.state == ConfigEntryState.LOADED
options = dict(mock_entry.entry.options) options = dict(ufp.entry.options)
options[CONF_DISABLE_RTSP] = True options[CONF_DISABLE_RTSP] = True
hass.config_entries.async_update_entry(mock_entry.entry, options=options) hass.config_entries.async_update_entry(ufp.entry, options=options)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_entry.entry.state == ConfigEntryState.LOADED assert ufp.entry.state == ConfigEntryState.LOADED
assert mock_entry.api.async_disconnect_ws.called assert ufp.api.async_disconnect_ws.called
async def test_unload(hass: HomeAssistant, mock_entry: MockEntityFixture): async def test_unload(hass: HomeAssistant, ufp: MockUFPFixture):
"""Test unloading of unifiprotect entry.""" """Test unloading of unifiprotect entry."""
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_entry.entry.state == ConfigEntryState.LOADED assert ufp.entry.state == ConfigEntryState.LOADED
await hass.config_entries.async_unload(mock_entry.entry.entry_id) await hass.config_entries.async_unload(ufp.entry.entry_id)
assert mock_entry.entry.state == ConfigEntryState.NOT_LOADED assert ufp.entry.state == ConfigEntryState.NOT_LOADED
assert mock_entry.api.async_disconnect_ws.called assert ufp.api.async_disconnect_ws.called
async def test_setup_too_old( async def test_setup_too_old(hass: HomeAssistant, ufp: MockUFPFixture, old_nvr: NVR):
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_old_nvr: NVR
):
"""Test setup of unifiprotect entry with too old of version of UniFi Protect.""" """Test setup of unifiprotect entry with too old of version of UniFi Protect."""
mock_entry.api.get_nvr.return_value = mock_old_nvr ufp.api.get_nvr.return_value = old_nvr
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_entry.entry.state == ConfigEntryState.SETUP_ERROR assert ufp.entry.state == ConfigEntryState.SETUP_ERROR
assert not mock_entry.api.update.called assert not ufp.api.update.called
async def test_setup_failed_update(hass: HomeAssistant, mock_entry: MockEntityFixture): async def test_setup_failed_update(hass: HomeAssistant, ufp: MockUFPFixture):
"""Test setup of unifiprotect entry with failed update.""" """Test setup of unifiprotect entry with failed update."""
mock_entry.api.update = AsyncMock(side_effect=NvrError) ufp.api.update = AsyncMock(side_effect=NvrError)
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_entry.entry.state == ConfigEntryState.SETUP_RETRY assert ufp.entry.state == ConfigEntryState.SETUP_RETRY
assert mock_entry.api.update.called assert ufp.api.update.called
async def test_setup_failed_update_reauth( async def test_setup_failed_update_reauth(hass: HomeAssistant, ufp: MockUFPFixture):
hass: HomeAssistant, mock_entry: MockEntityFixture
):
"""Test setup of unifiprotect entry with update that gives unauthroized error.""" """Test setup of unifiprotect entry with update that gives unauthroized error."""
mock_entry.api.update = AsyncMock(side_effect=NotAuthorized) ufp.api.update = AsyncMock(side_effect=NotAuthorized)
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_entry.entry.state == ConfigEntryState.SETUP_RETRY assert ufp.entry.state == ConfigEntryState.SETUP_RETRY
assert mock_entry.api.update.called assert ufp.api.update.called
async def test_setup_failed_error(hass: HomeAssistant, mock_entry: MockEntityFixture): async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture):
"""Test setup of unifiprotect entry with generic error.""" """Test setup of unifiprotect entry with generic error."""
mock_entry.api.get_nvr = AsyncMock(side_effect=NvrError) ufp.api.get_nvr = AsyncMock(side_effect=NvrError)
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_entry.entry.state == ConfigEntryState.SETUP_RETRY assert ufp.entry.state == ConfigEntryState.SETUP_RETRY
assert not mock_entry.api.update.called assert not ufp.api.update.called
async def test_setup_failed_auth(hass: HomeAssistant, mock_entry: MockEntityFixture): async def test_setup_failed_auth(hass: HomeAssistant, ufp: MockUFPFixture):
"""Test setup of unifiprotect entry with unauthorized error.""" """Test setup of unifiprotect entry with unauthorized error."""
mock_entry.api.get_nvr = AsyncMock(side_effect=NotAuthorized) ufp.api.get_nvr = AsyncMock(side_effect=NotAuthorized)
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
assert mock_entry.entry.state == ConfigEntryState.SETUP_ERROR assert ufp.entry.state == ConfigEntryState.SETUP_ERROR
assert not mock_entry.api.update.called assert not ufp.api.update.called
async def test_setup_starts_discovery( async def test_setup_starts_discovery(
hass: HomeAssistant, mock_ufp_config_entry: ConfigEntry, mock_client hass: HomeAssistant, ufp_config_entry: ConfigEntry, ufp_client: ProtectApiClient
): ):
"""Test setting up will start discovery.""" """Test setting up will start discovery."""
with _patch_discovery(), patch( with _patch_discovery(), patch(
"homeassistant.components.unifiprotect.ProtectApiClient" "homeassistant.components.unifiprotect.ProtectApiClient"
) as mock_api: ) as mock_api:
mock_ufp_config_entry.add_to_hass(hass) ufp_config_entry.add_to_hass(hass)
mock_api.return_value = mock_client mock_api.return_value = ufp_client
mock_entry = MockEntityFixture(mock_ufp_config_entry, mock_client) ufp = MockUFPFixture(ufp_config_entry, ufp_client)
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_entry.entry.state == ConfigEntryState.LOADED assert ufp.entry.state == ConfigEntryState.LOADED
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1 assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1
async def test_device_remove_devices( async def test_device_remove_devices(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
mock_light: Light, light: Light,
hass_ws_client: Callable[ hass_ws_client: Callable[
[HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse]
], ],
) -> None: ) -> None:
"""Test we can only remove a device that no longer exists.""" """Test we can only remove a device that no longer exists."""
await init_entry(hass, ufp, [light])
assert await async_setup_component(hass, "config", {}) assert await async_setup_component(hass, "config", {})
entity_id = "light.test_light"
light1 = mock_light.copy() entry_id = ufp.entry.entry_id
light1._api = mock_entry.api
light1.name = "Test Light 1"
regenerate_device_ids(light1)
mock_entry.api.bootstrap.lights = {
light1.id: light1,
}
mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap)
light_entity_id = "light.test_light_1"
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
entry_id = mock_entry.entry.entry_id
registry: er.EntityRegistry = er.async_get(hass) registry: er.EntityRegistry = er.async_get(hass)
entity = registry.entities[light_entity_id] entity = registry.async_get(entity_id)
assert entity is not None
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
live_device_entry = device_registry.async_get(entity.device_id) live_device_entry = device_registry.async_get(entity.device_id)
@ -246,7 +231,7 @@ async def test_device_remove_devices(
async def test_device_remove_devices_nvr( async def test_device_remove_devices_nvr(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
hass_ws_client: Callable[ hass_ws_client: Callable[
[HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse]
], ],
@ -254,10 +239,10 @@ async def test_device_remove_devices_nvr(
"""Test we can only remove a NVR device that no longer exists.""" """Test we can only remove a NVR device that no longer exists."""
assert await async_setup_component(hass, "config", {}) assert await async_setup_component(hass, "config", {})
mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap)
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
entry_id = mock_entry.entry.entry_id entry_id = ufp.entry.entry_id
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)

View File

@ -2,11 +2,10 @@
# pylint: disable=protected-access # pylint: disable=protected-access
from __future__ import annotations from __future__ import annotations
from copy import copy
from unittest.mock import AsyncMock, Mock from unittest.mock import AsyncMock, Mock
import pytest
from pyunifiprotect.data import Light from pyunifiprotect.data import Light
from pyunifiprotect.data.types import LEDLevel
from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
@ -20,53 +19,19 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .conftest import MockEntityFixture, assert_entity_counts, regenerate_device_ids from .utils import MockUFPFixture, assert_entity_counts, init_entry
@pytest.fixture(name="light")
async def light_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light
):
"""Fixture for a single light for testing the light platform."""
# disable pydantic validation so mocking can happen
Light.__config__.validate_assignment = False
light_obj = mock_light.copy()
light_obj._api = mock_entry.api
light_obj.name = "Test Light"
light_obj.is_light_on = False
regenerate_device_ids(light_obj)
no_light_obj = mock_light.copy()
no_light_obj._api = mock_entry.api
no_light_obj.name = "Unadopted Light"
no_light_obj.is_adopted = False
regenerate_device_ids(no_light_obj)
mock_entry.api.bootstrap.lights = {
light_obj.id: light_obj,
no_light_obj.id: no_light_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.LIGHT, 1, 1)
yield (light_obj, "light.test_light")
Light.__config__.validate_assignment = True
async def test_light_setup( async def test_light_setup(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, light: Light, unadopted_light: Light
light: tuple[Light, str],
): ):
"""Test light entity setup.""" """Test light entity setup."""
unique_id = light[0].mac await init_entry(hass, ufp, [light, unadopted_light])
entity_id = light[1] assert_entity_counts(hass, Platform.LIGHT, 1, 1)
unique_id = light.mac
entity_id = "light.test_light"
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -80,41 +45,42 @@ async def test_light_setup(
async def test_light_update( async def test_light_update(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, light: Light, unadopted_light: Light
mock_entry: MockEntityFixture,
light: tuple[Light, str],
): ):
"""Test light entity update.""" """Test light entity update."""
new_bootstrap = copy(mock_entry.api.bootstrap) await init_entry(hass, ufp, [light, unadopted_light])
new_light = light[0].copy() assert_entity_counts(hass, Platform.LIGHT, 1, 1)
new_light = light.copy()
new_light.is_light_on = True new_light.is_light_on = True
new_light.light_device_settings.led_level = 3 new_light.light_device_settings.led_level = LEDLevel(3)
mock_msg = Mock() mock_msg = Mock()
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_light mock_msg.new_obj = new_light
new_bootstrap.lights = {new_light.id: new_light} ufp.api.bootstrap.lights = {new_light.id: new_light}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(light[1]) state = hass.states.get("light.test_light")
assert state assert state
assert state.state == STATE_ON assert state.state == STATE_ON
assert state.attributes[ATTR_BRIGHTNESS] == 128 assert state.attributes[ATTR_BRIGHTNESS] == 128
async def test_light_turn_on( async def test_light_turn_on(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, light: Light, unadopted_light: Light
light: tuple[Light, str],
): ):
"""Test light entity turn off.""" """Test light entity turn off."""
entity_id = light[1] await init_entry(hass, ufp, [light, unadopted_light])
light[0].__fields__["set_light"] = Mock() assert_entity_counts(hass, Platform.LIGHT, 1, 1)
light[0].set_light = AsyncMock()
entity_id = "light.test_light"
light.__fields__["set_light"] = Mock()
light.set_light = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"light", "light",
@ -123,18 +89,20 @@ async def test_light_turn_on(
blocking=True, blocking=True,
) )
light[0].set_light.assert_called_once_with(True, 3) light.set_light.assert_called_once_with(True, 3)
async def test_light_turn_off( async def test_light_turn_off(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, light: Light, unadopted_light: Light
light: tuple[Light, str],
): ):
"""Test light entity turn on.""" """Test light entity turn on."""
entity_id = light[1] await init_entry(hass, ufp, [light, unadopted_light])
light[0].__fields__["set_light"] = Mock() assert_entity_counts(hass, Platform.LIGHT, 1, 1)
light[0].set_light = AsyncMock()
entity_id = "light.test_light"
light.__fields__["set_light"] = Mock()
light.set_light = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"light", "light",
@ -143,4 +111,4 @@ async def test_light_turn_off(
blocking=True, blocking=True,
) )
light[0].set_light.assert_called_once_with(False) light.set_light.assert_called_once_with(False)

View File

@ -2,10 +2,8 @@
# pylint: disable=protected-access # pylint: disable=protected-access
from __future__ import annotations from __future__ import annotations
from copy import copy
from unittest.mock import AsyncMock, Mock from unittest.mock import AsyncMock, Mock
import pytest
from pyunifiprotect.data import Doorlock, LockStatusType from pyunifiprotect.data import Doorlock, LockStatusType
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
@ -23,53 +21,22 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .conftest import MockEntityFixture, assert_entity_counts, regenerate_device_ids from .utils import MockUFPFixture, assert_entity_counts, init_entry
@pytest.fixture(name="doorlock")
async def doorlock_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_doorlock: Doorlock
):
"""Fixture for a single doorlock for testing the lock platform."""
# disable pydantic validation so mocking can happen
Doorlock.__config__.validate_assignment = False
lock_obj = mock_doorlock.copy()
lock_obj._api = mock_entry.api
lock_obj.name = "Test Lock"
lock_obj.lock_status = LockStatusType.OPEN
regenerate_device_ids(lock_obj)
no_lock_obj = mock_doorlock.copy()
no_lock_obj._api = mock_entry.api
no_lock_obj.name = "Unadopted Lock"
no_lock_obj.is_adopted = False
regenerate_device_ids(no_lock_obj)
mock_entry.api.bootstrap.doorlocks = {
lock_obj.id: lock_obj,
no_lock_obj.id: no_lock_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.LOCK, 1, 1)
yield (lock_obj, "lock.test_lock_lock")
Doorlock.__config__.validate_assignment = True
async def test_lock_setup( async def test_lock_setup(
hass: HomeAssistant, hass: HomeAssistant,
doorlock: tuple[Doorlock, str], ufp: MockUFPFixture,
doorlock: Doorlock,
unadopted_doorlock: Doorlock,
): ):
"""Test lock entity setup.""" """Test lock entity setup."""
unique_id = f"{doorlock[0].mac}_lock" await init_entry(hass, ufp, [doorlock, unadopted_doorlock])
entity_id = doorlock[1] assert_entity_counts(hass, Platform.LOCK, 1, 1)
unique_id = f"{doorlock.mac}_lock"
entity_id = "lock.test_lock_lock"
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -84,166 +51,183 @@ async def test_lock_setup(
async def test_lock_locked( async def test_lock_locked(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
doorlock: tuple[Doorlock, str], doorlock: Doorlock,
unadopted_doorlock: Doorlock,
): ):
"""Test lock entity locked.""" """Test lock entity locked."""
new_bootstrap = copy(mock_entry.api.bootstrap) await init_entry(hass, ufp, [doorlock, unadopted_doorlock])
new_lock = doorlock[0].copy() assert_entity_counts(hass, Platform.LOCK, 1, 1)
new_lock = doorlock.copy()
new_lock.lock_status = LockStatusType.CLOSED new_lock.lock_status = LockStatusType.CLOSED
mock_msg = Mock() mock_msg = Mock()
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_lock mock_msg.new_obj = new_lock
new_bootstrap.doorlocks = {new_lock.id: new_lock} ufp.api.bootstrap.doorlocks = {new_lock.id: new_lock}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(doorlock[1]) state = hass.states.get("lock.test_lock_lock")
assert state assert state
assert state.state == STATE_LOCKED assert state.state == STATE_LOCKED
async def test_lock_unlocking( async def test_lock_unlocking(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
doorlock: tuple[Doorlock, str], doorlock: Doorlock,
unadopted_doorlock: Doorlock,
): ):
"""Test lock entity unlocking.""" """Test lock entity unlocking."""
new_bootstrap = copy(mock_entry.api.bootstrap) await init_entry(hass, ufp, [doorlock, unadopted_doorlock])
new_lock = doorlock[0].copy() assert_entity_counts(hass, Platform.LOCK, 1, 1)
new_lock = doorlock.copy()
new_lock.lock_status = LockStatusType.OPENING new_lock.lock_status = LockStatusType.OPENING
mock_msg = Mock() mock_msg = Mock()
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_lock mock_msg.new_obj = new_lock
new_bootstrap.doorlocks = {new_lock.id: new_lock} ufp.api.bootstrap.doorlocks = {new_lock.id: new_lock}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(doorlock[1]) state = hass.states.get("lock.test_lock_lock")
assert state assert state
assert state.state == STATE_UNLOCKING assert state.state == STATE_UNLOCKING
async def test_lock_locking( async def test_lock_locking(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
doorlock: tuple[Doorlock, str], doorlock: Doorlock,
unadopted_doorlock: Doorlock,
): ):
"""Test lock entity locking.""" """Test lock entity locking."""
new_bootstrap = copy(mock_entry.api.bootstrap) await init_entry(hass, ufp, [doorlock, unadopted_doorlock])
new_lock = doorlock[0].copy() assert_entity_counts(hass, Platform.LOCK, 1, 1)
new_lock = doorlock.copy()
new_lock.lock_status = LockStatusType.CLOSING new_lock.lock_status = LockStatusType.CLOSING
mock_msg = Mock() mock_msg = Mock()
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_lock mock_msg.new_obj = new_lock
new_bootstrap.doorlocks = {new_lock.id: new_lock} ufp.api.bootstrap.doorlocks = {new_lock.id: new_lock}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(doorlock[1]) state = hass.states.get("lock.test_lock_lock")
assert state assert state
assert state.state == STATE_LOCKING assert state.state == STATE_LOCKING
async def test_lock_jammed( async def test_lock_jammed(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
doorlock: tuple[Doorlock, str], doorlock: Doorlock,
unadopted_doorlock: Doorlock,
): ):
"""Test lock entity jammed.""" """Test lock entity jammed."""
new_bootstrap = copy(mock_entry.api.bootstrap) await init_entry(hass, ufp, [doorlock, unadopted_doorlock])
new_lock = doorlock[0].copy() assert_entity_counts(hass, Platform.LOCK, 1, 1)
new_lock = doorlock.copy()
new_lock.lock_status = LockStatusType.JAMMED_WHILE_CLOSING new_lock.lock_status = LockStatusType.JAMMED_WHILE_CLOSING
mock_msg = Mock() mock_msg = Mock()
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_lock mock_msg.new_obj = new_lock
new_bootstrap.doorlocks = {new_lock.id: new_lock} ufp.api.bootstrap.doorlocks = {new_lock.id: new_lock}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(doorlock[1]) state = hass.states.get("lock.test_lock_lock")
assert state assert state
assert state.state == STATE_JAMMED assert state.state == STATE_JAMMED
async def test_lock_unavailable( async def test_lock_unavailable(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
doorlock: tuple[Doorlock, str], doorlock: Doorlock,
unadopted_doorlock: Doorlock,
): ):
"""Test lock entity unavailable.""" """Test lock entity unavailable."""
new_bootstrap = copy(mock_entry.api.bootstrap) await init_entry(hass, ufp, [doorlock, unadopted_doorlock])
new_lock = doorlock[0].copy() assert_entity_counts(hass, Platform.LOCK, 1, 1)
new_lock = doorlock.copy()
new_lock.lock_status = LockStatusType.NOT_CALIBRATED new_lock.lock_status = LockStatusType.NOT_CALIBRATED
mock_msg = Mock() mock_msg = Mock()
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_lock mock_msg.new_obj = new_lock
new_bootstrap.doorlocks = {new_lock.id: new_lock} ufp.api.bootstrap.doorlocks = {new_lock.id: new_lock}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(doorlock[1]) state = hass.states.get("lock.test_lock_lock")
assert state assert state
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
async def test_lock_do_lock( async def test_lock_do_lock(
hass: HomeAssistant, hass: HomeAssistant,
doorlock: tuple[Doorlock, str], ufp: MockUFPFixture,
doorlock: Doorlock,
unadopted_doorlock: Doorlock,
): ):
"""Test lock entity lock service.""" """Test lock entity lock service."""
doorlock[0].__fields__["close_lock"] = Mock() await init_entry(hass, ufp, [doorlock, unadopted_doorlock])
doorlock[0].close_lock = AsyncMock() assert_entity_counts(hass, Platform.LOCK, 1, 1)
doorlock.__fields__["close_lock"] = Mock()
doorlock.close_lock = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"lock", "lock",
"lock", "lock",
{ATTR_ENTITY_ID: doorlock[1]}, {ATTR_ENTITY_ID: "lock.test_lock_lock"},
blocking=True, blocking=True,
) )
doorlock[0].close_lock.assert_called_once() doorlock.close_lock.assert_called_once()
async def test_lock_do_unlock( async def test_lock_do_unlock(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
doorlock: tuple[Doorlock, str], doorlock: Doorlock,
unadopted_doorlock: Doorlock,
): ):
"""Test lock entity unlock service.""" """Test lock entity unlock service."""
new_bootstrap = copy(mock_entry.api.bootstrap) await init_entry(hass, ufp, [doorlock, unadopted_doorlock])
new_lock = doorlock[0].copy() assert_entity_counts(hass, Platform.LOCK, 1, 1)
new_lock = doorlock.copy()
new_lock.lock_status = LockStatusType.CLOSED new_lock.lock_status = LockStatusType.CLOSED
mock_msg = Mock() mock_msg = Mock()
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_lock mock_msg.new_obj = new_lock
new_bootstrap.doorlocks = {new_lock.id: new_lock} ufp.api.bootstrap.doorlocks = {new_lock.id: new_lock}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
new_lock.__fields__["open_lock"] = Mock() new_lock.__fields__["open_lock"] = Mock()
@ -252,7 +236,7 @@ async def test_lock_do_unlock(
await hass.services.async_call( await hass.services.async_call(
"lock", "lock",
"unlock", "unlock",
{ATTR_ENTITY_ID: doorlock[1]}, {ATTR_ENTITY_ID: "lock.test_lock_lock"},
blocking=True, blocking=True,
) )

View File

@ -2,7 +2,6 @@
# pylint: disable=protected-access # pylint: disable=protected-access
from __future__ import annotations from __future__ import annotations
from copy import copy
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
import pytest import pytest
@ -26,66 +25,29 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .conftest import MockEntityFixture, assert_entity_counts, regenerate_device_ids from .utils import MockUFPFixture, assert_entity_counts, init_entry
@pytest.fixture(name="camera")
async def camera_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera
):
"""Fixture for a single camera for testing the media_player platform."""
# disable pydantic validation so mocking can happen
Camera.__config__.validate_assignment = False
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.feature_flags.has_speaker = True
regenerate_device_ids(camera_obj)
no_camera_obj = mock_camera.copy()
no_camera_obj._api = mock_entry.api
no_camera_obj.channels[0]._api = mock_entry.api
no_camera_obj.channels[1]._api = mock_entry.api
no_camera_obj.channels[2]._api = mock_entry.api
no_camera_obj.name = "Unadopted Camera"
no_camera_obj.is_adopted = False
regenerate_device_ids(no_camera_obj)
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
no_camera_obj.id: no_camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1)
yield (camera_obj, "media_player.test_camera_speaker")
Camera.__config__.validate_assignment = True
async def test_media_player_setup( async def test_media_player_setup(
hass: HomeAssistant, hass: HomeAssistant,
camera: tuple[Camera, str], ufp: MockUFPFixture,
doorbell: Camera,
unadopted_camera: Camera,
): ):
"""Test media_player entity setup.""" """Test media_player entity setup."""
unique_id = f"{camera[0].mac}_speaker" await init_entry(hass, ufp, [doorbell, unadopted_camera])
entity_id = camera[1] assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1)
unique_id = f"{doorbell.mac}_speaker"
entity_id = "media_player.test_camera_speaker"
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
assert entity assert entity
assert entity.unique_id == unique_id assert entity.unique_id == unique_id
expected_volume = float(camera[0].speaker_settings.volume / 100) expected_volume = float(doorbell.speaker_settings.volume / 100)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
@ -98,13 +60,16 @@ async def test_media_player_setup(
async def test_media_player_update( async def test_media_player_update(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
camera: tuple[Camera, str], doorbell: Camera,
unadopted_camera: Camera,
): ):
"""Test media_player entity update.""" """Test media_player entity update."""
new_bootstrap = copy(mock_entry.api.bootstrap) await init_entry(hass, ufp, [doorbell, unadopted_camera])
new_camera = camera[0].copy() assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1)
new_camera = doorbell.copy()
new_camera.talkback_stream = Mock() new_camera.talkback_stream = Mock()
new_camera.talkback_stream.is_running = True new_camera.talkback_stream.is_running = True
@ -112,44 +77,51 @@ async def test_media_player_update(
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_camera mock_msg.new_obj = new_camera
new_bootstrap.cameras = {new_camera.id: new_camera} ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(camera[1]) state = hass.states.get("media_player.test_camera_speaker")
assert state assert state
assert state.state == STATE_PLAYING assert state.state == STATE_PLAYING
async def test_media_player_set_volume( async def test_media_player_set_volume(
hass: HomeAssistant, hass: HomeAssistant,
camera: tuple[Camera, str], ufp: MockUFPFixture,
doorbell: Camera,
unadopted_camera: Camera,
): ):
"""Test media_player entity test set_volume_level.""" """Test media_player entity test set_volume_level."""
camera[0].__fields__["set_speaker_volume"] = Mock() await init_entry(hass, ufp, [doorbell, unadopted_camera])
camera[0].set_speaker_volume = AsyncMock() assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1)
doorbell.__fields__["set_speaker_volume"] = Mock()
doorbell.set_speaker_volume = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"media_player", "media_player",
"volume_set", "volume_set",
{ATTR_ENTITY_ID: camera[1], "volume_level": 0.5}, {ATTR_ENTITY_ID: "media_player.test_camera_speaker", "volume_level": 0.5},
blocking=True, blocking=True,
) )
camera[0].set_speaker_volume.assert_called_once_with(50) doorbell.set_speaker_volume.assert_called_once_with(50)
async def test_media_player_stop( async def test_media_player_stop(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
camera: tuple[Camera, str], doorbell: Camera,
unadopted_camera: Camera,
): ):
"""Test media_player entity test media_stop.""" """Test media_player entity test media_stop."""
new_bootstrap = copy(mock_entry.api.bootstrap) await init_entry(hass, ufp, [doorbell, unadopted_camera])
new_camera = camera[0].copy() assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1)
new_camera = doorbell.copy()
new_camera.talkback_stream = AsyncMock() new_camera.talkback_stream = AsyncMock()
new_camera.talkback_stream.is_running = True new_camera.talkback_stream.is_running = True
@ -157,15 +129,14 @@ async def test_media_player_stop(
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_camera mock_msg.new_obj = new_camera
new_bootstrap.cameras = {new_camera.id: new_camera} ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.services.async_call( await hass.services.async_call(
"media_player", "media_player",
"media_stop", "media_stop",
{ATTR_ENTITY_ID: camera[1]}, {ATTR_ENTITY_ID: "media_player.test_camera_speaker"},
blocking=True, blocking=True,
) )
@ -174,44 +145,56 @@ async def test_media_player_stop(
async def test_media_player_play( async def test_media_player_play(
hass: HomeAssistant, hass: HomeAssistant,
camera: tuple[Camera, str], ufp: MockUFPFixture,
doorbell: Camera,
unadopted_camera: Camera,
): ):
"""Test media_player entity test play_media.""" """Test media_player entity test play_media."""
camera[0].__fields__["stop_audio"] = Mock()
camera[0].__fields__["play_audio"] = Mock() await init_entry(hass, ufp, [doorbell, unadopted_camera])
camera[0].__fields__["wait_until_audio_completes"] = Mock() assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1)
camera[0].stop_audio = AsyncMock()
camera[0].play_audio = AsyncMock() doorbell.__fields__["stop_audio"] = Mock()
camera[0].wait_until_audio_completes = AsyncMock() doorbell.__fields__["play_audio"] = Mock()
doorbell.__fields__["wait_until_audio_completes"] = Mock()
doorbell.stop_audio = AsyncMock()
doorbell.play_audio = AsyncMock()
doorbell.wait_until_audio_completes = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"media_player", "media_player",
"play_media", "play_media",
{ {
ATTR_ENTITY_ID: camera[1], ATTR_ENTITY_ID: "media_player.test_camera_speaker",
"media_content_id": "http://example.com/test.mp3", "media_content_id": "http://example.com/test.mp3",
"media_content_type": "music", "media_content_type": "music",
}, },
blocking=True, blocking=True,
) )
camera[0].play_audio.assert_called_once_with( doorbell.play_audio.assert_called_once_with(
"http://example.com/test.mp3", blocking=False "http://example.com/test.mp3", blocking=False
) )
camera[0].wait_until_audio_completes.assert_called_once() doorbell.wait_until_audio_completes.assert_called_once()
async def test_media_player_play_media_source( async def test_media_player_play_media_source(
hass: HomeAssistant, hass: HomeAssistant,
camera: tuple[Camera, str], ufp: MockUFPFixture,
doorbell: Camera,
unadopted_camera: Camera,
): ):
"""Test media_player entity test play_media.""" """Test media_player entity test play_media."""
camera[0].__fields__["stop_audio"] = Mock()
camera[0].__fields__["play_audio"] = Mock() await init_entry(hass, ufp, [doorbell, unadopted_camera])
camera[0].__fields__["wait_until_audio_completes"] = Mock() assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1)
camera[0].stop_audio = AsyncMock()
camera[0].play_audio = AsyncMock() doorbell.__fields__["stop_audio"] = Mock()
camera[0].wait_until_audio_completes = AsyncMock() doorbell.__fields__["play_audio"] = Mock()
doorbell.__fields__["wait_until_audio_completes"] = Mock()
doorbell.stop_audio = AsyncMock()
doorbell.play_audio = AsyncMock()
doorbell.wait_until_audio_completes = AsyncMock()
with patch( with patch(
"homeassistant.components.media_source.async_resolve_media", "homeassistant.components.media_source.async_resolve_media",
@ -221,65 +204,75 @@ async def test_media_player_play_media_source(
"media_player", "media_player",
"play_media", "play_media",
{ {
ATTR_ENTITY_ID: camera[1], ATTR_ENTITY_ID: "media_player.test_camera_speaker",
"media_content_id": "media-source://some_source/some_id", "media_content_id": "media-source://some_source/some_id",
"media_content_type": "audio/mpeg", "media_content_type": "audio/mpeg",
}, },
blocking=True, blocking=True,
) )
camera[0].play_audio.assert_called_once_with( doorbell.play_audio.assert_called_once_with(
"http://example.com/test.mp3", blocking=False "http://example.com/test.mp3", blocking=False
) )
camera[0].wait_until_audio_completes.assert_called_once() doorbell.wait_until_audio_completes.assert_called_once()
async def test_media_player_play_invalid( async def test_media_player_play_invalid(
hass: HomeAssistant, hass: HomeAssistant,
camera: tuple[Camera, str], ufp: MockUFPFixture,
doorbell: Camera,
unadopted_camera: Camera,
): ):
"""Test media_player entity test play_media, not music.""" """Test media_player entity test play_media, not music."""
camera[0].__fields__["play_audio"] = Mock() await init_entry(hass, ufp, [doorbell, unadopted_camera])
camera[0].play_audio = AsyncMock() assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1)
doorbell.__fields__["play_audio"] = Mock()
doorbell.play_audio = AsyncMock()
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
"media_player", "media_player",
"play_media", "play_media",
{ {
ATTR_ENTITY_ID: camera[1], ATTR_ENTITY_ID: "media_player.test_camera_speaker",
"media_content_id": "/test.png", "media_content_id": "/test.png",
"media_content_type": "image", "media_content_type": "image",
}, },
blocking=True, blocking=True,
) )
assert not camera[0].play_audio.called assert not doorbell.play_audio.called
async def test_media_player_play_error( async def test_media_player_play_error(
hass: HomeAssistant, hass: HomeAssistant,
camera: tuple[Camera, str], ufp: MockUFPFixture,
doorbell: Camera,
unadopted_camera: Camera,
): ):
"""Test media_player entity test play_media, not music.""" """Test media_player entity test play_media, not music."""
camera[0].__fields__["play_audio"] = Mock() await init_entry(hass, ufp, [doorbell, unadopted_camera])
camera[0].__fields__["wait_until_audio_completes"] = Mock() assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1)
camera[0].play_audio = AsyncMock(side_effect=StreamError)
camera[0].wait_until_audio_completes = AsyncMock() doorbell.__fields__["play_audio"] = Mock()
doorbell.__fields__["wait_until_audio_completes"] = Mock()
doorbell.play_audio = AsyncMock(side_effect=StreamError)
doorbell.wait_until_audio_completes = AsyncMock()
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
"media_player", "media_player",
"play_media", "play_media",
{ {
ATTR_ENTITY_ID: camera[1], ATTR_ENTITY_ID: "media_player.test_camera_speaker",
"media_content_id": "/test.mp3", "media_content_id": "/test.mp3",
"media_content_type": "music", "media_content_type": "music",
}, },
blocking=True, blocking=True,
) )
assert camera[0].play_audio.called assert doorbell.play_audio.called
assert not camera[0].wait_until_audio_completes.called assert not doorbell.wait_until_audio_completes.called

View File

@ -5,7 +5,6 @@ from __future__ import annotations
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from pyunifiprotect.data import Light from pyunifiprotect.data import Light
from pyunifiprotect.data.bootstrap import ProtectDeviceRef
from pyunifiprotect.exceptions import NvrError from pyunifiprotect.exceptions import NvrError
from homeassistant.components.unifiprotect.const import DOMAIN from homeassistant.components.unifiprotect.const import DOMAIN
@ -14,56 +13,47 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .conftest import MockEntityFixture, generate_random_ids, regenerate_device_ids from .utils import (
MockUFPFixture,
generate_random_ids,
init_entry,
regenerate_device_ids,
)
async def test_migrate_reboot_button( async def test_migrate_reboot_button(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light hass: HomeAssistant, ufp: MockUFPFixture, light: Light
): ):
"""Test migrating unique ID of reboot button.""" """Test migrating unique ID of reboot button."""
light1 = mock_light.copy() light1 = light.copy()
light1._api = mock_entry.api
light1.name = "Test Light 1" light1.name = "Test Light 1"
regenerate_device_ids(light1) regenerate_device_ids(light1)
light2 = mock_light.copy() light2 = light.copy()
light2._api = mock_entry.api
light2.name = "Test Light 2" light2.name = "Test Light 2"
regenerate_device_ids(light2) regenerate_device_ids(light2)
mock_entry.api.bootstrap.lights = {
light1.id: light1,
light2.id: light2,
}
mock_entry.api.bootstrap.id_lookup = {
light1.id: ProtectDeviceRef(id=light1.id, model=light1.model),
light2.id: ProtectDeviceRef(id=light2.id, model=light2.model),
}
mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap)
registry = er.async_get(hass) registry = er.async_get(hass)
registry.async_get_or_create( registry.async_get_or_create(
Platform.BUTTON, DOMAIN, light1.id, config_entry=mock_entry.entry Platform.BUTTON, DOMAIN, light1.id, config_entry=ufp.entry
) )
registry.async_get_or_create( registry.async_get_or_create(
Platform.BUTTON, Platform.BUTTON,
DOMAIN, DOMAIN,
f"{light2.mac}_reboot", f"{light2.mac}_reboot",
config_entry=mock_entry.entry, config_entry=ufp.entry,
) )
await hass.config_entries.async_setup(mock_entry.entry.entry_id) ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap)
await hass.async_block_till_done() await init_entry(hass, ufp, [light1, light2], regenerate_ids=False)
assert mock_entry.entry.state == ConfigEntryState.LOADED assert ufp.entry.state == ConfigEntryState.LOADED
assert mock_entry.api.update.called assert ufp.api.update.called
assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
buttons = [] buttons = []
for entity in er.async_entries_for_config_entry( for entity in er.async_entries_for_config_entry(registry, ufp.entry.entry_id):
registry, mock_entry.entry.entry_id
):
if entity.domain == Platform.BUTTON.value: if entity.domain == Platform.BUTTON.value:
buttons.append(entity) buttons.append(entity)
assert len(buttons) == 2 assert len(buttons) == 2
@ -83,29 +73,33 @@ async def test_migrate_reboot_button(
assert light.unique_id == f"{light2.mac}_reboot" assert light.unique_id == f"{light2.mac}_reboot"
async def test_migrate_nvr_mac( async def test_migrate_nvr_mac(hass: HomeAssistant, ufp: MockUFPFixture, light: Light):
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light
):
"""Test migrating unique ID of NVR to use MAC address.""" """Test migrating unique ID of NVR to use MAC address."""
mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) light1 = light.copy()
nvr = mock_entry.api.bootstrap.nvr light1.name = "Test Light 1"
regenerate_device_ids(nvr) regenerate_device_ids(light1)
light2 = light.copy()
light2.name = "Test Light 2"
regenerate_device_ids(light2)
nvr = ufp.api.bootstrap.nvr
regenerate_device_ids(nvr)
registry = er.async_get(hass) registry = er.async_get(hass)
registry.async_get_or_create( registry.async_get_or_create(
Platform.SENSOR, Platform.SENSOR,
DOMAIN, DOMAIN,
f"{nvr.id}_storage_utilization", f"{nvr.id}_storage_utilization",
config_entry=mock_entry.entry, config_entry=ufp.entry,
) )
await hass.config_entries.async_setup(mock_entry.entry.entry_id) ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap)
await hass.async_block_till_done() await init_entry(hass, ufp, [light1, light2], regenerate_ids=False)
assert mock_entry.entry.state == ConfigEntryState.LOADED assert ufp.entry.state == ConfigEntryState.LOADED
assert mock_entry.api.update.called assert ufp.api.update.called
assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
assert registry.async_get(f"{Platform.SENSOR}.{DOMAIN}_storage_utilization") is None assert registry.async_get(f"{Platform.SENSOR}.{DOMAIN}_storage_utilization") is None
assert ( assert (
@ -119,171 +113,123 @@ async def test_migrate_nvr_mac(
async def test_migrate_reboot_button_no_device( async def test_migrate_reboot_button_no_device(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light hass: HomeAssistant, ufp: MockUFPFixture, light: Light
): ):
"""Test migrating unique ID of reboot button if UniFi Protect device ID changed.""" """Test migrating unique ID of reboot button if UniFi Protect device ID changed."""
light1 = mock_light.copy()
light1._api = mock_entry.api
light1.name = "Test Light 1"
regenerate_device_ids(light1)
light2_id, _ = generate_random_ids() light2_id, _ = generate_random_ids()
mock_entry.api.bootstrap.lights = {
light1.id: light1,
}
mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap)
registry = er.async_get(hass) registry = er.async_get(hass)
registry.async_get_or_create( registry.async_get_or_create(
Platform.BUTTON, DOMAIN, light2_id, config_entry=mock_entry.entry Platform.BUTTON, DOMAIN, light2_id, config_entry=ufp.entry
) )
await hass.config_entries.async_setup(mock_entry.entry.entry_id) ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap)
await hass.async_block_till_done() await init_entry(hass, ufp, [light], regenerate_ids=False)
assert mock_entry.entry.state == ConfigEntryState.LOADED assert ufp.entry.state == ConfigEntryState.LOADED
assert mock_entry.api.update.called assert ufp.api.update.called
assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
buttons = [] buttons = []
for entity in er.async_entries_for_config_entry( for entity in er.async_entries_for_config_entry(registry, ufp.entry.entry_id):
registry, mock_entry.entry.entry_id
):
if entity.domain == Platform.BUTTON.value: if entity.domain == Platform.BUTTON.value:
buttons.append(entity) buttons.append(entity)
assert len(buttons) == 2 assert len(buttons) == 2
light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light2_id.lower()}") entity = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light2_id.lower()}")
assert light is not None assert entity is not None
assert light.unique_id == light2_id assert entity.unique_id == light2_id
async def test_migrate_reboot_button_fail( async def test_migrate_reboot_button_fail(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light hass: HomeAssistant, ufp: MockUFPFixture, light: Light
): ):
"""Test migrating unique ID of reboot button.""" """Test migrating unique ID of reboot button."""
light1 = mock_light.copy()
light1._api = mock_entry.api
light1.name = "Test Light 1"
regenerate_device_ids(light1)
mock_entry.api.bootstrap.lights = {
light1.id: light1,
}
mock_entry.api.bootstrap.id_lookup = {
light1.id: ProtectDeviceRef(id=light1.id, model=light1.model),
}
mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap)
registry = er.async_get(hass) registry = er.async_get(hass)
registry.async_get_or_create( registry.async_get_or_create(
Platform.BUTTON, Platform.BUTTON,
DOMAIN, DOMAIN,
light1.id, light.id,
config_entry=mock_entry.entry, config_entry=ufp.entry,
suggested_object_id=light1.name, suggested_object_id=light.display_name,
) )
registry.async_get_or_create( registry.async_get_or_create(
Platform.BUTTON, Platform.BUTTON,
DOMAIN, DOMAIN,
f"{light1.id}_reboot", f"{light.id}_reboot",
config_entry=mock_entry.entry, config_entry=ufp.entry,
suggested_object_id=light1.name, suggested_object_id=light.display_name,
) )
await hass.config_entries.async_setup(mock_entry.entry.entry_id) ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap)
await hass.async_block_till_done() await init_entry(hass, ufp, [light], regenerate_ids=False)
assert mock_entry.entry.state == ConfigEntryState.LOADED assert ufp.entry.state == ConfigEntryState.LOADED
assert mock_entry.api.update.called assert ufp.api.update.called
assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
light = registry.async_get(f"{Platform.BUTTON}.test_light_1") entity = registry.async_get(f"{Platform.BUTTON}.test_light")
assert light is not None assert entity is not None
assert light.unique_id == f"{light1.mac}" assert entity.unique_id == f"{light.mac}"
async def test_migrate_device_mac_button_fail( async def test_migrate_device_mac_button_fail(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light hass: HomeAssistant, ufp: MockUFPFixture, light: Light
): ):
"""Test migrating unique ID to MAC format.""" """Test migrating unique ID to MAC format."""
light1 = mock_light.copy()
light1._api = mock_entry.api
light1.name = "Test Light 1"
regenerate_device_ids(light1)
mock_entry.api.bootstrap.lights = {
light1.id: light1,
}
mock_entry.api.bootstrap.id_lookup = {
light1.id: ProtectDeviceRef(id=light1.id, model=light1.model)
}
mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap)
registry = er.async_get(hass) registry = er.async_get(hass)
registry.async_get_or_create( registry.async_get_or_create(
Platform.BUTTON, Platform.BUTTON,
DOMAIN, DOMAIN,
f"{light1.id}_reboot", f"{light.id}_reboot",
config_entry=mock_entry.entry, config_entry=ufp.entry,
suggested_object_id=light1.name, suggested_object_id=light.display_name,
) )
registry.async_get_or_create( registry.async_get_or_create(
Platform.BUTTON, Platform.BUTTON,
DOMAIN, DOMAIN,
f"{light1.mac}_reboot", f"{light.mac}_reboot",
config_entry=mock_entry.entry, config_entry=ufp.entry,
suggested_object_id=light1.name, suggested_object_id=light.display_name,
) )
await hass.config_entries.async_setup(mock_entry.entry.entry_id) ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap)
await hass.async_block_till_done() await init_entry(hass, ufp, [light], regenerate_ids=False)
assert mock_entry.entry.state == ConfigEntryState.LOADED assert ufp.entry.state == ConfigEntryState.LOADED
assert mock_entry.api.update.called assert ufp.api.update.called
assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac
light = registry.async_get(f"{Platform.BUTTON}.test_light_1") entity = registry.async_get(f"{Platform.BUTTON}.test_light")
assert light is not None assert entity is not None
assert light.unique_id == f"{light1.id}_reboot" assert entity.unique_id == f"{light.id}_reboot"
async def test_migrate_device_mac_bootstrap_fail( async def test_migrate_device_mac_bootstrap_fail(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light hass: HomeAssistant, ufp: MockUFPFixture, light: Light
): ):
"""Test migrating with a network error.""" """Test migrating with a network error."""
light1 = mock_light.copy()
light1._api = mock_entry.api
light1.name = "Test Light 1"
regenerate_device_ids(light1)
mock_entry.api.bootstrap.lights = {
light1.id: light1,
}
mock_entry.api.get_bootstrap = AsyncMock(side_effect=NvrError)
registry = er.async_get(hass) registry = er.async_get(hass)
registry.async_get_or_create( registry.async_get_or_create(
Platform.BUTTON, Platform.BUTTON,
DOMAIN, DOMAIN,
f"{light1.id}_reboot", f"{light.id}_reboot",
config_entry=mock_entry.entry, config_entry=ufp.entry,
suggested_object_id=light1.name, suggested_object_id=light.name,
) )
registry.async_get_or_create( registry.async_get_or_create(
Platform.BUTTON, Platform.BUTTON,
DOMAIN, DOMAIN,
f"{light1.mac}_reboot", f"{light.mac}_reboot",
config_entry=mock_entry.entry, config_entry=ufp.entry,
suggested_object_id=light1.name, suggested_object_id=light.name,
) )
await hass.config_entries.async_setup(mock_entry.entry.entry_id) ufp.api.get_bootstrap = AsyncMock(side_effect=NvrError)
await hass.async_block_till_done() await init_entry(hass, ufp, [light], regenerate_ids=False)
assert mock_entry.entry.state == ConfigEntryState.SETUP_RETRY assert ufp.entry.state == ConfigEntryState.SETUP_RETRY

View File

@ -19,119 +19,23 @@ from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .conftest import ( from .utils import (
MockEntityFixture, MockUFPFixture,
assert_entity_counts, assert_entity_counts,
ids_from_device_description, ids_from_device_description,
reset_objects, init_entry,
) )
@pytest.fixture(name="light")
async def light_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light
):
"""Fixture for a single light for testing the number platform."""
# disable pydantic validation so mocking can happen
Light.__config__.validate_assignment = False
light_obj = mock_light.copy()
light_obj._api = mock_entry.api
light_obj.name = "Test Light"
light_obj.light_device_settings.pir_sensitivity = 45
light_obj.light_device_settings.pir_duration = timedelta(seconds=45)
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.lights = {
light_obj.id: light_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.NUMBER, 2, 2)
yield light_obj
Light.__config__.validate_assignment = True
@pytest.fixture(name="camera")
async def camera_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera
):
"""Fixture for a single camera for testing the number platform."""
# disable pydantic validation so mocking can happen
Camera.__config__.validate_assignment = False
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.feature_flags.can_optical_zoom = True
camera_obj.feature_flags.has_mic = True
# has_wdr is an the inverse of has HDR
camera_obj.feature_flags.has_hdr = False
camera_obj.isp_settings.wdr = 0
camera_obj.mic_volume = 0
camera_obj.isp_settings.zoom_position = 0
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.NUMBER, 3, 3)
yield camera_obj
Camera.__config__.validate_assignment = True
@pytest.fixture(name="doorlock")
async def doorlock_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_doorlock: Doorlock
):
"""Fixture for a single doorlock for testing the number platform."""
# disable pydantic validation so mocking can happen
Doorlock.__config__.validate_assignment = False
lock_obj = mock_doorlock.copy()
lock_obj._api = mock_entry.api
lock_obj.name = "Test Lock"
lock_obj.auto_close_time = timedelta(seconds=45)
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.doorlocks = {
lock_obj.id: lock_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.NUMBER, 1, 1)
yield lock_obj
Doorlock.__config__.validate_assignment = True
async def test_number_setup_light( async def test_number_setup_light(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, light: Light
light: Light,
): ):
"""Test number entity setup for light devices.""" """Test number entity setup for light devices."""
entity_registry = er.async_get(hass) await init_entry(hass, ufp, [light])
assert_entity_counts(hass, Platform.NUMBER, 2, 2)
entity_registry = er.async_get(hass)
for description in LIGHT_NUMBERS: for description in LIGHT_NUMBERS:
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.NUMBER, light, description Platform.NUMBER, light, description
@ -148,11 +52,13 @@ async def test_number_setup_light(
async def test_number_setup_camera_all( async def test_number_setup_camera_all(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera
camera: Camera,
): ):
"""Test number entity setup for camera devices (all features).""" """Test number entity setup for camera devices (all features)."""
await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.NUMBER, 3, 3)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
for description in CAMERA_NUMBERS: for description in CAMERA_NUMBERS:
@ -171,64 +77,38 @@ async def test_number_setup_camera_all(
async def test_number_setup_camera_none( async def test_number_setup_camera_none(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera
): ):
"""Test number entity setup for camera devices (no features).""" """Test number entity setup for camera devices (no features)."""
camera_obj = mock_camera.copy() camera.feature_flags.can_optical_zoom = False
camera_obj._api = mock_entry.api camera.feature_flags.has_mic = False
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.feature_flags.can_optical_zoom = False
camera_obj.feature_flags.has_mic = False
# has_wdr is an the inverse of has HDR # has_wdr is an the inverse of has HDR
camera_obj.feature_flags.has_hdr = True camera.feature_flags.has_hdr = True
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.NUMBER, 0, 0) assert_entity_counts(hass, Platform.NUMBER, 0, 0)
async def test_number_setup_camera_missing_attr( async def test_number_setup_camera_missing_attr(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera
): ):
"""Test number entity setup for camera devices (no features, bad attrs).""" """Test number entity setup for camera devices (no features, bad attrs)."""
# disable pydantic validation so mocking can happen camera.feature_flags = None
Camera.__config__.validate_assignment = False
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.feature_flags = None
Camera.__config__.validate_assignment = True
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.NUMBER, 0, 0) assert_entity_counts(hass, Platform.NUMBER, 0, 0)
async def test_number_light_sensitivity(hass: HomeAssistant, light: Light): async def test_number_light_sensitivity(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
):
"""Test sensitivity number entity for lights.""" """Test sensitivity number entity for lights."""
await init_entry(hass, ufp, [light])
assert_entity_counts(hass, Platform.NUMBER, 2, 2)
description = LIGHT_NUMBERS[0] description = LIGHT_NUMBERS[0]
assert description.ufp_set_method is not None assert description.ufp_set_method is not None
@ -244,9 +124,14 @@ async def test_number_light_sensitivity(hass: HomeAssistant, light: Light):
light.set_sensitivity.assert_called_once_with(15.0) light.set_sensitivity.assert_called_once_with(15.0)
async def test_number_light_duration(hass: HomeAssistant, light: Light): async def test_number_light_duration(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
):
"""Test auto-shutoff duration number entity for lights.""" """Test auto-shutoff duration number entity for lights."""
await init_entry(hass, ufp, [light])
assert_entity_counts(hass, Platform.NUMBER, 2, 2)
description = LIGHT_NUMBERS[1] description = LIGHT_NUMBERS[1]
light.__fields__["set_duration"] = Mock() light.__fields__["set_duration"] = Mock()
@ -263,10 +148,16 @@ async def test_number_light_duration(hass: HomeAssistant, light: Light):
@pytest.mark.parametrize("description", CAMERA_NUMBERS) @pytest.mark.parametrize("description", CAMERA_NUMBERS)
async def test_number_camera_simple( async def test_number_camera_simple(
hass: HomeAssistant, camera: Camera, description: ProtectNumberEntityDescription hass: HomeAssistant,
ufp: MockUFPFixture,
camera: Camera,
description: ProtectNumberEntityDescription,
): ):
"""Tests all simple numbers for cameras.""" """Tests all simple numbers for cameras."""
await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.NUMBER, 3, 3)
assert description.ufp_set_method is not None assert description.ufp_set_method is not None
camera.__fields__[description.ufp_set_method] = Mock() camera.__fields__[description.ufp_set_method] = Mock()
@ -282,9 +173,14 @@ async def test_number_camera_simple(
set_method.assert_called_once_with(1.0) set_method.assert_called_once_with(1.0)
async def test_number_lock_auto_close(hass: HomeAssistant, doorlock: Doorlock): async def test_number_lock_auto_close(
hass: HomeAssistant, ufp: MockUFPFixture, doorlock: Doorlock
):
"""Test auto-lock timeout for locks.""" """Test auto-lock timeout for locks."""
await init_entry(hass, ufp, [doorlock])
assert_entity_counts(hass, Platform.NUMBER, 1, 1)
description = DOORLOCK_NUMBERS[0] description = DOORLOCK_NUMBERS[0]
doorlock.__fields__["set_auto_close_time"] = Mock() doorlock.__fields__["set_auto_close_time"] = Mock()

View File

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from copy import copy from copy import copy
from datetime import timedelta from datetime import datetime, timedelta
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
import pytest import pytest
@ -38,162 +38,24 @@ from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, ATTR_OPTION, P
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util.dt import utcnow
from .conftest import ( from .utils import (
MockEntityFixture, MockUFPFixture,
assert_entity_counts, assert_entity_counts,
ids_from_device_description, ids_from_device_description,
reset_objects, init_entry,
) )
@pytest.fixture(name="viewer")
async def viewer_fixture(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
mock_viewer: Viewer,
mock_liveview: Liveview,
):
"""Fixture for a single viewport for testing the select platform."""
# disable pydantic validation so mocking can happen
Viewer.__config__.validate_assignment = False
viewer_obj = mock_viewer.copy()
viewer_obj._api = mock_entry.api
viewer_obj.name = "Test Viewer"
viewer_obj.liveview_id = mock_liveview.id
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.viewers = {
viewer_obj.id: viewer_obj,
}
mock_entry.api.bootstrap.liveviews = {mock_liveview.id: mock_liveview}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.SELECT, 1, 1)
yield viewer_obj
Viewer.__config__.validate_assignment = True
@pytest.fixture(name="camera")
async def camera_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera
):
"""Fixture for a single camera for testing the select platform."""
# disable pydantic validation so mocking can happen
Camera.__config__.validate_assignment = False
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.feature_flags.has_lcd_screen = True
camera_obj.feature_flags.has_chime = True
camera_obj.recording_settings.mode = RecordingMode.ALWAYS
camera_obj.isp_settings.ir_led_mode = IRLEDMode.AUTO
camera_obj.lcd_message = None
camera_obj.chime_duration = 0
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.SELECT, 4, 4)
yield camera_obj
Camera.__config__.validate_assignment = True
@pytest.fixture(name="light")
async def light_fixture(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
mock_light: Light,
camera: Camera,
):
"""Fixture for a single light for testing the select platform."""
# disable pydantic validation so mocking can happen
Light.__config__.validate_assignment = False
light_obj = mock_light.copy()
light_obj._api = mock_entry.api
light_obj.name = "Test Light"
light_obj.camera_id = None
light_obj.light_mode_settings.mode = LightModeType.MOTION
light_obj.light_mode_settings.enable_at = LightModeEnableType.DARK
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.cameras = {camera.id: camera}
mock_entry.api.bootstrap.lights = {
light_obj.id: light_obj,
}
await hass.config_entries.async_reload(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.SELECT, 6, 6)
yield light_obj
Light.__config__.validate_assignment = True
@pytest.fixture(name="camera_none")
async def camera_none_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera
):
"""Fixture for a single camera for testing the select platform."""
# disable pydantic validation so mocking can happen
Camera.__config__.validate_assignment = False
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.feature_flags.has_lcd_screen = False
camera_obj.feature_flags.has_chime = False
camera_obj.recording_settings.mode = RecordingMode.ALWAYS
camera_obj.isp_settings.ir_led_mode = IRLEDMode.AUTO
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.SELECT, 2, 2)
yield camera_obj
Camera.__config__.validate_assignment = True
async def test_select_setup_light( async def test_select_setup_light(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, light: Light
light: Light,
): ):
"""Test select entity setup for light devices.""" """Test select entity setup for light devices."""
light.light_mode_settings.enable_at = LightModeEnableType.DARK
await init_entry(hass, ufp, [light])
assert_entity_counts(hass, Platform.SELECT, 2, 2)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
expected_values = ("On Motion - When Dark", "Not Paired") expected_values = ("On Motion - When Dark", "Not Paired")
@ -213,11 +75,14 @@ async def test_select_setup_light(
async def test_select_setup_viewer( async def test_select_setup_viewer(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, viewer: Viewer, liveview: Liveview
viewer: Viewer,
): ):
"""Test select entity setup for light devices.""" """Test select entity setup for light devices."""
ufp.api.bootstrap.liveviews = {liveview.id: liveview}
await init_entry(hass, ufp, [viewer])
assert_entity_counts(hass, Platform.SELECT, 1, 1)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
description = VIEWER_SELECTS[0] description = VIEWER_SELECTS[0]
@ -236,15 +101,46 @@ async def test_select_setup_viewer(
async def test_select_setup_camera_all( async def test_select_setup_camera_all(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
camera: Camera,
): ):
"""Test select entity setup for camera devices (all features).""" """Test select entity setup for camera devices (all features)."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SELECT, 4, 4)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
expected_values = ("Always", "Auto", "Default Message (Welcome)", "None") expected_values = ("Always", "Auto", "Default Message (Welcome)", "None")
for index, description in enumerate(CAMERA_SELECTS): for index, description in enumerate(CAMERA_SELECTS):
unique_id, entity_id = ids_from_device_description(
Platform.SELECT, doorbell, description
)
entity = entity_registry.async_get(entity_id)
assert entity
assert entity.unique_id == unique_id
state = hass.states.get(entity_id)
assert state
assert state.state == expected_values[index]
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_select_setup_camera_none(
hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera
):
"""Test select entity setup for camera devices (no features)."""
await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.SELECT, 2, 2)
entity_registry = er.async_get(hass)
expected_values = ("Always", "Auto", "Default Message (Welcome)")
for index, description in enumerate(CAMERA_SELECTS):
if index == 2:
return
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.SELECT, camera, description Platform.SELECT, camera, description
) )
@ -259,41 +155,15 @@ async def test_select_setup_camera_all(
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_select_setup_camera_none(
hass: HomeAssistant,
camera_none: Camera,
):
"""Test select entity setup for camera devices (no features)."""
entity_registry = er.async_get(hass)
expected_values = ("Always", "Auto", "Default Message (Welcome)")
for index, description in enumerate(CAMERA_SELECTS):
if index == 2:
return
unique_id, entity_id = ids_from_device_description(
Platform.SELECT, camera_none, description
)
entity = entity_registry.async_get(entity_id)
assert entity
assert entity.unique_id == unique_id
state = hass.states.get(entity_id)
assert state
assert state.state == expected_values[index]
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_select_update_liveview( async def test_select_update_liveview(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, viewer: Viewer, liveview: Liveview
mock_entry: MockEntityFixture,
viewer: Viewer,
mock_liveview: Liveview,
): ):
"""Test select entity update (new Liveview).""" """Test select entity update (new Liveview)."""
ufp.api.bootstrap.liveviews = {liveview.id: liveview}
await init_entry(hass, ufp, [viewer])
assert_entity_counts(hass, Platform.SELECT, 1, 1)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SELECT, viewer, VIEWER_SELECTS[0] Platform.SELECT, viewer, VIEWER_SELECTS[0]
) )
@ -302,17 +172,18 @@ async def test_select_update_liveview(
assert state assert state
expected_options = state.attributes[ATTR_OPTIONS] expected_options = state.attributes[ATTR_OPTIONS]
new_bootstrap = copy(mock_entry.api.bootstrap) new_liveview = copy(liveview)
new_liveview = copy(mock_liveview)
new_liveview.id = "test_id" new_liveview.id = "test_id"
mock_msg = Mock() mock_msg = Mock()
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_liveview mock_msg.new_obj = new_liveview
new_bootstrap.liveviews = {**new_bootstrap.liveviews, new_liveview.id: new_liveview} ufp.api.bootstrap.liveviews = {
mock_entry.api.bootstrap = new_bootstrap **ufp.api.bootstrap.liveviews,
mock_entry.api.ws_subscription(mock_msg) new_liveview.id: new_liveview,
}
ufp.ws_msg(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
@ -321,16 +192,17 @@ async def test_select_update_liveview(
async def test_select_update_doorbell_settings( async def test_select_update_doorbell_settings(
hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
): ):
"""Test select entity update (new Doorbell Message).""" """Test select entity update (new Doorbell Message)."""
expected_length = ( await init_entry(hass, ufp, [doorbell])
len(mock_entry.api.bootstrap.nvr.doorbell_settings.all_messages) + 1 assert_entity_counts(hass, Platform.SELECT, 4, 4)
)
expected_length = len(ufp.api.bootstrap.nvr.doorbell_settings.all_messages) + 1
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SELECT, camera, CAMERA_SELECTS[2] Platform.SELECT, doorbell, CAMERA_SELECTS[2]
) )
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
@ -338,7 +210,7 @@ async def test_select_update_doorbell_settings(
assert len(state.attributes[ATTR_OPTIONS]) == expected_length assert len(state.attributes[ATTR_OPTIONS]) == expected_length
expected_length += 1 expected_length += 1
new_nvr = copy(mock_entry.api.bootstrap.nvr) new_nvr = copy(ufp.api.bootstrap.nvr)
new_nvr.__fields__["update_all_messages"] = Mock() new_nvr.__fields__["update_all_messages"] = Mock()
new_nvr.update_all_messages = Mock() new_nvr.update_all_messages = Mock()
@ -354,8 +226,8 @@ async def test_select_update_doorbell_settings(
mock_msg.changed_data = {"doorbell_settings": {}} mock_msg.changed_data = {"doorbell_settings": {}}
mock_msg.new_obj = new_nvr mock_msg.new_obj = new_nvr
mock_entry.api.bootstrap.nvr = new_nvr ufp.api.bootstrap.nvr = new_nvr
mock_entry.api.ws_subscription(mock_msg) ufp.ws_msg(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
new_nvr.update_all_messages.assert_called_once() new_nvr.update_all_messages.assert_called_once()
@ -366,22 +238,22 @@ async def test_select_update_doorbell_settings(
async def test_select_update_doorbell_message( async def test_select_update_doorbell_message(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
mock_entry: MockEntityFixture,
camera: Camera,
): ):
"""Test select entity update (change doorbell message).""" """Test select entity update (change doorbell message)."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SELECT, 4, 4)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SELECT, camera, CAMERA_SELECTS[2] Platform.SELECT, doorbell, CAMERA_SELECTS[2]
) )
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.state == "Default Message (Welcome)" assert state.state == "Default Message (Welcome)"
new_bootstrap = copy(mock_entry.api.bootstrap) new_camera = doorbell.copy()
new_camera = camera.copy()
new_camera.lcd_message = LCDMessage( new_camera.lcd_message = LCDMessage(
type=DoorbellMessageType.CUSTOM_MESSAGE, text="Test" type=DoorbellMessageType.CUSTOM_MESSAGE, text="Test"
) )
@ -390,9 +262,8 @@ async def test_select_update_doorbell_message(
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = new_camera mock_msg.new_obj = new_camera
new_bootstrap.cameras = {new_camera.id: new_camera} ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
@ -401,10 +272,13 @@ async def test_select_update_doorbell_message(
async def test_select_set_option_light_motion( async def test_select_set_option_light_motion(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, light: Light
light: Light,
): ):
"""Test Light Mode select.""" """Test Light Mode select."""
await init_entry(hass, ufp, [light])
assert_entity_counts(hass, Platform.SELECT, 2, 2)
_, entity_id = ids_from_device_description(Platform.SELECT, light, LIGHT_SELECTS[0]) _, entity_id = ids_from_device_description(Platform.SELECT, light, LIGHT_SELECTS[0])
light.__fields__["set_light_settings"] = Mock() light.__fields__["set_light_settings"] = Mock()
@ -423,10 +297,13 @@ async def test_select_set_option_light_motion(
async def test_select_set_option_light_camera( async def test_select_set_option_light_camera(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, light: Light, camera: Camera
light: Light,
): ):
"""Test Paired Camera select.""" """Test Paired Camera select."""
await init_entry(hass, ufp, [light, camera])
assert_entity_counts(hass, Platform.SELECT, 4, 4)
_, entity_id = ids_from_device_description(Platform.SELECT, light, LIGHT_SELECTS[1]) _, entity_id = ids_from_device_description(Platform.SELECT, light, LIGHT_SELECTS[1])
light.__fields__["set_paired_camera"] = Mock() light.__fields__["set_paired_camera"] = Mock()
@ -454,16 +331,19 @@ async def test_select_set_option_light_camera(
async def test_select_set_option_camera_recording( async def test_select_set_option_camera_recording(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
camera: Camera,
): ):
"""Test Recording Mode select.""" """Test Recording Mode select."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SELECT, 4, 4)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SELECT, camera, CAMERA_SELECTS[0] Platform.SELECT, doorbell, CAMERA_SELECTS[0]
) )
camera.__fields__["set_recording_mode"] = Mock() doorbell.__fields__["set_recording_mode"] = Mock()
camera.set_recording_mode = AsyncMock() doorbell.set_recording_mode = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"select", "select",
@ -472,20 +352,23 @@ async def test_select_set_option_camera_recording(
blocking=True, blocking=True,
) )
camera.set_recording_mode.assert_called_once_with(RecordingMode.NEVER) doorbell.set_recording_mode.assert_called_once_with(RecordingMode.NEVER)
async def test_select_set_option_camera_ir( async def test_select_set_option_camera_ir(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
camera: Camera,
): ):
"""Test Infrared Mode select.""" """Test Infrared Mode select."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SELECT, 4, 4)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SELECT, camera, CAMERA_SELECTS[1] Platform.SELECT, doorbell, CAMERA_SELECTS[1]
) )
camera.__fields__["set_ir_led_model"] = Mock() doorbell.__fields__["set_ir_led_model"] = Mock()
camera.set_ir_led_model = AsyncMock() doorbell.set_ir_led_model = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"select", "select",
@ -494,20 +377,23 @@ async def test_select_set_option_camera_ir(
blocking=True, blocking=True,
) )
camera.set_ir_led_model.assert_called_once_with(IRLEDMode.ON) doorbell.set_ir_led_model.assert_called_once_with(IRLEDMode.ON)
async def test_select_set_option_camera_doorbell_custom( async def test_select_set_option_camera_doorbell_custom(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
camera: Camera,
): ):
"""Test Doorbell Text select (user defined message).""" """Test Doorbell Text select (user defined message)."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SELECT, 4, 4)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SELECT, camera, CAMERA_SELECTS[2] Platform.SELECT, doorbell, CAMERA_SELECTS[2]
) )
camera.__fields__["set_lcd_text"] = Mock() doorbell.__fields__["set_lcd_text"] = Mock()
camera.set_lcd_text = AsyncMock() doorbell.set_lcd_text = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"select", "select",
@ -516,22 +402,25 @@ async def test_select_set_option_camera_doorbell_custom(
blocking=True, blocking=True,
) )
camera.set_lcd_text.assert_called_once_with( doorbell.set_lcd_text.assert_called_once_with(
DoorbellMessageType.CUSTOM_MESSAGE, text="Test" DoorbellMessageType.CUSTOM_MESSAGE, text="Test"
) )
async def test_select_set_option_camera_doorbell_unifi( async def test_select_set_option_camera_doorbell_unifi(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
camera: Camera,
): ):
"""Test Doorbell Text select (unifi message).""" """Test Doorbell Text select (unifi message)."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SELECT, 4, 4)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SELECT, camera, CAMERA_SELECTS[2] Platform.SELECT, doorbell, CAMERA_SELECTS[2]
) )
camera.__fields__["set_lcd_text"] = Mock() doorbell.__fields__["set_lcd_text"] = Mock()
camera.set_lcd_text = AsyncMock() doorbell.set_lcd_text = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"select", "select",
@ -543,7 +432,7 @@ async def test_select_set_option_camera_doorbell_unifi(
blocking=True, blocking=True,
) )
camera.set_lcd_text.assert_called_once_with( doorbell.set_lcd_text.assert_called_once_with(
DoorbellMessageType.LEAVE_PACKAGE_AT_DOOR DoorbellMessageType.LEAVE_PACKAGE_AT_DOOR
) )
@ -557,20 +446,23 @@ async def test_select_set_option_camera_doorbell_unifi(
blocking=True, blocking=True,
) )
camera.set_lcd_text.assert_called_with(None) doorbell.set_lcd_text.assert_called_with(None)
async def test_select_set_option_camera_doorbell_default( async def test_select_set_option_camera_doorbell_default(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
camera: Camera,
): ):
"""Test Doorbell Text select (default message).""" """Test Doorbell Text select (default message)."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SELECT, 4, 4)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SELECT, camera, CAMERA_SELECTS[2] Platform.SELECT, doorbell, CAMERA_SELECTS[2]
) )
camera.__fields__["set_lcd_text"] = Mock() doorbell.__fields__["set_lcd_text"] = Mock()
camera.set_lcd_text = AsyncMock() doorbell.set_lcd_text = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"select", "select",
@ -582,14 +474,18 @@ async def test_select_set_option_camera_doorbell_default(
blocking=True, blocking=True,
) )
camera.set_lcd_text.assert_called_once_with(None) doorbell.set_lcd_text.assert_called_once_with(None)
async def test_select_set_option_viewer( async def test_select_set_option_viewer(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, viewer: Viewer, liveview: Liveview
viewer: Viewer,
): ):
"""Test Liveview select.""" """Test Liveview select."""
ufp.api.bootstrap.liveviews = {liveview.id: liveview}
await init_entry(hass, ufp, [viewer])
assert_entity_counts(hass, Platform.SELECT, 1, 1)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SELECT, viewer, VIEWER_SELECTS[0] Platform.SELECT, viewer, VIEWER_SELECTS[0]
) )
@ -610,16 +506,19 @@ async def test_select_set_option_viewer(
async def test_select_service_doorbell_invalid( async def test_select_service_doorbell_invalid(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
camera: Camera,
): ):
"""Test Doorbell Text service (invalid).""" """Test Doorbell Text service (invalid)."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SELECT, 4, 4)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SELECT, camera, CAMERA_SELECTS[1] Platform.SELECT, doorbell, CAMERA_SELECTS[1]
) )
camera.__fields__["set_lcd_text"] = Mock() doorbell.__fields__["set_lcd_text"] = Mock()
camera.set_lcd_text = AsyncMock() doorbell.set_lcd_text = AsyncMock()
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
@ -629,20 +528,23 @@ async def test_select_service_doorbell_invalid(
blocking=True, blocking=True,
) )
assert not camera.set_lcd_text.called assert not doorbell.set_lcd_text.called
async def test_select_service_doorbell_success( async def test_select_service_doorbell_success(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
camera: Camera,
): ):
"""Test Doorbell Text service (success).""" """Test Doorbell Text service (success)."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SELECT, 4, 4)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SELECT, camera, CAMERA_SELECTS[2] Platform.SELECT, doorbell, CAMERA_SELECTS[2]
) )
camera.__fields__["set_lcd_text"] = Mock() doorbell.__fields__["set_lcd_text"] = Mock()
camera.set_lcd_text = AsyncMock() doorbell.set_lcd_text = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"unifiprotect", "unifiprotect",
@ -654,7 +556,7 @@ async def test_select_service_doorbell_success(
blocking=True, blocking=True,
) )
camera.set_lcd_text.assert_called_once_with( doorbell.set_lcd_text.assert_called_once_with(
DoorbellMessageType.CUSTOM_MESSAGE, "Test", reset_at=None DoorbellMessageType.CUSTOM_MESSAGE, "Test", reset_at=None
) )
@ -663,18 +565,23 @@ async def test_select_service_doorbell_success(
async def test_select_service_doorbell_with_reset( async def test_select_service_doorbell_with_reset(
mock_now, mock_now,
hass: HomeAssistant, hass: HomeAssistant,
camera: Camera, ufp: MockUFPFixture,
doorbell: Camera,
fixed_now: datetime,
): ):
"""Test Doorbell Text service (success with reset time).""" """Test Doorbell Text service (success with reset time)."""
now = utcnow()
mock_now.return_value = now mock_now.return_value = fixed_now
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SELECT, camera, CAMERA_SELECTS[2] Platform.SELECT, doorbell, CAMERA_SELECTS[2]
) )
camera.__fields__["set_lcd_text"] = Mock() await init_entry(hass, ufp, [doorbell])
camera.set_lcd_text = AsyncMock() assert_entity_counts(hass, Platform.SELECT, 4, 4)
doorbell.__fields__["set_lcd_text"] = Mock()
doorbell.set_lcd_text = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
"unifiprotect", "unifiprotect",
@ -687,8 +594,8 @@ async def test_select_service_doorbell_with_reset(
blocking=True, blocking=True,
) )
camera.set_lcd_text.assert_called_once_with( doorbell.set_lcd_text.assert_called_once_with(
DoorbellMessageType.CUSTOM_MESSAGE, DoorbellMessageType.CUSTOM_MESSAGE,
"Test", "Test",
reset_at=now + timedelta(minutes=60), reset_at=fixed_now + timedelta(minutes=60),
) )

View File

@ -2,11 +2,9 @@
# pylint: disable=protected-access # pylint: disable=protected-access
from __future__ import annotations from __future__ import annotations
from copy import copy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import AsyncMock, Mock from unittest.mock import AsyncMock, Mock
import pytest
from pyunifiprotect.data import ( from pyunifiprotect.data import (
NVR, NVR,
Camera, Camera,
@ -15,7 +13,6 @@ from pyunifiprotect.data import (
Sensor, Sensor,
SmartDetectObjectType, SmartDetectObjectType,
) )
from pyunifiprotect.data.base import WifiConnectionState, WiredConnectionState
from pyunifiprotect.data.nvr import EventMetadata from pyunifiprotect.data.nvr import EventMetadata
from homeassistant.components.unifiprotect.const import ( from homeassistant.components.unifiprotect.const import (
@ -42,11 +39,12 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .conftest import ( from .utils import (
MockEntityFixture, MockUFPFixture,
assert_entity_counts, assert_entity_counts,
enable_entity, enable_entity,
ids_from_device_description, ids_from_device_description,
init_entry,
reset_objects, reset_objects,
time_changed, time_changed,
) )
@ -55,136 +53,12 @@ CAMERA_SENSORS_WRITE = CAMERA_SENSORS[:5]
SENSE_SENSORS_WRITE = SENSE_SENSORS[:8] SENSE_SENSORS_WRITE = SENSE_SENSORS[:8]
@pytest.fixture(name="sensor")
async def sensor_fixture(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
mock_sensor: Sensor,
now: datetime,
):
"""Fixture for a single sensor for testing the sensor platform."""
# disable pydantic validation so mocking can happen
Sensor.__config__.validate_assignment = False
sensor_obj = mock_sensor.copy()
sensor_obj._api = mock_entry.api
sensor_obj.name = "Test Sensor"
sensor_obj.battery_status.percentage = 10.0
sensor_obj.light_settings.is_enabled = True
sensor_obj.humidity_settings.is_enabled = True
sensor_obj.temperature_settings.is_enabled = True
sensor_obj.alarm_settings.is_enabled = True
sensor_obj.stats.light.value = 10.0
sensor_obj.stats.humidity.value = 10.0
sensor_obj.stats.temperature.value = 10.0
sensor_obj.up_since = now
sensor_obj.bluetooth_connection_state.signal_strength = -50.0
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.sensors = {
sensor_obj.id: sensor_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
yield sensor_obj
Sensor.__config__.validate_assignment = True
@pytest.fixture(name="sensor_none")
async def sensor_none_fixture(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
mock_sensor: Sensor,
now: datetime,
):
"""Fixture for a single sensor for testing the sensor platform."""
# disable pydantic validation so mocking can happen
Sensor.__config__.validate_assignment = False
sensor_obj = mock_sensor.copy()
sensor_obj._api = mock_entry.api
sensor_obj.name = "Test Sensor"
sensor_obj.battery_status.percentage = 10.0
sensor_obj.light_settings.is_enabled = False
sensor_obj.humidity_settings.is_enabled = False
sensor_obj.temperature_settings.is_enabled = False
sensor_obj.alarm_settings.is_enabled = False
sensor_obj.up_since = now
sensor_obj.bluetooth_connection_state.signal_strength = -50.0
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.sensors = {
sensor_obj.id: sensor_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
# 4 from all, 5 from sense, 12 NVR
assert_entity_counts(hass, Platform.SENSOR, 22, 14)
yield sensor_obj
Sensor.__config__.validate_assignment = True
@pytest.fixture(name="camera")
async def camera_fixture(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
mock_camera: Camera,
now: datetime,
):
"""Fixture for a single camera for testing the sensor platform."""
# disable pydantic validation so mocking can happen
Camera.__config__.validate_assignment = False
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.feature_flags.has_smart_detect = True
camera_obj.feature_flags.has_chime = True
camera_obj.is_smart_detected = False
camera_obj.wired_connection_state = WiredConnectionState(phy_rate=1000)
camera_obj.wifi_connection_state = WifiConnectionState(
signal_quality=100, signal_strength=-50
)
camera_obj.stats.rx_bytes = 100.0
camera_obj.stats.tx_bytes = 100.0
camera_obj.stats.video.recording_start = now
camera_obj.stats.storage.used = 100.0
camera_obj.stats.storage.used = 100.0
camera_obj.stats.storage.rate = 0.1
camera_obj.voltage = 20.0
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
yield camera_obj
Camera.__config__.validate_assignment = True
async def test_sensor_setup_sensor( async def test_sensor_setup_sensor(
hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor
): ):
"""Test sensor entity setup for sensor devices.""" """Test sensor entity setup for sensor devices."""
# 5 from all, 5 from sense, 12 NVR
await init_entry(hass, ufp, [sensor_all])
assert_entity_counts(hass, Platform.SENSOR, 22, 14) assert_entity_counts(hass, Platform.SENSOR, 22, 14)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@ -196,6 +70,57 @@ async def test_sensor_setup_sensor(
"10.0", "10.0",
"none", "none",
) )
for index, description in enumerate(SENSE_SENSORS_WRITE):
if not description.entity_registry_enabled_default:
continue
unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, sensor_all, description
)
entity = entity_registry.async_get(entity_id)
assert entity
assert entity.unique_id == unique_id
state = hass.states.get(entity_id)
assert state
assert state.state == expected_values[index]
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
# BLE signal
unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, sensor_all, ALL_DEVICES_SENSORS[1]
)
entity = entity_registry.async_get(entity_id)
assert entity
assert entity.disabled is True
assert entity.unique_id == unique_id
await enable_entity(hass, ufp.entry.entry_id, entity_id)
state = hass.states.get(entity_id)
assert state
assert state.state == "-50"
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_sensor_setup_sensor_none(
hass: HomeAssistant, ufp: MockUFPFixture, sensor: Sensor
):
"""Test sensor entity setup for sensor devices with no sensors enabled."""
await init_entry(hass, ufp, [sensor])
assert_entity_counts(hass, Platform.SENSOR, 22, 14)
entity_registry = er.async_get(hass)
expected_values = (
"10",
STATE_UNAVAILABLE,
STATE_UNAVAILABLE,
STATE_UNAVAILABLE,
STATE_UNAVAILABLE,
)
for index, description in enumerate(SENSE_SENSORS_WRITE): for index, description in enumerate(SENSE_SENSORS_WRITE):
if not description.entity_registry_enabled_default: if not description.entity_registry_enabled_default:
continue continue
@ -212,63 +137,15 @@ async def test_sensor_setup_sensor(
assert state.state == expected_values[index] assert state.state == expected_values[index]
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
# BLE signal
unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, sensor, ALL_DEVICES_SENSORS[1]
)
entity = entity_registry.async_get(entity_id)
assert entity
assert entity.disabled is True
assert entity.unique_id == unique_id
await enable_entity(hass, mock_entry.entry.entry_id, entity_id)
state = hass.states.get(entity_id)
assert state
assert state.state == "-50"
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_sensor_setup_sensor_none(
hass: HomeAssistant, mock_entry: MockEntityFixture, sensor_none: Sensor
):
"""Test sensor entity setup for sensor devices with no sensors enabled."""
entity_registry = er.async_get(hass)
expected_values = (
"10",
STATE_UNAVAILABLE,
STATE_UNAVAILABLE,
STATE_UNAVAILABLE,
STATE_UNAVAILABLE,
)
for index, description in enumerate(SENSE_SENSORS_WRITE):
if not description.entity_registry_enabled_default:
continue
unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, sensor_none, description
)
entity = entity_registry.async_get(entity_id)
assert entity
assert entity.unique_id == unique_id
state = hass.states.get(entity_id)
assert state
assert state.state == expected_values[index]
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_sensor_setup_nvr( async def test_sensor_setup_nvr(
hass: HomeAssistant, mock_entry: MockEntityFixture, now: datetime hass: HomeAssistant, ufp: MockUFPFixture, fixed_now: datetime
): ):
"""Test sensor entity setup for NVR device.""" """Test sensor entity setup for NVR device."""
reset_objects(mock_entry.api.bootstrap) reset_objects(ufp.api.bootstrap)
nvr: NVR = mock_entry.api.bootstrap.nvr nvr: NVR = ufp.api.bootstrap.nvr
nvr.up_since = now nvr.up_since = fixed_now
nvr.system_info.cpu.average_load = 50.0 nvr.system_info.cpu.average_load = 50.0
nvr.system_info.cpu.temperature = 50.0 nvr.system_info.cpu.temperature = 50.0
nvr.storage_stats.utilization = 50.0 nvr.storage_stats.utilization = 50.0
@ -282,16 +159,15 @@ async def test_sensor_setup_nvr(
nvr.storage_stats.storage_distribution.free.percentage = 50.0 nvr.storage_stats.storage_distribution.free.percentage = 50.0
nvr.storage_stats.capacity = 50.0 nvr.storage_stats.capacity = 50.0
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
# 2 from all, 4 from sense, 12 NVR
assert_entity_counts(hass, Platform.SENSOR, 12, 9) assert_entity_counts(hass, Platform.SENSOR, 12, 9)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
expected_values = ( expected_values = (
now.replace(second=0, microsecond=0).isoformat(), fixed_now.replace(second=0, microsecond=0).isoformat(),
"50.0", "50.0",
"50.0", "50.0",
"50.0", "50.0",
@ -312,7 +188,7 @@ async def test_sensor_setup_nvr(
assert entity.unique_id == unique_id assert entity.unique_id == unique_id
if not description.entity_registry_enabled_default: if not description.entity_registry_enabled_default:
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
@ -330,7 +206,7 @@ async def test_sensor_setup_nvr(
assert entity.disabled is not description.entity_registry_enabled_default assert entity.disabled is not description.entity_registry_enabled_default
assert entity.unique_id == unique_id assert entity.unique_id == unique_id
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
@ -338,22 +214,19 @@ async def test_sensor_setup_nvr(
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_sensor_nvr_missing_values( async def test_sensor_nvr_missing_values(hass: HomeAssistant, ufp: MockUFPFixture):
hass: HomeAssistant, mock_entry: MockEntityFixture, now: datetime
):
"""Test NVR sensor sensors if no data available.""" """Test NVR sensor sensors if no data available."""
reset_objects(mock_entry.api.bootstrap) reset_objects(ufp.api.bootstrap)
nvr: NVR = mock_entry.api.bootstrap.nvr nvr: NVR = ufp.api.bootstrap.nvr
nvr.system_info.memory.available = None nvr.system_info.memory.available = None
nvr.system_info.memory.total = None nvr.system_info.memory.total = None
nvr.up_since = None nvr.up_since = None
nvr.storage_stats.capacity = None nvr.storage_stats.capacity = None
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
# 2 from all, 4 from sense, 12 NVR
assert_entity_counts(hass, Platform.SENSOR, 12, 9) assert_entity_counts(hass, Platform.SENSOR, 12, 9)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@ -368,7 +241,7 @@ async def test_sensor_nvr_missing_values(
assert entity assert entity
assert entity.unique_id == unique_id assert entity.unique_id == unique_id
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
@ -401,7 +274,7 @@ async def test_sensor_nvr_missing_values(
assert entity.disabled is True assert entity.disabled is True
assert entity.unique_id == unique_id assert entity.unique_id == unique_id
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
@ -410,16 +283,17 @@ async def test_sensor_nvr_missing_values(
async def test_sensor_setup_camera( async def test_sensor_setup_camera(
hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime
): ):
"""Test sensor entity setup for camera devices.""" """Test sensor entity setup for camera devices."""
# 3 from all, 7 from camera, 12 NVR
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SENSOR, 25, 13) assert_entity_counts(hass, Platform.SENSOR, 25, 13)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
expected_values = ( expected_values = (
now.replace(microsecond=0).isoformat(), fixed_now.replace(microsecond=0).isoformat(),
"100", "100",
"100.0", "100.0",
"20.0", "20.0",
@ -428,7 +302,7 @@ async def test_sensor_setup_camera(
if not description.entity_registry_enabled_default: if not description.entity_registry_enabled_default:
continue continue
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, camera, description Platform.SENSOR, doorbell, description
) )
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -444,7 +318,7 @@ async def test_sensor_setup_camera(
expected_values = ("100", "100") expected_values = ("100", "100")
for index, description in enumerate(CAMERA_DISABLED_SENSORS): for index, description in enumerate(CAMERA_DISABLED_SENSORS):
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, camera, description Platform.SENSOR, doorbell, description
) )
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -452,7 +326,7 @@ async def test_sensor_setup_camera(
assert entity.disabled is not description.entity_registry_enabled_default assert entity.disabled is not description.entity_registry_enabled_default
assert entity.unique_id == unique_id assert entity.unique_id == unique_id
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
@ -461,7 +335,7 @@ async def test_sensor_setup_camera(
# Wired signal # Wired signal
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, camera, ALL_DEVICES_SENSORS[2] Platform.SENSOR, doorbell, ALL_DEVICES_SENSORS[2]
) )
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -469,7 +343,7 @@ async def test_sensor_setup_camera(
assert entity.disabled is True assert entity.disabled is True
assert entity.unique_id == unique_id assert entity.unique_id == unique_id
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
@ -478,7 +352,7 @@ async def test_sensor_setup_camera(
# WiFi signal # WiFi signal
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, camera, ALL_DEVICES_SENSORS[3] Platform.SENSOR, doorbell, ALL_DEVICES_SENSORS[3]
) )
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -486,7 +360,7 @@ async def test_sensor_setup_camera(
assert entity.disabled is True assert entity.disabled is True
assert entity.unique_id == unique_id assert entity.unique_id == unique_id
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
@ -495,7 +369,7 @@ async def test_sensor_setup_camera(
# Detected Object # Detected Object
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, camera, MOTION_SENSORS[0] Platform.SENSOR, doorbell, MOTION_SENSORS[0]
) )
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -512,16 +386,20 @@ async def test_sensor_setup_camera(
async def test_sensor_setup_camera_with_last_trip_time( async def test_sensor_setup_camera_with_last_trip_time(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry_enabled_by_default: AsyncMock, entity_registry_enabled_by_default: AsyncMock,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
camera: Camera, doorbell: Camera,
now: datetime, fixed_now: datetime,
): ):
"""Test sensor entity setup for camera devices with last trip time.""" """Test sensor entity setup for camera devices with last trip time."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SENSOR, 25, 25)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
# Last Trip Time # Last Trip Time
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, camera, MOTION_TRIP_SENSORS[0] Platform.SENSOR, doorbell, MOTION_TRIP_SENSORS[0]
) )
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
@ -530,35 +408,38 @@ async def test_sensor_setup_camera_with_last_trip_time(
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.state == "2021-12-20T17:26:53+00:00" assert (
state.state
== (fixed_now - timedelta(hours=1)).replace(microsecond=0).isoformat()
)
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_sensor_update_motion( async def test_sensor_update_motion(
hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime
): ):
"""Test sensor motion entity.""" """Test sensor motion entity."""
# 3 from all, 7 from camera, 12 NVR
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SENSOR, 25, 13) assert_entity_counts(hass, Platform.SENSOR, 25, 13)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SENSOR, camera, MOTION_SENSORS[0] Platform.SENSOR, doorbell, MOTION_SENSORS[0]
) )
event = Event( event = Event(
id="test_event_id", id="test_event_id",
type=EventType.SMART_DETECT, type=EventType.SMART_DETECT,
start=now - timedelta(seconds=1), start=fixed_now - timedelta(seconds=1),
end=None, end=None,
score=100, score=100,
smart_detect_types=[SmartDetectObjectType.PERSON], smart_detect_types=[SmartDetectObjectType.PERSON],
smart_detect_event_ids=[], smart_detect_event_ids=[],
camera_id=camera.id, camera_id=doorbell.id,
api=mock_entry.api, api=ufp.api,
) )
new_bootstrap = copy(mock_entry.api.bootstrap) new_camera = doorbell.copy()
new_camera = camera.copy()
new_camera.is_smart_detected = True new_camera.is_smart_detected = True
new_camera.last_smart_detect_event_id = event.id new_camera.last_smart_detect_event_id = event.id
@ -566,10 +447,9 @@ async def test_sensor_update_motion(
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = event mock_msg.new_obj = event
new_bootstrap.cameras = {new_camera.id: new_camera} ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
new_bootstrap.events = {event.id: event} ufp.api.bootstrap.events = {event.id: event}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
@ -580,31 +460,31 @@ async def test_sensor_update_motion(
async def test_sensor_update_alarm( async def test_sensor_update_alarm(
hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor, now: datetime hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor, fixed_now: datetime
): ):
"""Test sensor motion entity.""" """Test sensor motion entity."""
# 5 from all, 5 from sense, 12 NVR
await init_entry(hass, ufp, [sensor_all])
assert_entity_counts(hass, Platform.SENSOR, 22, 14) assert_entity_counts(hass, Platform.SENSOR, 22, 14)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SENSOR, sensor, SENSE_SENSORS_WRITE[4] Platform.SENSOR, sensor_all, SENSE_SENSORS_WRITE[4]
) )
event_metadata = EventMetadata(sensor_id=sensor.id, alarm_type="smoke") event_metadata = EventMetadata(sensor_id=sensor_all.id, alarm_type="smoke")
event = Event( event = Event(
id="test_event_id", id="test_event_id",
type=EventType.SENSOR_ALARM, type=EventType.SENSOR_ALARM,
start=now - timedelta(seconds=1), start=fixed_now - timedelta(seconds=1),
end=None, end=None,
score=100, score=100,
smart_detect_types=[], smart_detect_types=[],
smart_detect_event_ids=[], smart_detect_event_ids=[],
metadata=event_metadata, metadata=event_metadata,
api=mock_entry.api, api=ufp.api,
) )
new_bootstrap = copy(mock_entry.api.bootstrap) new_sensor = sensor_all.copy()
new_sensor = sensor.copy()
new_sensor.set_alarm_timeout() new_sensor.set_alarm_timeout()
new_sensor.last_alarm_event_id = event.id new_sensor.last_alarm_event_id = event.id
@ -612,10 +492,9 @@ async def test_sensor_update_alarm(
mock_msg.changed_data = {} mock_msg.changed_data = {}
mock_msg.new_obj = event mock_msg.new_obj = event
new_bootstrap.sensors = {new_sensor.id: new_sensor} ufp.api.bootstrap.sensors = {new_sensor.id: new_sensor}
new_bootstrap.events = {event.id: event} ufp.api.bootstrap.events = {event.id: event}
mock_entry.api.bootstrap = new_bootstrap ufp.ws_msg(mock_msg)
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
@ -627,15 +506,18 @@ async def test_sensor_update_alarm(
async def test_sensor_update_alarm_with_last_trip_time( async def test_sensor_update_alarm_with_last_trip_time(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry_enabled_by_default: AsyncMock, entity_registry_enabled_by_default: AsyncMock,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
sensor: Sensor, sensor_all: Sensor,
now: datetime, fixed_now: datetime,
): ):
"""Test sensor motion entity with last trip time.""" """Test sensor motion entity with last trip time."""
await init_entry(hass, ufp, [sensor_all])
assert_entity_counts(hass, Platform.SENSOR, 22, 22)
# Last Trip Time # Last Trip Time
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, sensor, SENSE_SENSORS_WRITE[-3] Platform.SENSOR, sensor_all, SENSE_SENSORS_WRITE[-3]
) )
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@ -645,5 +527,8 @@ async def test_sensor_update_alarm_with_last_trip_time(
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.state == "2022-01-04T04:03:56+00:00" assert (
state.state
== (fixed_now - timedelta(hours=1)).replace(microsecond=0).isoformat()
)
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION

View File

@ -6,7 +6,6 @@ from unittest.mock import AsyncMock, Mock
import pytest import pytest
from pyunifiprotect.data import Camera, Chime, Light, ModelType from pyunifiprotect.data import Camera, Chime, Light, ModelType
from pyunifiprotect.data.bootstrap import ProtectDeviceRef
from pyunifiprotect.exceptions import BadRequest from pyunifiprotect.exceptions import BadRequest
from homeassistant.components.unifiprotect.const import ATTR_MESSAGE, DOMAIN from homeassistant.components.unifiprotect.const import ATTR_MESSAGE, DOMAIN
@ -21,15 +20,14 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from .conftest import MockEntityFixture, regenerate_device_ids from .utils import MockUFPFixture, init_entry
@pytest.fixture(name="device") @pytest.fixture(name="device")
async def device_fixture(hass: HomeAssistant, mock_entry: MockEntityFixture): async def device_fixture(hass: HomeAssistant, ufp: MockUFPFixture):
"""Fixture with entry setup to call services with.""" """Fixture with entry setup to call services with."""
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await init_entry(hass, ufp, [])
await hass.async_block_till_done()
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
@ -37,30 +35,20 @@ async def device_fixture(hass: HomeAssistant, mock_entry: MockEntityFixture):
@pytest.fixture(name="subdevice") @pytest.fixture(name="subdevice")
async def subdevice_fixture( async def subdevice_fixture(hass: HomeAssistant, ufp: MockUFPFixture, light: Light):
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light
):
"""Fixture with entry setup to call services with.""" """Fixture with entry setup to call services with."""
mock_light._api = mock_entry.api await init_entry(hass, ufp, [light])
mock_entry.api.bootstrap.lights = {
mock_light.id: mock_light,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
return [d for d in device_registry.devices.values() if d.name != "UnifiProtect"][0] return [d for d in device_registry.devices.values() if d.name != "UnifiProtect"][0]
async def test_global_service_bad_device( async def test_global_service_bad_device(hass: HomeAssistant, ufp: MockUFPFixture):
hass: HomeAssistant, device: dr.DeviceEntry, mock_entry: MockEntityFixture
):
"""Test global service, invalid device ID.""" """Test global service, invalid device ID."""
nvr = mock_entry.api.bootstrap.nvr nvr = ufp.api.bootstrap.nvr
nvr.__fields__["add_custom_doorbell_message"] = Mock() nvr.__fields__["add_custom_doorbell_message"] = Mock()
nvr.add_custom_doorbell_message = AsyncMock() nvr.add_custom_doorbell_message = AsyncMock()
@ -75,11 +63,11 @@ async def test_global_service_bad_device(
async def test_global_service_exception( async def test_global_service_exception(
hass: HomeAssistant, device: dr.DeviceEntry, mock_entry: MockEntityFixture hass: HomeAssistant, device: dr.DeviceEntry, ufp: MockUFPFixture
): ):
"""Test global service, unexpected error.""" """Test global service, unexpected error."""
nvr = mock_entry.api.bootstrap.nvr nvr = ufp.api.bootstrap.nvr
nvr.__fields__["add_custom_doorbell_message"] = Mock() nvr.__fields__["add_custom_doorbell_message"] = Mock()
nvr.add_custom_doorbell_message = AsyncMock(side_effect=BadRequest) nvr.add_custom_doorbell_message = AsyncMock(side_effect=BadRequest)
@ -94,11 +82,11 @@ async def test_global_service_exception(
async def test_add_doorbell_text( async def test_add_doorbell_text(
hass: HomeAssistant, device: dr.DeviceEntry, mock_entry: MockEntityFixture hass: HomeAssistant, device: dr.DeviceEntry, ufp: MockUFPFixture
): ):
"""Test add_doorbell_text service.""" """Test add_doorbell_text service."""
nvr = mock_entry.api.bootstrap.nvr nvr = ufp.api.bootstrap.nvr
nvr.__fields__["add_custom_doorbell_message"] = Mock() nvr.__fields__["add_custom_doorbell_message"] = Mock()
nvr.add_custom_doorbell_message = AsyncMock() nvr.add_custom_doorbell_message = AsyncMock()
@ -112,11 +100,11 @@ async def test_add_doorbell_text(
async def test_remove_doorbell_text( async def test_remove_doorbell_text(
hass: HomeAssistant, subdevice: dr.DeviceEntry, mock_entry: MockEntityFixture hass: HomeAssistant, subdevice: dr.DeviceEntry, ufp: MockUFPFixture
): ):
"""Test remove_doorbell_text service.""" """Test remove_doorbell_text service."""
nvr = mock_entry.api.bootstrap.nvr nvr = ufp.api.bootstrap.nvr
nvr.__fields__["remove_custom_doorbell_message"] = Mock() nvr.__fields__["remove_custom_doorbell_message"] = Mock()
nvr.remove_custom_doorbell_message = AsyncMock() nvr.remove_custom_doorbell_message = AsyncMock()
@ -130,11 +118,11 @@ async def test_remove_doorbell_text(
async def test_set_default_doorbell_text( async def test_set_default_doorbell_text(
hass: HomeAssistant, device: dr.DeviceEntry, mock_entry: MockEntityFixture hass: HomeAssistant, device: dr.DeviceEntry, ufp: MockUFPFixture
): ):
"""Test set_default_doorbell_text service.""" """Test set_default_doorbell_text service."""
nvr = mock_entry.api.bootstrap.nvr nvr = ufp.api.bootstrap.nvr
nvr.__fields__["set_default_doorbell_message"] = Mock() nvr.__fields__["set_default_doorbell_message"] = Mock()
nvr.set_default_doorbell_message = AsyncMock() nvr.set_default_doorbell_message = AsyncMock()
@ -149,57 +137,21 @@ async def test_set_default_doorbell_text(
async def test_set_chime_paired_doorbells( async def test_set_chime_paired_doorbells(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
mock_chime: Chime, chime: Chime,
mock_camera: Camera, doorbell: Camera,
): ):
"""Test set_chime_paired_doorbells.""" """Test set_chime_paired_doorbells."""
mock_entry.api.update_device = AsyncMock() ufp.api.update_device = AsyncMock()
mock_chime._api = mock_entry.api camera1 = doorbell.copy()
mock_chime.name = "Test Chime"
mock_chime._initial_data = mock_chime.dict()
mock_entry.api.bootstrap.chimes = {
mock_chime.id: mock_chime,
}
mock_entry.api.bootstrap.mac_lookup = {
mock_chime.mac.lower(): ProtectDeviceRef(
model=mock_chime.model, id=mock_chime.id
)
}
camera1 = mock_camera.copy()
camera1.name = "Test Camera 1" camera1.name = "Test Camera 1"
camera1._api = mock_entry.api
camera1.channels[0]._api = mock_entry.api
camera1.channels[1]._api = mock_entry.api
camera1.channels[2]._api = mock_entry.api
camera1.feature_flags.has_chime = True
regenerate_device_ids(camera1)
camera2 = mock_camera.copy() camera2 = doorbell.copy()
camera2.name = "Test Camera 2" camera2.name = "Test Camera 2"
camera2._api = mock_entry.api
camera2.channels[0]._api = mock_entry.api
camera2.channels[1]._api = mock_entry.api
camera2.channels[2]._api = mock_entry.api
camera2.feature_flags.has_chime = True
regenerate_device_ids(camera2)
mock_entry.api.bootstrap.cameras = { await init_entry(hass, ufp, [camera1, camera2, chime])
camera1.id: camera1,
camera2.id: camera2,
}
mock_entry.api.bootstrap.mac_lookup[camera1.mac.lower()] = ProtectDeviceRef(
model=camera1.model, id=camera1.id
)
mock_entry.api.bootstrap.mac_lookup[camera2.mac.lower()] = ProtectDeviceRef(
model=camera2.model, id=camera2.id
)
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
registry = er.async_get(hass) registry = er.async_get(hass)
chime_entry = registry.async_get("button.test_chime_play_chime") chime_entry = registry.async_get("button.test_chime_play_chime")
@ -220,6 +172,6 @@ async def test_set_chime_paired_doorbells(
blocking=True, blocking=True,
) )
mock_entry.api.update_device.assert_called_once_with( ufp.api.update_device.assert_called_once_with(
ModelType.CHIME, mock_chime.id, {"cameraIds": sorted([camera1.id, camera2.id])} ModelType.CHIME, chime.id, {"cameraIds": sorted([camera1.id, camera2.id])}
) )

View File

@ -5,14 +5,7 @@ from __future__ import annotations
from unittest.mock import AsyncMock, Mock from unittest.mock import AsyncMock, Mock
import pytest import pytest
from pyunifiprotect.data import ( from pyunifiprotect.data import Camera, Light, Permission, RecordingMode, VideoMode
Camera,
Light,
Permission,
RecordingMode,
SmartDetectObjectType,
VideoMode,
)
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
from homeassistant.components.unifiprotect.switch import ( from homeassistant.components.unifiprotect.switch import (
@ -24,12 +17,12 @@ from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, STATE_OFF, Pla
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .conftest import ( from .utils import (
MockEntityFixture, MockUFPFixture,
assert_entity_counts, assert_entity_counts,
enable_entity, enable_entity,
ids_from_device_description, ids_from_device_description,
reset_objects, init_entry,
) )
CAMERA_SWITCHES_BASIC = [ CAMERA_SWITCHES_BASIC = [
@ -44,218 +37,33 @@ CAMERA_SWITCHES_NO_EXTRA = [
] ]
@pytest.fixture(name="light")
async def light_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light
):
"""Fixture for a single light for testing the switch platform."""
# disable pydantic validation so mocking can happen
Light.__config__.validate_assignment = False
light_obj = mock_light.copy()
light_obj._api = mock_entry.api
light_obj.name = "Test Light"
light_obj.is_ssh_enabled = False
light_obj.light_device_settings.is_indicator_enabled = False
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.lights = {
light_obj.id: light_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.SWITCH, 2, 1)
yield light_obj
Light.__config__.validate_assignment = True
@pytest.fixture(name="camera")
async def camera_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera
):
"""Fixture for a single camera for testing the switch platform."""
# disable pydantic validation so mocking can happen
Camera.__config__.validate_assignment = False
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.recording_settings.mode = RecordingMode.DETECTIONS
camera_obj.feature_flags.has_led_status = True
camera_obj.feature_flags.has_hdr = True
camera_obj.feature_flags.video_modes = [VideoMode.DEFAULT, VideoMode.HIGH_FPS]
camera_obj.feature_flags.has_privacy_mask = True
camera_obj.feature_flags.has_speaker = True
camera_obj.feature_flags.has_smart_detect = True
camera_obj.feature_flags.smart_detect_types = [
SmartDetectObjectType.PERSON,
SmartDetectObjectType.VEHICLE,
]
camera_obj.is_ssh_enabled = False
camera_obj.led_settings.is_enabled = False
camera_obj.hdr_mode = False
camera_obj.video_mode = VideoMode.DEFAULT
camera_obj.remove_privacy_zone()
camera_obj.speaker_settings.are_system_sounds_enabled = False
camera_obj.osd_settings.is_name_enabled = False
camera_obj.osd_settings.is_date_enabled = False
camera_obj.osd_settings.is_logo_enabled = False
camera_obj.osd_settings.is_debug_enabled = False
camera_obj.smart_detect_settings.object_types = []
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.SWITCH, 13, 12)
yield camera_obj
Camera.__config__.validate_assignment = True
@pytest.fixture(name="camera_none")
async def camera_none_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera
):
"""Fixture for a single camera for testing the switch platform."""
# disable pydantic validation so mocking can happen
Camera.__config__.validate_assignment = False
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.recording_settings.mode = RecordingMode.DETECTIONS
camera_obj.feature_flags.has_led_status = False
camera_obj.feature_flags.has_hdr = False
camera_obj.feature_flags.video_modes = [VideoMode.DEFAULT]
camera_obj.feature_flags.has_privacy_mask = False
camera_obj.feature_flags.has_speaker = False
camera_obj.feature_flags.has_smart_detect = False
camera_obj.is_ssh_enabled = False
camera_obj.osd_settings.is_name_enabled = False
camera_obj.osd_settings.is_date_enabled = False
camera_obj.osd_settings.is_logo_enabled = False
camera_obj.osd_settings.is_debug_enabled = False
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.SWITCH, 6, 5)
yield camera_obj
Camera.__config__.validate_assignment = True
@pytest.fixture(name="camera_privacy")
async def camera_privacy_fixture(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera
):
"""Fixture for a single camera for testing the switch platform."""
# disable pydantic validation so mocking can happen
Camera.__config__.validate_assignment = False
# mock_camera._update_lock = None
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.recording_settings.mode = RecordingMode.NEVER
camera_obj.feature_flags.has_led_status = False
camera_obj.feature_flags.has_hdr = False
camera_obj.feature_flags.video_modes = [VideoMode.DEFAULT]
camera_obj.feature_flags.has_privacy_mask = True
camera_obj.feature_flags.has_speaker = False
camera_obj.feature_flags.has_smart_detect = False
camera_obj.add_privacy_zone()
camera_obj.is_ssh_enabled = False
camera_obj.osd_settings.is_name_enabled = False
camera_obj.osd_settings.is_date_enabled = False
camera_obj.osd_settings.is_logo_enabled = False
camera_obj.osd_settings.is_debug_enabled = False
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.SWITCH, 7, 6)
yield camera_obj
Camera.__config__.validate_assignment = True
async def test_switch_setup_no_perm( async def test_switch_setup_no_perm(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
mock_light: Light, light: Light,
mock_camera: Camera, doorbell: Camera,
): ):
"""Test switch entity setup for light devices.""" """Test switch entity setup for light devices."""
light_obj = mock_light.copy() ufp.api.bootstrap.auth_user.all_permissions = [
light_obj._api = mock_entry.api
camera_obj = mock_camera.copy()
camera_obj._api = mock_entry.api
camera_obj.channels[0]._api = mock_entry.api
camera_obj.channels[1]._api = mock_entry.api
camera_obj.channels[2]._api = mock_entry.api
reset_objects(mock_entry.api.bootstrap)
mock_entry.api.bootstrap.lights = {
light_obj.id: light_obj,
}
mock_entry.api.bootstrap.cameras = {
camera_obj.id: camera_obj,
}
mock_entry.api.bootstrap.auth_user.all_permissions = [
Permission.unifi_dict_to_dict({"rawPermission": "light:read:*"}) Permission.unifi_dict_to_dict({"rawPermission": "light:read:*"})
] ]
await hass.config_entries.async_setup(mock_entry.entry.entry_id) await init_entry(hass, ufp, [light, doorbell])
await hass.async_block_till_done()
assert_entity_counts(hass, Platform.SWITCH, 0, 0) assert_entity_counts(hass, Platform.SWITCH, 0, 0)
async def test_switch_setup_light( async def test_switch_setup_light(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
light: Light, light: Light,
): ):
"""Test switch entity setup for light devices.""" """Test switch entity setup for light devices."""
await init_entry(hass, ufp, [light])
assert_entity_counts(hass, Platform.SWITCH, 2, 1)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
description = LIGHT_SWITCHES[1] description = LIGHT_SWITCHES[1]
@ -283,7 +91,7 @@ async def test_switch_setup_light(
assert entity.disabled is True assert entity.disabled is True
assert entity.unique_id == unique_id assert entity.unique_id == unique_id
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
@ -293,14 +101,67 @@ async def test_switch_setup_light(
async def test_switch_setup_camera_all( async def test_switch_setup_camera_all(
hass: HomeAssistant, hass: HomeAssistant,
mock_entry: MockEntityFixture, ufp: MockUFPFixture,
camera: Camera, doorbell: Camera,
): ):
"""Test switch entity setup for camera devices (all enabled feature flags).""" """Test switch entity setup for camera devices (all enabled feature flags)."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 13, 12)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
for description in CAMERA_SWITCHES_BASIC: for description in CAMERA_SWITCHES_BASIC:
unique_id, entity_id = ids_from_device_description(
Platform.SWITCH, doorbell, description
)
entity = entity_registry.async_get(entity_id)
assert entity
assert entity.unique_id == unique_id
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
description = CAMERA_SWITCHES[0]
description_entity_name = (
description.name.lower().replace(":", "").replace(" ", "_")
)
unique_id = f"{doorbell.mac}_{description.key}"
entity_id = f"switch.test_camera_{description_entity_name}"
entity = entity_registry.async_get(entity_id)
assert entity
assert entity.disabled is True
assert entity.unique_id == unique_id
await enable_entity(hass, ufp.entry.entry_id, entity_id)
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_switch_setup_camera_none(
hass: HomeAssistant,
ufp: MockUFPFixture,
camera: Camera,
):
"""Test switch entity setup for camera devices (no enabled feature flags)."""
await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.SWITCH, 6, 5)
entity_registry = er.async_get(hass)
for description in CAMERA_SWITCHES_BASIC:
if description.ufp_required_field is not None:
continue
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.SWITCH, camera, description Platform.SWITCH, camera, description
) )
@ -327,7 +188,7 @@ async def test_switch_setup_camera_all(
assert entity.disabled is True assert entity.disabled is True
assert entity.unique_id == unique_id assert entity.unique_id == unique_id
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
@ -335,56 +196,14 @@ async def test_switch_setup_camera_all(
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_switch_setup_camera_none( async def test_switch_light_status(
hass: HomeAssistant, hass: HomeAssistant, ufp: MockUFPFixture, light: Light
mock_entry: MockEntityFixture,
camera_none: Camera,
): ):
"""Test switch entity setup for camera devices (no enabled feature flags)."""
entity_registry = er.async_get(hass)
for description in CAMERA_SWITCHES_BASIC:
if description.ufp_required_field is not None:
continue
unique_id, entity_id = ids_from_device_description(
Platform.SWITCH, camera_none, description
)
entity = entity_registry.async_get(entity_id)
assert entity
assert entity.unique_id == unique_id
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
description = CAMERA_SWITCHES[0]
description_entity_name = (
description.name.lower().replace(":", "").replace(" ", "_")
)
unique_id = f"{camera_none.mac}_{description.key}"
entity_id = f"switch.test_camera_{description_entity_name}"
entity = entity_registry.async_get(entity_id)
assert entity
assert entity.disabled is True
assert entity.unique_id == unique_id
await enable_entity(hass, mock_entry.entry.entry_id, entity_id)
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_switch_light_status(hass: HomeAssistant, light: Light):
"""Tests status light switch for lights.""" """Tests status light switch for lights."""
await init_entry(hass, ufp, [light])
assert_entity_counts(hass, Platform.SWITCH, 2, 1)
description = LIGHT_SWITCHES[1] description = LIGHT_SWITCHES[1]
light.__fields__["set_status_light"] = Mock() light.__fields__["set_status_light"] = Mock()
@ -406,44 +225,53 @@ async def test_switch_light_status(hass: HomeAssistant, light: Light):
async def test_switch_camera_ssh( async def test_switch_camera_ssh(
hass: HomeAssistant, camera: Camera, mock_entry: MockEntityFixture hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
): ):
"""Tests SSH switch for cameras.""" """Tests SSH switch for cameras."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 13, 12)
description = CAMERA_SWITCHES[0] description = CAMERA_SWITCHES[0]
camera.__fields__["set_ssh"] = Mock() doorbell.__fields__["set_ssh"] = Mock()
camera.set_ssh = AsyncMock() doorbell.set_ssh = AsyncMock()
_, entity_id = ids_from_device_description(Platform.SWITCH, camera, description) _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description)
await enable_entity(hass, mock_entry.entry.entry_id, entity_id) await enable_entity(hass, ufp.entry.entry_id, entity_id)
await hass.services.async_call( await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
camera.set_ssh.assert_called_once_with(True) doorbell.set_ssh.assert_called_once_with(True)
await hass.services.async_call( await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
camera.set_ssh.assert_called_with(False) doorbell.set_ssh.assert_called_with(False)
@pytest.mark.parametrize("description", CAMERA_SWITCHES_NO_EXTRA) @pytest.mark.parametrize("description", CAMERA_SWITCHES_NO_EXTRA)
async def test_switch_camera_simple( async def test_switch_camera_simple(
hass: HomeAssistant, camera: Camera, description: ProtectSwitchEntityDescription hass: HomeAssistant,
ufp: MockUFPFixture,
doorbell: Camera,
description: ProtectSwitchEntityDescription,
): ):
"""Tests all simple switches for cameras.""" """Tests all simple switches for cameras."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 13, 12)
assert description.ufp_set_method is not None assert description.ufp_set_method is not None
camera.__fields__[description.ufp_set_method] = Mock() doorbell.__fields__[description.ufp_set_method] = Mock()
setattr(camera, description.ufp_set_method, AsyncMock()) setattr(doorbell, description.ufp_set_method, AsyncMock())
set_method = getattr(camera, description.ufp_set_method) set_method = getattr(doorbell, description.ufp_set_method)
_, entity_id = ids_from_device_description(Platform.SWITCH, camera, description) _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description)
await hass.services.async_call( await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
@ -458,70 +286,82 @@ async def test_switch_camera_simple(
set_method.assert_called_with(False) set_method.assert_called_with(False)
async def test_switch_camera_highfps(hass: HomeAssistant, camera: Camera): async def test_switch_camera_highfps(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
):
"""Tests High FPS switch for cameras.""" """Tests High FPS switch for cameras."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 13, 12)
description = CAMERA_SWITCHES[3] description = CAMERA_SWITCHES[3]
camera.__fields__["set_video_mode"] = Mock() doorbell.__fields__["set_video_mode"] = Mock()
camera.set_video_mode = AsyncMock() doorbell.set_video_mode = AsyncMock()
_, entity_id = ids_from_device_description(Platform.SWITCH, camera, description) _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description)
await hass.services.async_call( await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
camera.set_video_mode.assert_called_once_with(VideoMode.HIGH_FPS) doorbell.set_video_mode.assert_called_once_with(VideoMode.HIGH_FPS)
await hass.services.async_call( await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
camera.set_video_mode.assert_called_with(VideoMode.DEFAULT) doorbell.set_video_mode.assert_called_with(VideoMode.DEFAULT)
async def test_switch_camera_privacy(hass: HomeAssistant, camera: Camera): async def test_switch_camera_privacy(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
):
"""Tests Privacy Mode switch for cameras.""" """Tests Privacy Mode switch for cameras."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 13, 12)
description = CAMERA_SWITCHES[4] description = CAMERA_SWITCHES[4]
camera.__fields__["set_privacy"] = Mock() doorbell.__fields__["set_privacy"] = Mock()
camera.set_privacy = AsyncMock() doorbell.set_privacy = AsyncMock()
_, entity_id = ids_from_device_description(Platform.SWITCH, camera, description) _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description)
await hass.services.async_call( await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
camera.set_privacy.assert_called_once_with(True, 0, RecordingMode.NEVER) doorbell.set_privacy.assert_called_once_with(True, 0, RecordingMode.NEVER)
await hass.services.async_call( await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
camera.set_privacy.assert_called_with( doorbell.set_privacy.assert_called_with(
False, camera.mic_volume, camera.recording_settings.mode False, doorbell.mic_volume, doorbell.recording_settings.mode
) )
async def test_switch_camera_privacy_already_on( async def test_switch_camera_privacy_already_on(
hass: HomeAssistant, camera_privacy: Camera hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
): ):
"""Tests Privacy Mode switch for cameras with privacy mode defaulted on.""" """Tests Privacy Mode switch for cameras with privacy mode defaulted on."""
doorbell.add_privacy_zone()
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 13, 12)
description = CAMERA_SWITCHES[4] description = CAMERA_SWITCHES[4]
camera_privacy.__fields__["set_privacy"] = Mock() doorbell.__fields__["set_privacy"] = Mock()
camera_privacy.set_privacy = AsyncMock() doorbell.set_privacy = AsyncMock()
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description)
Platform.SWITCH, camera_privacy, description
)
await hass.services.async_call( await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
camera_privacy.set_privacy.assert_called_once_with(False, 100, RecordingMode.ALWAYS) doorbell.set_privacy.assert_called_once_with(False, 100, RecordingMode.ALWAYS)

View File

@ -0,0 +1,168 @@
"""Test helpers for UniFi Protect."""
# pylint: disable=protected-access
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from typing import Any, Callable, Sequence
from pyunifiprotect import ProtectApiClient
from pyunifiprotect.data import (
Bootstrap,
Camera,
ProtectAdoptableDeviceModel,
WSSubscriptionMessage,
)
from pyunifiprotect.data.bootstrap import ProtectDeviceRef
from pyunifiprotect.test_util.anonymize import random_hex
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity import EntityDescription
import homeassistant.util.dt as dt_util
from tests.common import MockConfigEntry, async_fire_time_changed
@dataclass
class MockUFPFixture:
"""Mock for NVR."""
entry: MockConfigEntry
api: ProtectApiClient
ws_subscription: Callable[[WSSubscriptionMessage], None] | None = None
def ws_msg(self, msg: WSSubscriptionMessage) -> Any:
"""Emit WS message for testing."""
if self.ws_subscription is not None:
return self.ws_subscription(msg)
def reset_objects(bootstrap: Bootstrap):
"""Reset bootstrap objects."""
bootstrap.cameras = {}
bootstrap.lights = {}
bootstrap.sensors = {}
bootstrap.viewers = {}
bootstrap.events = {}
bootstrap.doorlocks = {}
bootstrap.chimes = {}
async def time_changed(hass: HomeAssistant, seconds: int) -> None:
"""Trigger time changed."""
next_update = dt_util.utcnow() + timedelta(seconds)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
async def enable_entity(
hass: HomeAssistant, entry_id: str, entity_id: str
) -> er.RegistryEntry:
"""Enable a disabled entity."""
entity_registry = er.async_get(hass)
updated_entity = entity_registry.async_update_entity(entity_id, disabled_by=None)
assert not updated_entity.disabled
await hass.config_entries.async_reload(entry_id)
await hass.async_block_till_done()
return updated_entity
def assert_entity_counts(
hass: HomeAssistant, platform: Platform, total: int, enabled: int
) -> None:
"""Assert entity counts for a given platform."""
entity_registry = er.async_get(hass)
entities = [
e for e in entity_registry.entities if split_entity_id(e)[0] == platform.value
]
assert len(entities) == total
assert len(hass.states.async_all(platform.value)) == enabled
def normalize_name(name: str) -> str:
"""Normalize name."""
return name.lower().replace(":", "").replace(" ", "_").replace("-", "_")
def ids_from_device_description(
platform: Platform,
device: ProtectAdoptableDeviceModel,
description: EntityDescription,
) -> tuple[str, str]:
"""Return expected unique_id and entity_id for a give platform/device/description combination."""
entity_name = normalize_name(device.display_name)
description_entity_name = normalize_name(str(description.name))
unique_id = f"{device.mac}_{description.key}"
entity_id = f"{platform.value}.{entity_name}_{description_entity_name}"
return unique_id, entity_id
def generate_random_ids() -> tuple[str, str]:
"""Generate random IDs for device."""
return random_hex(24).lower(), random_hex(12).upper()
def regenerate_device_ids(device: ProtectAdoptableDeviceModel) -> None:
"""Regenerate the IDs on UFP device."""
device.id, device.mac = generate_random_ids()
def add_device_ref(bootstrap: Bootstrap, device: ProtectAdoptableDeviceModel) -> None:
"""Manually add device ref to bootstrap for lookup."""
ref = ProtectDeviceRef(id=device.id, model=device.model)
bootstrap.id_lookup[device.id] = ref
bootstrap.mac_lookup[device.mac.lower()] = ref
def add_device(
bootstrap: Bootstrap, device: ProtectAdoptableDeviceModel, regenerate_ids: bool
) -> None:
"""Add test device to bootstrap."""
if device.model is None:
return
device._api = bootstrap.api
if isinstance(device, Camera):
for channel in device.channels:
channel._api = bootstrap.api
if regenerate_ids:
regenerate_device_ids(device)
device._initial_data = device.dict()
devices = getattr(bootstrap, f"{device.model.value}s")
devices[device.id] = device
add_device_ref(bootstrap, device)
async def init_entry(
hass: HomeAssistant,
ufp: MockUFPFixture,
devices: Sequence[ProtectAdoptableDeviceModel],
regenerate_ids: bool = True,
) -> None:
"""Initialize Protect entry with given devices."""
reset_objects(ufp.api.bootstrap)
for device in devices:
add_device(ufp.api.bootstrap, device, regenerate_ids)
await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done()