Files
core/tests/components/unifiprotect/test_switch.py
T

1108 lines
35 KiB
Python

"""Test the UniFi Protect switch platform."""
from typing import Any
from unittest.mock import AsyncMock, Mock, call
import pytest
from uiprotect.data import (
Camera,
Light,
Permission,
PublicHdrMode,
RecordingMode,
Sensor,
SmartDetectAudioType,
SmartDetectObjectType,
VideoMode,
)
from uiprotect.data.public_devices import SensorFeatureCapability
from uiprotect.exceptions import ClientError, NotAuthorized
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION, DOMAIN
from homeassistant.components.unifiprotect.switch import (
ATTR_PREV_MIC,
ATTR_PREV_RECORD,
CAMERA_SWITCHES,
LIGHT_SWITCHES,
PRIVACY_MODE_SWITCH,
SENSE_SWITCHES,
ProtectSwitchEntityDescription,
)
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_ENTITY_ID,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import patch_ufp_method
from .utils import (
MockUFPFixture,
adopt_devices,
assert_entity_counts,
enable_entity,
ids_from_device_description,
init_entry,
make_public_camera,
make_public_light,
make_public_sensor,
public_device_ws_message,
remove_entities,
setup_public_camera,
setup_public_light,
setup_public_sensor,
)
CAMERA_SWITCHES_BASIC = [
d
for d in CAMERA_SWITCHES
if (
not d.translation_key.startswith("detections_")
and d.key not in {"ssh", "color_night_vision", "track_person", "hdr_mode"}
)
or d.key
in {
"detections_motion",
"detections_person",
"detections_vehicle",
"detections_animal",
}
]
CAMERA_SWITCHES_NO_EXTRA = [
d
for d in CAMERA_SWITCHES_BASIC
if d.key not in ("high_fps", "privacy_mode", "hdr_mode")
]
async def test_switch_camera_remove(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, unadopted_camera: Camera
) -> None:
"""Test removing and re-adding a camera device."""
ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
await remove_entities(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.SWITCH, 2, 2)
await adopt_devices(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
async def test_switch_light_remove(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
"""Test removing and re-adding a light device."""
ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [light])
assert_entity_counts(hass, Platform.SWITCH, 4, 3)
await remove_entities(hass, ufp, [light])
assert_entity_counts(hass, Platform.SWITCH, 2, 2)
await adopt_devices(hass, ufp, [light])
assert_entity_counts(hass, Platform.SWITCH, 4, 3)
async def test_switch_nvr(hass: HomeAssistant, ufp: MockUFPFixture) -> None:
"""Test switch entity setup for light devices."""
await init_entry(hass, ufp, [])
assert_entity_counts(hass, Platform.SWITCH, 2, 2)
nvr = ufp.api.bootstrap.nvr
entity_id = "switch.unifiprotect_insights_enabled"
with patch_ufp_method(nvr, "set_insights", new_callable=AsyncMock) as mock_method:
await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_called_once_with(True)
await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_called_with(False)
async def test_switch_setup_no_perm(
hass: HomeAssistant,
ufp: MockUFPFixture,
light: Light,
doorbell: Camera,
) -> None:
"""Test switch entity setup for light devices."""
ufp.api.bootstrap.auth_user.all_permissions = [
Permission.unifi_dict_to_dict({"rawPermission": "light:read:*"})
]
await init_entry(hass, ufp, [light, doorbell])
assert_entity_counts(hass, Platform.SWITCH, 0, 0)
async def test_switch_setup_light(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
ufp: MockUFPFixture,
light: Light,
) -> None:
"""Test switch entity setup for light devices."""
setup_public_light(ufp)
await init_entry(hass, ufp, [light])
assert_entity_counts(hass, Platform.SWITCH, 4, 3)
description = LIGHT_SWITCHES[1]
unique_id, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, 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 == STATE_OFF
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
description = LIGHT_SWITCHES[0]
unique_id = f"{light.mac}_{description.key}"
entity_id = f"switch.test_light_{description.translation_key}"
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_all(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
ufp: MockUFPFixture,
doorbell: Camera,
) -> None:
"""Test switch entity setup for camera devices (all enabled feature flags)."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
for description in CAMERA_SWITCHES_BASIC:
unique_id, entity_id = await ids_from_device_description(
hass, 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]
unique_id = f"{doorbell.mac}_{description.key}"
entity_id = f"switch.test_camera_{description.translation_key}"
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,
entity_registry: er.EntityRegistry,
ufp: MockUFPFixture,
camera: Camera,
) -> None:
"""Test switch entity setup for camera devices (no enabled feature flags)."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.SWITCH, 8, 7)
for description in CAMERA_SWITCHES_BASIC:
if description.ufp_required_field is not None:
continue
unique_id, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, 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 == STATE_OFF
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
description = CAMERA_SWITCHES[0]
unique_id = f"{camera.mac}_{description.key}"
entity_id = f"switch.test_camera_{description.translation_key}"
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_light_status(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
"""Tests status light switch for lights."""
setup_public_light(ufp)
await init_entry(hass, ufp, [light])
assert_entity_counts(hass, Platform.SWITCH, 4, 3)
description = LIGHT_SWITCHES[1]
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, light, description
)
with patch_ufp_method(
light, "set_status_light_public", new_callable=AsyncMock
) as mock_method:
await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_called_once_with(True)
await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_called_with(False)
async def test_switch_light_status_public_value(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
"""Status light switch reads from the public object and refreshes on a WS update."""
setup_public_light(ufp)
await init_entry(hass, ufp, [light])
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, light, LIGHT_SWITCHES[1]
)
assert hass.states.get(entity_id).state == STATE_OFF
# The private fixture has the indicator disabled; the public ON proves the source.
public = make_public_light(light, is_indicator_enabled=True)
ufp.devices_ws_subscription(public_device_ws_message(public))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
async def test_switch_light_status_unavailable_without_public(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
"""The migrated status light switch is unavailable without a public object."""
await init_entry(hass, ufp, [light])
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, light, LIGHT_SWITCHES[1]
)
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
async def test_switch_camera_ssh(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
"""Tests SSH switch for cameras."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
description = CAMERA_SWITCHES[0]
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
await enable_entity(hass, ufp.entry.entry_id, entity_id)
with patch_ufp_method(doorbell, "set_ssh", new_callable=AsyncMock) as mock_method:
await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_called_once_with(True)
await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_called_with(False)
@pytest.mark.parametrize("description", CAMERA_SWITCHES_NO_EXTRA)
async def test_switch_camera_simple(
hass: HomeAssistant,
ufp: MockUFPFixture,
doorbell: Camera,
description: ProtectSwitchEntityDescription,
) -> None:
"""Tests all simple switches for cameras."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
assert description.ufp_set_method is not None
with patch_ufp_method(
doorbell, description.ufp_set_method, new_callable=AsyncMock
) as mock_method:
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_called_once_with(True)
await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_called_with(False)
async def test_switch_camera_highfps(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
"""Tests High FPS switch for cameras."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
description = CAMERA_SWITCHES[3]
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
with patch_ufp_method(
doorbell, "set_video_mode_public", new_callable=AsyncMock
) as mock_method:
await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_called_once_with(VideoMode.HIGH_FPS)
await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_called_with(VideoMode.DEFAULT)
CAMERA_SWITCHES_DETECTIONS_EXTRA = [
d
for d in CAMERA_SWITCHES
if d.translation_key.startswith("detections_")
and d.key
not in {
"detections_motion",
"detections_person",
"detections_vehicle",
"detections_animal",
}
]
async def test_switch_camera_hdr(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
"""Tests HDR mode switch uses the public API helper."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
description = next(d for d in CAMERA_SWITCHES if d.key == "hdr_mode")
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
await enable_entity(hass, ufp.entry.entry_id, entity_id)
with patch_ufp_method(
doorbell, "set_hdr_mode_public", new_callable=AsyncMock
) as mock_method:
await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_has_calls(
[call(PublicHdrMode.AUTO), call(PublicHdrMode.OFF)]
)
assert mock_method.call_count == 2
@pytest.mark.parametrize("description", CAMERA_SWITCHES_DETECTIONS_EXTRA)
async def test_switch_camera_detections_public_api(
hass: HomeAssistant,
ufp: MockUFPFixture,
doorbell: Camera,
description: ProtectSwitchEntityDescription,
) -> None:
"""Tests detection switches call the public API setters."""
doorbell.feature_flags.smart_detect_types = [
SmartDetectObjectType.PERSON,
SmartDetectObjectType.VEHICLE,
SmartDetectObjectType.ANIMAL,
SmartDetectObjectType.PACKAGE,
SmartDetectObjectType.LICENSE_PLATE,
]
doorbell.feature_flags.smart_detect_audio_types = [
SmartDetectAudioType.SMOKE,
SmartDetectAudioType.CMONX,
SmartDetectAudioType.SIREN,
SmartDetectAudioType.BABY_CRY,
SmartDetectAudioType.SPEAK,
SmartDetectAudioType.BARK,
SmartDetectAudioType.BURGLAR,
SmartDetectAudioType.CAR_HORN,
SmartDetectAudioType.GLASS_BREAK,
]
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell])
assert description.ufp_set_method is not None
assert description.ufp_set_method.endswith("_public")
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
with patch_ufp_method(
doorbell, description.ufp_set_method, new_callable=AsyncMock
) as mock_method:
await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_has_calls([call(True), call(False)])
assert mock_method.call_count == 2
async def test_switch_camera_status_light_public_value(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
"""Status light reads from the public object and refreshes on a public WS update."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell])
description = next(d for d in CAMERA_SWITCHES if d.key == "status_light")
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
assert hass.states.get(entity_id).state == STATE_OFF
public = make_public_camera(doorbell, status_light=True)
ufp.devices_ws_subscription(public_device_ws_message(public))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
@pytest.mark.parametrize(
("switch_key", "camera_kwarg"),
[
("osd_name", "osd_name"),
("osd_date", "osd_date"),
("osd_logo", "osd_logo"),
("osd_bitrate", "osd_debug"),
],
)
async def test_switch_camera_osd_public_value(
hass: HomeAssistant,
ufp: MockUFPFixture,
doorbell: Camera,
switch_key: str,
camera_kwarg: str,
) -> None:
"""Each OSD switch reads its own public osd_settings flag independently."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell])
description = next(d for d in CAMERA_SWITCHES if d.key == switch_key)
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
assert hass.states.get(entity_id).state == STATE_OFF
public = make_public_camera(doorbell, **{camera_kwarg: True})
ufp.devices_ws_subscription(public_device_ws_message(public))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
# Only the switch under test is enabled; ``make_public_camera`` defaults both
# lists to every type, so each case pins both to keep the others off.
CAMERA_SWITCHES_DETECTION_READ = [
("smart_person", [SmartDetectObjectType.PERSON], []),
("smart_vehicle", [SmartDetectObjectType.VEHICLE], []),
("smart_animal", [SmartDetectObjectType.ANIMAL], []),
("smart_package", [SmartDetectObjectType.PACKAGE], []),
("smart_licenseplate", [SmartDetectObjectType.LICENSE_PLATE], []),
("smart_smoke", [], [SmartDetectAudioType.SMOKE]),
("smart_cmonx", [], [SmartDetectAudioType.CMONX]),
("smart_siren", [], [SmartDetectAudioType.SIREN]),
("smart_baby_cry", [], [SmartDetectAudioType.BABY_CRY]),
("smart_speak", [], [SmartDetectAudioType.SPEAK]),
("smart_bark", [], [SmartDetectAudioType.BARK]),
("smart_car_alarm", [], [SmartDetectAudioType.BURGLAR]),
("smart_car_horn", [], [SmartDetectAudioType.CAR_HORN]),
("smart_glass_break", [], [SmartDetectAudioType.GLASS_BREAK]),
]
@pytest.mark.parametrize(
("key", "object_types", "audio_types"), CAMERA_SWITCHES_DETECTION_READ
)
async def test_switch_camera_detection_public_value(
hass: HomeAssistant,
ufp: MockUFPFixture,
doorbell: Camera,
key: str,
object_types: list[SmartDetectObjectType],
audio_types: list[SmartDetectAudioType],
) -> None:
"""Each detection toggle reads its on/off state from its own public flag."""
doorbell.feature_flags.smart_detect_types = [
SmartDetectObjectType.PERSON,
SmartDetectObjectType.VEHICLE,
SmartDetectObjectType.ANIMAL,
SmartDetectObjectType.PACKAGE,
SmartDetectObjectType.LICENSE_PLATE,
]
doorbell.feature_flags.smart_detect_audio_types = [
SmartDetectAudioType.SMOKE,
SmartDetectAudioType.CMONX,
SmartDetectAudioType.SIREN,
SmartDetectAudioType.BABY_CRY,
SmartDetectAudioType.SPEAK,
SmartDetectAudioType.BARK,
SmartDetectAudioType.BURGLAR,
SmartDetectAudioType.CAR_HORN,
SmartDetectAudioType.GLASS_BREAK,
]
setup_public_camera(ufp)
async def _prime_without_camera() -> Any:
pb = ufp.api.public_bootstrap
pb.cameras = {}
return pb
ufp.api.update_public = AsyncMock(side_effect=_prime_without_camera)
await init_entry(hass, ufp, [doorbell])
description = next(d for d in CAMERA_SWITCHES if d.key == key)
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
all_off = make_public_camera(doorbell, object_types=[], audio_types=[])
ufp.devices_ws_subscription(public_device_ws_message(all_off))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF
public = make_public_camera(
doorbell, object_types=object_types, audio_types=audio_types
)
ufp.devices_ws_subscription(public_device_ws_message(public))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
async def test_switch_camera_highfps_public_value(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
"""The high FPS switch reads video_mode from the public object."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell])
description = next(d for d in CAMERA_SWITCHES if d.key == "high_fps")
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
assert hass.states.get(entity_id).state == STATE_OFF
public = make_public_camera(doorbell, video_mode=VideoMode.HIGH_FPS)
ufp.devices_ws_subscription(public_device_ws_message(public))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
async def test_switch_camera_detection_unavailable_without_public(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
"""A migrated detection toggle is unavailable without a public object."""
async def _prime_without_camera() -> Any:
pb = ufp.api.public_bootstrap
pb.cameras = {}
return pb
ufp.api.update_public = AsyncMock(side_effect=_prime_without_camera)
await init_entry(hass, ufp, [doorbell])
description = next(d for d in CAMERA_SWITCHES if d.key == "smart_person")
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
async def test_switch_camera_detection_available_with_recording_disabled(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
"""A migrated detection toggle stays available with recording disabled.
Unlike the legacy private-only switch, the public detection toggles no
longer gate their availability on ``is_recording_enabled`` (breaking change).
"""
doorbell.recording_settings.mode = RecordingMode.NEVER
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell])
description = next(d for d in CAMERA_SWITCHES if d.key == "smart_person")
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
assert hass.states.get(entity_id).state != STATE_UNAVAILABLE
async def test_switch_camera_privacy(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
"""Tests Privacy Mode switch for cameras with privacy mode defaulted on."""
previous_mic = doorbell.mic_volume = 53
previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
description = PRIVACY_MODE_SWITCH
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
state = hass.states.get(entity_id)
assert state and state.state == "off"
assert ATTR_PREV_MIC not in state.attributes
assert ATTR_PREV_RECORD not in state.attributes
with patch_ufp_method(
doorbell, "set_privacy", new_callable=AsyncMock
) as mock_set_privacy:
await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_set_privacy.assert_called_with(True, 0, RecordingMode.NEVER)
new_doorbell = doorbell.model_copy()
new_doorbell.add_privacy_zone()
new_doorbell.mic_volume = 0
new_doorbell.recording_settings.mode = RecordingMode.NEVER
ufp.api.bootstrap.cameras = {new_doorbell.id: new_doorbell}
mock_msg = Mock()
mock_msg.changed_data = {}
mock_msg.new_obj = new_doorbell
ufp.ws_msg(mock_msg)
state = hass.states.get(entity_id)
assert state and state.state == "on"
assert state.attributes[ATTR_PREV_MIC] == previous_mic
assert state.attributes[ATTR_PREV_RECORD] == previous_record.value
mock_set_privacy.reset_mock()
await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_set_privacy.assert_called_with(False, previous_mic, previous_record)
async def test_switch_camera_privacy_already_on(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
"""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, 17, 15)
description = PRIVACY_MODE_SWITCH
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
with patch_ufp_method(
doorbell, "set_privacy", new_callable=AsyncMock
) as mock_set_privacy:
await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_set_privacy.assert_called_once_with(False, 100, RecordingMode.ALWAYS)
async def test_switch_turn_on_client_error(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
"""Test switch turn on with ClientError raises HomeAssistantError."""
setup_public_light(ufp)
await init_entry(hass, ufp, [light])
description = LIGHT_SWITCHES[1]
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, light, description
)
with (
patch_ufp_method(
light,
"set_status_light_public",
new_callable=AsyncMock,
side_effect=ClientError("Test error"),
),
pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
async def test_switch_turn_on_not_authorized(
hass: HomeAssistant, ufp: MockUFPFixture, light: Light
) -> None:
"""Test switch turn on with NotAuthorized raises HomeAssistantError."""
setup_public_light(ufp)
await init_entry(hass, ufp, [light])
description = LIGHT_SWITCHES[1]
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, light, description
)
with (
patch_ufp_method(
light,
"set_status_light_public",
new_callable=AsyncMock,
side_effect=NotAuthorized("Not authorized"),
),
pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
"switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
# A USL Environmental reports these four and neither motion nor alarm-sound
# detection, so none of its capabilities back a motion or alarm switch.
_ENV_CAPABILITIES = {
SensorFeatureCapability.TEMPERATURE,
SensorFeatureCapability.HUMIDITY,
SensorFeatureCapability.LIGHT,
SensorFeatureCapability.WATER_LEAK,
}
async def test_switch_sense_capability_creation_filter(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
ufp: MockUFPFixture,
sensor_all: Sensor,
) -> None:
"""A capability map limits the config switches to the advertised capabilities."""
setup_public_sensor(ufp, capabilities=_ENV_CAPABILITIES)
await init_entry(hass, ufp, [sensor_all])
for key, created in (
("status_light", True),
("temperature", True),
("humidity", True),
("light", True),
("motion", False),
("alarm", False),
):
description = next(d for d in SENSE_SWITCHES if d.key == key)
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, sensor_all, description
)
assert (entity_registry.async_get(entity_id) is not None) is created, key
async def test_switch_sense_capability_registry_cleanup(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
ufp: MockUFPFixture,
sensor_all: Sensor,
) -> None:
"""A console upgrade removes registry entries for unsupported capabilities."""
stale = entity_registry.async_get_or_create(
Platform.SWITCH,
DOMAIN,
f"{sensor_all.mac}_motion",
config_entry=ufp.entry,
)
setup_public_sensor(ufp, capabilities=_ENV_CAPABILITIES)
await init_entry(hass, ufp, [sensor_all], regenerate_ids=False)
assert entity_registry.async_get(stale.entity_id) is None
async def test_switch_sense_no_capability_map_creates_all(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
ufp: MockUFPFixture,
sensor_all: Sensor,
) -> None:
"""Without a capability map (Protect below 7.2) every config switch is created."""
setup_public_sensor(ufp)
await init_entry(hass, ufp, [sensor_all])
for description in SENSE_SWITCHES:
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, sensor_all, description
)
assert entity_registry.async_get(entity_id) is not None, description.key
async def test_switch_sense_no_capability_map_keeps_existing(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
ufp: MockUFPFixture,
sensor_all: Sensor,
) -> None:
"""Without a capability map (Protect below 7.2) nothing is removed.
The console cannot say which capabilities it lacks, so an existing entity
must survive setup instead of being deleted on a guess.
"""
existing = entity_registry.async_get_or_create(
Platform.SWITCH,
DOMAIN,
f"{sensor_all.mac}_motion",
config_entry=ufp.entry,
)
setup_public_sensor(ufp)
await init_entry(hass, ufp, [sensor_all], regenerate_ids=False)
assert entity_registry.async_get(existing.entity_id) is not None
# The five sense settings the public API exposes, with the public-mock override
# that flips them and the public setter each switch must write through.
MIGRATED_SENSE_SWITCHES = [
("motion", "motion_enabled", "set_motion_status_public"),
("temperature", "temperature_enabled", "set_temperature_status_public"),
("humidity", "humidity_enabled", "set_humidity_status_public"),
("light", "light_enabled", "set_light_status_public"),
("alarm", "alarm_enabled", "set_alarm_public"),
]
@pytest.mark.parametrize(("key", "public_kwarg", "set_method"), MIGRATED_SENSE_SWITCHES)
async def test_switch_sense_public_value(
hass: HomeAssistant,
ufp: MockUFPFixture,
sensor_all: Sensor,
key: str,
public_kwarg: str,
set_method: str,
) -> None:
"""Each migrated sense switch reads its state from the public object."""
setup_public_sensor(ufp)
await init_entry(hass, ufp, [sensor_all])
description = next(d for d in SENSE_SWITCHES if d.key == key)
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, sensor_all, description
)
assert hass.states.get(entity_id).state == STATE_ON
# every setting is enabled on the private fixture, so a public OFF can only
# come from the public object
public = make_public_sensor(sensor_all, **{public_kwarg: False})
ufp.devices_ws_subscription(public_device_ws_message(public))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF
@pytest.mark.parametrize(("key", "public_kwarg", "set_method"), MIGRATED_SENSE_SWITCHES)
async def test_switch_sense_set_public(
hass: HomeAssistant,
ufp: MockUFPFixture,
sensor_all: Sensor,
key: str,
public_kwarg: str,
set_method: str,
) -> None:
"""Each migrated sense switch writes through the public API."""
setup_public_sensor(ufp)
await init_entry(hass, ufp, [sensor_all])
description = next(d for d in SENSE_SWITCHES if d.key == key)
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, sensor_all, description
)
with patch_ufp_method(
sensor_all, set_method, new_callable=AsyncMock
) as mock_method:
await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_called_once_with(False)
async def test_switch_sense_unavailable_without_public(
hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor
) -> None:
"""A migrated sense switch is unavailable without a public object."""
await init_entry(hass, ufp, [sensor_all])
description = next(d for d in SENSE_SWITCHES if d.key == "motion")
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, sensor_all, description
)
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
async def test_switch_sense_status_light_stays_private(
hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor
) -> None:
"""The status light has no public counterpart, so it reads the private object.
Unlike the migrated switches it must stay usable without a public object.
"""
await init_entry(hass, ufp, [sensor_all])
description = next(d for d in SENSE_SWITCHES if d.key == "status_light")
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, sensor_all, description
)
assert hass.states.get(entity_id).state == STATE_ON
with patch_ufp_method(
sensor_all, "set_status_light", new_callable=AsyncMock
) as mock_method:
await hass.services.async_call(
"switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_method.assert_called_once_with(False)
async def test_switch_sense_public_switches_ignore_local_permissions(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
ufp: MockUFPFixture,
sensor_all: Sensor,
) -> None:
"""A read-only local user keeps the migrated switches but not the private one.
The migrated switches write through the API key, so the local user's write
bit must not gate them; the status light still uses a private setter and
stays behind PermRequired.WRITE.
"""
ufp.api.bootstrap.auth_user.all_permissions = [
Permission.unifi_dict_to_dict({"rawPermission": "sensor:read:*"})
]
setup_public_sensor(ufp)
await init_entry(hass, ufp, [sensor_all])
for key, _public_kwarg, _set_method in MIGRATED_SENSE_SWITCHES:
description = next(d for d in SENSE_SWITCHES if d.key == key)
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, sensor_all, description
)
assert entity_registry.async_get(entity_id) is not None, key
description = next(d for d in SENSE_SWITCHES if d.key == "status_light")
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, sensor_all, description
)
assert entity_registry.async_get(entity_id) is None