mirror of
https://github.com/home-assistant/core.git
synced 2026-02-21 10:27:52 +00:00
404 lines
12 KiB
Python
404 lines
12 KiB
Python
"""Test the UniFi Protect number platform."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
import pytest
|
|
from uiprotect.data import Camera, Chime, Doorlock, IRLEDMode, Light, RingSetting
|
|
|
|
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
|
|
from homeassistant.components.unifiprotect.number import (
|
|
CAMERA_NUMBERS,
|
|
DOORLOCK_NUMBERS,
|
|
LIGHT_NUMBERS,
|
|
ProtectNumberEntityDescription,
|
|
)
|
|
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import entity_registry as er
|
|
|
|
from . import patch_ufp_method
|
|
from .utils import (
|
|
MockUFPFixture,
|
|
adopt_devices,
|
|
assert_entity_counts,
|
|
ids_from_device_description,
|
|
init_entry,
|
|
remove_entities,
|
|
)
|
|
|
|
|
|
async def test_number_sensor_camera_remove(
|
|
hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera, unadopted_camera: Camera
|
|
) -> None:
|
|
"""Test removing and re-adding a camera device."""
|
|
|
|
await init_entry(hass, ufp, [camera, unadopted_camera])
|
|
assert_entity_counts(hass, Platform.NUMBER, 4, 4)
|
|
await remove_entities(hass, ufp, [camera, unadopted_camera])
|
|
assert_entity_counts(hass, Platform.NUMBER, 0, 0)
|
|
await adopt_devices(hass, ufp, [camera, unadopted_camera])
|
|
assert_entity_counts(hass, Platform.NUMBER, 4, 4)
|
|
|
|
|
|
async def test_number_sensor_light_remove(
|
|
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
|
|
) -> None:
|
|
"""Test removing and re-adding a light device."""
|
|
|
|
await init_entry(hass, ufp, [light])
|
|
assert_entity_counts(hass, Platform.NUMBER, 2, 2)
|
|
await remove_entities(hass, ufp, [light])
|
|
assert_entity_counts(hass, Platform.NUMBER, 0, 0)
|
|
await adopt_devices(hass, ufp, [light])
|
|
assert_entity_counts(hass, Platform.NUMBER, 2, 2)
|
|
|
|
|
|
async def test_number_lock_remove(
|
|
hass: HomeAssistant, ufp: MockUFPFixture, doorlock: Doorlock
|
|
) -> None:
|
|
"""Test removing and re-adding a light device."""
|
|
|
|
await init_entry(hass, ufp, [doorlock])
|
|
assert_entity_counts(hass, Platform.NUMBER, 1, 1)
|
|
await remove_entities(hass, ufp, [doorlock])
|
|
assert_entity_counts(hass, Platform.NUMBER, 0, 0)
|
|
await adopt_devices(hass, ufp, [doorlock])
|
|
assert_entity_counts(hass, Platform.NUMBER, 1, 1)
|
|
|
|
|
|
async def test_number_setup_light(
|
|
hass: HomeAssistant,
|
|
entity_registry: er.EntityRegistry,
|
|
ufp: MockUFPFixture,
|
|
light: Light,
|
|
) -> None:
|
|
"""Test number entity setup for light devices."""
|
|
|
|
await init_entry(hass, ufp, [light])
|
|
assert_entity_counts(hass, Platform.NUMBER, 2, 2)
|
|
|
|
for description in LIGHT_NUMBERS:
|
|
unique_id, entity_id = await ids_from_device_description(
|
|
hass, Platform.NUMBER, light, 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 == "45"
|
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
|
|
|
|
|
async def test_number_setup_camera_all(
|
|
hass: HomeAssistant,
|
|
entity_registry: er.EntityRegistry,
|
|
ufp: MockUFPFixture,
|
|
camera: Camera,
|
|
) -> None:
|
|
"""Test number entity setup for camera devices (all features)."""
|
|
|
|
camera.feature_flags.has_chime = True
|
|
camera.chime_duration = timedelta(seconds=1)
|
|
camera.feature_flags.has_led_ir = True
|
|
camera.isp_settings.icr_custom_value = 1
|
|
camera.isp_settings.ir_led_mode = IRLEDMode.CUSTOM
|
|
camera.feature_flags.has_speaker = True
|
|
camera.speaker_settings.volume = 1
|
|
camera.feature_flags.is_doorbell = True
|
|
camera.speaker_settings.ring_volume = 1
|
|
await init_entry(hass, ufp, [camera])
|
|
assert_entity_counts(hass, Platform.NUMBER, 7, 7)
|
|
|
|
for description in CAMERA_NUMBERS:
|
|
unique_id, entity_id = await ids_from_device_description(
|
|
hass, Platform.NUMBER, camera, 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 == "1"
|
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
|
|
|
|
|
async def test_number_setup_camera_none(
|
|
hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera
|
|
) -> None:
|
|
"""Test number entity setup for camera devices (no features)."""
|
|
|
|
camera.feature_flags.can_optical_zoom = False
|
|
camera.feature_flags.has_mic = False
|
|
# has_wdr is an the inverse of has HDR
|
|
camera.feature_flags.has_hdr = True
|
|
camera.feature_flags.has_led_ir = False
|
|
|
|
await init_entry(hass, ufp, [camera])
|
|
assert_entity_counts(hass, Platform.NUMBER, 0, 0)
|
|
|
|
|
|
async def test_number_setup_camera_missing_attr(
|
|
hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera
|
|
) -> None:
|
|
"""Test number entity setup for camera devices (no features, bad attrs)."""
|
|
|
|
camera.feature_flags = None
|
|
|
|
await init_entry(hass, ufp, [camera])
|
|
assert_entity_counts(hass, Platform.NUMBER, 0, 0)
|
|
|
|
|
|
async def test_number_light_sensitivity(
|
|
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
|
|
) -> None:
|
|
"""Test sensitivity number entity for lights."""
|
|
|
|
await init_entry(hass, ufp, [light])
|
|
assert_entity_counts(hass, Platform.NUMBER, 2, 2)
|
|
|
|
description = LIGHT_NUMBERS[0]
|
|
assert description.ufp_set_method is not None
|
|
|
|
_, entity_id = await ids_from_device_description(
|
|
hass, Platform.NUMBER, light, description
|
|
)
|
|
|
|
with patch_ufp_method(
|
|
light, "set_sensitivity", new_callable=AsyncMock
|
|
) as mock_method:
|
|
await hass.services.async_call(
|
|
"number",
|
|
"set_value",
|
|
{ATTR_ENTITY_ID: entity_id, "value": 15.0},
|
|
blocking=True,
|
|
)
|
|
|
|
mock_method.assert_called_once_with(15.0)
|
|
|
|
|
|
async def test_number_light_duration(
|
|
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
|
|
) -> None:
|
|
"""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]
|
|
|
|
_, entity_id = await ids_from_device_description(
|
|
hass, Platform.NUMBER, light, description
|
|
)
|
|
|
|
with patch_ufp_method(light, "set_duration", new_callable=AsyncMock) as mock_method:
|
|
await hass.services.async_call(
|
|
"number",
|
|
"set_value",
|
|
{ATTR_ENTITY_ID: entity_id, "value": 15.0},
|
|
blocking=True,
|
|
)
|
|
|
|
mock_method.assert_called_once_with(timedelta(seconds=15.0))
|
|
|
|
|
|
@pytest.mark.parametrize("description", CAMERA_NUMBERS)
|
|
async def test_number_camera_simple(
|
|
hass: HomeAssistant,
|
|
ufp: MockUFPFixture,
|
|
camera_all_features: Camera,
|
|
description: ProtectNumberEntityDescription,
|
|
) -> None:
|
|
"""Tests simple numbers for cameras using the all features fixture."""
|
|
await init_entry(hass, ufp, [camera_all_features])
|
|
assert_entity_counts(hass, Platform.NUMBER, 7, 7)
|
|
|
|
assert description.ufp_set_method is not None
|
|
|
|
_, entity_id = await ids_from_device_description(
|
|
hass, Platform.NUMBER, camera_all_features, description
|
|
)
|
|
|
|
with patch_ufp_method(
|
|
camera_all_features, description.ufp_set_method, new_callable=AsyncMock
|
|
) as mock_method:
|
|
await hass.services.async_call(
|
|
"number",
|
|
"set_value",
|
|
{ATTR_ENTITY_ID: entity_id, "value": 1.0},
|
|
blocking=True,
|
|
)
|
|
|
|
mock_method.assert_called_once_with(1.0)
|
|
|
|
|
|
async def test_number_lock_auto_close(
|
|
hass: HomeAssistant, ufp: MockUFPFixture, doorlock: Doorlock
|
|
) -> None:
|
|
"""Test auto-lock timeout for locks."""
|
|
|
|
await init_entry(hass, ufp, [doorlock])
|
|
assert_entity_counts(hass, Platform.NUMBER, 1, 1)
|
|
|
|
description = DOORLOCK_NUMBERS[0]
|
|
|
|
_, entity_id = await ids_from_device_description(
|
|
hass, Platform.NUMBER, doorlock, description
|
|
)
|
|
|
|
with patch_ufp_method(
|
|
doorlock, "set_auto_close_time", new_callable=AsyncMock
|
|
) as mock_method:
|
|
await hass.services.async_call(
|
|
"number",
|
|
"set_value",
|
|
{ATTR_ENTITY_ID: entity_id, "value": 15.0},
|
|
blocking=True,
|
|
)
|
|
|
|
mock_method.assert_called_once_with(timedelta(seconds=15.0))
|
|
|
|
|
|
def _setup_chime_with_doorbell(
|
|
chime: Chime, doorbell: Camera, volume: int = 50
|
|
) -> None:
|
|
"""Set up chime with paired doorbell for testing."""
|
|
chime.camera_ids = [doorbell.id]
|
|
chime.ring_settings = [
|
|
RingSetting(
|
|
camera_id=doorbell.id,
|
|
repeat_times=1,
|
|
ringtone_id="test-ringtone-id",
|
|
volume=volume,
|
|
)
|
|
]
|
|
|
|
|
|
async def test_chime_ring_volume_setup(
|
|
hass: HomeAssistant,
|
|
entity_registry: er.EntityRegistry,
|
|
ufp: MockUFPFixture,
|
|
chime: Chime,
|
|
doorbell: Camera,
|
|
) -> None:
|
|
"""Test chime ring volume number entity setup."""
|
|
_setup_chime_with_doorbell(chime, doorbell, volume=75)
|
|
|
|
await init_entry(hass, ufp, [chime, doorbell], regenerate_ids=False)
|
|
|
|
entity_id = "number.test_chime_ring_volume_test_camera"
|
|
entity = entity_registry.async_get(entity_id)
|
|
assert entity is not None
|
|
assert entity.unique_id == f"{chime.mac}_ring_volume_{doorbell.id}"
|
|
|
|
state = hass.states.get(entity_id)
|
|
assert state is not None
|
|
assert state.state == "75"
|
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
|
|
|
|
|
async def test_chime_ring_volume_set_value(
|
|
hass: HomeAssistant,
|
|
ufp: MockUFPFixture,
|
|
chime: Chime,
|
|
doorbell: Camera,
|
|
) -> None:
|
|
"""Test setting chime ring volume."""
|
|
_setup_chime_with_doorbell(chime, doorbell)
|
|
|
|
await init_entry(hass, ufp, [chime, doorbell], regenerate_ids=False)
|
|
|
|
entity_id = "number.test_chime_ring_volume_test_camera"
|
|
|
|
with patch_ufp_method(
|
|
chime, "set_volume_for_camera_public", new_callable=AsyncMock
|
|
) as mock_method:
|
|
await hass.services.async_call(
|
|
"number",
|
|
"set_value",
|
|
{ATTR_ENTITY_ID: entity_id, "value": 80.0},
|
|
blocking=True,
|
|
)
|
|
|
|
mock_method.assert_called_once_with(doorbell, 80)
|
|
|
|
|
|
async def test_chime_ring_volume_multiple_cameras(
|
|
hass: HomeAssistant,
|
|
ufp: MockUFPFixture,
|
|
chime: Chime,
|
|
doorbell: Camera,
|
|
) -> None:
|
|
"""Test chime ring volume with multiple paired cameras."""
|
|
doorbell2 = doorbell.model_copy()
|
|
doorbell2.id = "test-doorbell-2"
|
|
doorbell2.name = "Test Doorbell 2"
|
|
doorbell2.mac = "aa:bb:cc:dd:ee:02"
|
|
|
|
chime.camera_ids = [doorbell.id, doorbell2.id]
|
|
chime.ring_settings = [
|
|
RingSetting(
|
|
camera_id=doorbell.id,
|
|
repeat_times=1,
|
|
ringtone_id="test-ringtone-id",
|
|
volume=60,
|
|
),
|
|
RingSetting(
|
|
camera_id=doorbell2.id,
|
|
repeat_times=2,
|
|
ringtone_id="test-ringtone-id-2",
|
|
volume=80,
|
|
),
|
|
]
|
|
|
|
await init_entry(hass, ufp, [chime, doorbell, doorbell2], regenerate_ids=False)
|
|
|
|
state1 = hass.states.get("number.test_chime_ring_volume_test_camera")
|
|
assert state1 is not None
|
|
assert state1.state == "60"
|
|
|
|
state2 = hass.states.get("number.test_chime_ring_volume_test_doorbell_2")
|
|
assert state2 is not None
|
|
assert state2.state == "80"
|
|
|
|
|
|
async def test_chime_ring_volume_unavailable_when_unpaired(
|
|
hass: HomeAssistant,
|
|
ufp: MockUFPFixture,
|
|
chime: Chime,
|
|
doorbell: Camera,
|
|
) -> None:
|
|
"""Test chime ring volume becomes unavailable when camera is unpaired."""
|
|
_setup_chime_with_doorbell(chime, doorbell)
|
|
|
|
await init_entry(hass, ufp, [chime, doorbell], regenerate_ids=False)
|
|
|
|
entity_id = "number.test_chime_ring_volume_test_camera"
|
|
state = hass.states.get(entity_id)
|
|
assert state
|
|
assert state.state == "50"
|
|
|
|
# Simulate removing the camera pairing
|
|
new_chime = chime.model_copy()
|
|
new_chime.ring_settings = []
|
|
|
|
ufp.api.bootstrap.chimes = {new_chime.id: new_chime}
|
|
ufp.api.bootstrap.nvr.system_info.ustorage = None
|
|
mock_msg = Mock()
|
|
mock_msg.changed_data = {}
|
|
mock_msg.new_obj = new_chime
|
|
|
|
ufp.ws_msg(mock_msg)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(entity_id)
|
|
assert state
|
|
assert state.state == "unavailable"
|