mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
Add adopt/unadopt flows for UniFi Protect devices (#76524)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
bda9cb59d2
commit
5c0fc0c002
@ -2,9 +2,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from pyunifiprotect.data import ProtectAdoptableDeviceModel
|
from pyunifiprotect.data import ProtectAdoptableDeviceModel, ProtectModelWithId
|
||||||
|
|
||||||
from homeassistant.components.button import (
|
from homeassistant.components.button import (
|
||||||
ButtonDeviceClass,
|
ButtonDeviceClass,
|
||||||
@ -12,16 +13,20 @@ from homeassistant.components.button import (
|
|||||||
ButtonEntityDescription,
|
ButtonEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DISPATCH_ADOPT, DOMAIN
|
from .const import DEVICES_THAT_ADOPT, DISPATCH_ADD, DISPATCH_ADOPT, DOMAIN
|
||||||
from .data import ProtectData
|
from .data import ProtectData
|
||||||
from .entity import ProtectDeviceEntity, async_all_device_entities
|
from .entity import ProtectDeviceEntity, async_all_device_entities
|
||||||
from .models import PermRequired, ProtectSetableKeysMixin, T
|
from .models import PermRequired, ProtectSetableKeysMixin, T
|
||||||
from .utils import async_dispatch_id as _ufpd
|
from .utils import async_dispatch_id as _ufpd
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ProtectButtonEntityDescription(
|
class ProtectButtonEntityDescription(
|
||||||
@ -33,6 +38,7 @@ class ProtectButtonEntityDescription(
|
|||||||
|
|
||||||
|
|
||||||
DEVICE_CLASS_CHIME_BUTTON: Final = "unifiprotect__chime_button"
|
DEVICE_CLASS_CHIME_BUTTON: Final = "unifiprotect__chime_button"
|
||||||
|
KEY_ADOPT = "adopt"
|
||||||
|
|
||||||
|
|
||||||
ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = (
|
ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = (
|
||||||
@ -44,6 +50,21 @@ ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = (
|
|||||||
ufp_press="reboot",
|
ufp_press="reboot",
|
||||||
ufp_perm=PermRequired.WRITE,
|
ufp_perm=PermRequired.WRITE,
|
||||||
),
|
),
|
||||||
|
ProtectButtonEntityDescription(
|
||||||
|
key="unadopt",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
name="Unadopt Device",
|
||||||
|
icon="mdi:delete",
|
||||||
|
ufp_press="unadopt",
|
||||||
|
ufp_perm=PermRequired.DELETE,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
ADOPT_BUTTON = ProtectButtonEntityDescription[ProtectAdoptableDeviceModel](
|
||||||
|
key=KEY_ADOPT,
|
||||||
|
name="Adopt Device",
|
||||||
|
icon="mdi:plus-circle",
|
||||||
|
ufp_press="adopt",
|
||||||
)
|
)
|
||||||
|
|
||||||
SENSOR_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = (
|
SENSOR_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = (
|
||||||
@ -73,6 +94,20 @@ CHIME_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_remove_adopt_button(
|
||||||
|
hass: HomeAssistant, device: ProtectAdoptableDeviceModel
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
if device.is_adopted_by_us and (
|
||||||
|
entity_id := entity_registry.async_get_entity_id(
|
||||||
|
Platform.BUTTON, DOMAIN, f"{device.mac}_adopt"
|
||||||
|
)
|
||||||
|
):
|
||||||
|
entity_registry.async_remove(entity_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: ConfigEntry,
|
||||||
@ -86,25 +121,49 @@ async def async_setup_entry(
|
|||||||
data,
|
data,
|
||||||
ProtectButton,
|
ProtectButton,
|
||||||
all_descs=ALL_DEVICE_BUTTONS,
|
all_descs=ALL_DEVICE_BUTTONS,
|
||||||
|
unadopted_descs=[ADOPT_BUTTON],
|
||||||
chime_descs=CHIME_BUTTONS,
|
chime_descs=CHIME_BUTTONS,
|
||||||
sense_descs=SENSOR_BUTTONS,
|
sense_descs=SENSOR_BUTTONS,
|
||||||
ufp_device=device,
|
ufp_device=device,
|
||||||
)
|
)
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
_async_remove_adopt_button(hass, device)
|
||||||
|
|
||||||
|
async def _add_unadopted_device(device: ProtectAdoptableDeviceModel) -> None:
|
||||||
|
if not device.can_adopt or not device.can_create(data.api.bootstrap.auth_user):
|
||||||
|
_LOGGER.debug("Device is not adoptable: %s", device.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
entities = async_all_device_entities(
|
||||||
|
data,
|
||||||
|
ProtectButton,
|
||||||
|
unadopted_descs=[ADOPT_BUTTON],
|
||||||
|
ufp_device=device,
|
||||||
|
)
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device)
|
async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device)
|
||||||
)
|
)
|
||||||
|
entry.async_on_unload(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
hass, _ufpd(entry, DISPATCH_ADD), _add_unadopted_device
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
entities: list[ProtectDeviceEntity] = async_all_device_entities(
|
entities: list[ProtectDeviceEntity] = async_all_device_entities(
|
||||||
data,
|
data,
|
||||||
ProtectButton,
|
ProtectButton,
|
||||||
all_descs=ALL_DEVICE_BUTTONS,
|
all_descs=ALL_DEVICE_BUTTONS,
|
||||||
|
unadopted_descs=[ADOPT_BUTTON],
|
||||||
chime_descs=CHIME_BUTTONS,
|
chime_descs=CHIME_BUTTONS,
|
||||||
sense_descs=SENSOR_BUTTONS,
|
sense_descs=SENSOR_BUTTONS,
|
||||||
)
|
)
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
for device in data.get_by_types(DEVICES_THAT_ADOPT):
|
||||||
|
_async_remove_adopt_button(hass, device)
|
||||||
|
|
||||||
|
|
||||||
class ProtectButton(ProtectDeviceEntity, ButtonEntity):
|
class ProtectButton(ProtectDeviceEntity, ButtonEntity):
|
||||||
"""A Ubiquiti UniFi Protect Reboot button."""
|
"""A Ubiquiti UniFi Protect Reboot button."""
|
||||||
@ -121,6 +180,15 @@ class ProtectButton(ProtectDeviceEntity, ButtonEntity):
|
|||||||
super().__init__(data, device, description)
|
super().__init__(data, device, description)
|
||||||
self._attr_name = f"{self.device.display_name} {self.entity_description.name}"
|
self._attr_name = f"{self.device.display_name} {self.entity_description.name}"
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
|
||||||
|
super()._async_update_device_from_protect(device)
|
||||||
|
|
||||||
|
if self.entity_description.key == KEY_ADOPT:
|
||||||
|
self._attr_available = self.device.can_adopt and self.device.can_create(
|
||||||
|
self.data.api.bootstrap.auth_user
|
||||||
|
)
|
||||||
|
|
||||||
async def async_press(self) -> None:
|
async def async_press(self) -> None:
|
||||||
"""Press the button."""
|
"""Press the button."""
|
||||||
|
|
||||||
|
@ -63,5 +63,6 @@ PLATFORMS = [
|
|||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
DISPATCH_ADD = "add_device"
|
||||||
DISPATCH_ADOPT = "adopt_device"
|
DISPATCH_ADOPT = "adopt_device"
|
||||||
DISPATCH_CHANNELS = "new_camera_channels"
|
DISPATCH_CHANNELS = "new_camera_channels"
|
||||||
|
@ -29,6 +29,7 @@ from .const import (
|
|||||||
CONF_MAX_MEDIA,
|
CONF_MAX_MEDIA,
|
||||||
DEFAULT_MAX_MEDIA,
|
DEFAULT_MAX_MEDIA,
|
||||||
DEVICES_THAT_ADOPT,
|
DEVICES_THAT_ADOPT,
|
||||||
|
DISPATCH_ADD,
|
||||||
DISPATCH_ADOPT,
|
DISPATCH_ADOPT,
|
||||||
DISPATCH_CHANNELS,
|
DISPATCH_CHANNELS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -157,28 +158,33 @@ class ProtectData:
|
|||||||
self._pending_camera_ids.add(camera_id)
|
self._pending_camera_ids.add(camera_id)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None:
|
def _async_add_device(self, device: ProtectAdoptableDeviceModel) -> None:
|
||||||
# removed packets are not processed yet
|
if device.is_adopted_by_us:
|
||||||
if message.new_obj is None or not getattr(
|
_LOGGER.debug("Device adopted: %s", device.id)
|
||||||
message.new_obj, "is_adopted_by_us", True
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
obj = message.new_obj
|
|
||||||
if isinstance(obj, (ProtectAdoptableDeviceModel, NVR)):
|
|
||||||
self._async_signal_device_update(obj)
|
|
||||||
if (
|
|
||||||
obj.model == ModelType.CAMERA
|
|
||||||
and obj.id in self._pending_camera_ids
|
|
||||||
and "channels" in message.changed_data
|
|
||||||
):
|
|
||||||
self._pending_camera_ids.remove(obj.id)
|
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
self._hass, _ufpd(self._entry, DISPATCH_CHANNELS), obj
|
self._hass, _ufpd(self._entry, DISPATCH_ADOPT), device
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("New device detected: %s", device.id)
|
||||||
|
async_dispatcher_send(self._hass, _ufpd(self._entry, DISPATCH_ADD), device)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_device(
|
||||||
|
self, device: ProtectAdoptableDeviceModel | NVR, changed_data: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
self._async_signal_device_update(device)
|
||||||
|
if (
|
||||||
|
device.model == ModelType.CAMERA
|
||||||
|
and device.id in self._pending_camera_ids
|
||||||
|
and "channels" in changed_data
|
||||||
|
):
|
||||||
|
self._pending_camera_ids.remove(device.id)
|
||||||
|
async_dispatcher_send(
|
||||||
|
self._hass, _ufpd(self._entry, DISPATCH_CHANNELS), device
|
||||||
)
|
)
|
||||||
|
|
||||||
# trigger update for all Cameras with LCD screens when NVR Doorbell settings updates
|
# trigger update for all Cameras with LCD screens when NVR Doorbell settings updates
|
||||||
if "doorbell_settings" in message.changed_data:
|
if "doorbell_settings" in changed_data:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Doorbell messages updated. Updating devices with LCD screens"
|
"Doorbell messages updated. Updating devices with LCD screens"
|
||||||
)
|
)
|
||||||
@ -186,6 +192,20 @@ class ProtectData:
|
|||||||
for camera in self.api.bootstrap.cameras.values():
|
for camera in self.api.bootstrap.cameras.values():
|
||||||
if camera.feature_flags.has_lcd_screen:
|
if camera.feature_flags.has_lcd_screen:
|
||||||
self._async_signal_device_update(camera)
|
self._async_signal_device_update(camera)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None:
|
||||||
|
# removed packets are not processed yet
|
||||||
|
if message.new_obj is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
obj = message.new_obj
|
||||||
|
if isinstance(obj, (ProtectAdoptableDeviceModel, NVR)):
|
||||||
|
if message.old_obj is None and isinstance(obj, ProtectAdoptableDeviceModel):
|
||||||
|
self._async_add_device(obj)
|
||||||
|
elif getattr(obj, "is_adopted_by_us", True):
|
||||||
|
self._async_update_device(obj, message.changed_data)
|
||||||
|
|
||||||
# trigger updates for camera that the event references
|
# trigger updates for camera that the event references
|
||||||
elif isinstance(obj, Event):
|
elif isinstance(obj, Event):
|
||||||
if obj.type == EventType.DEVICE_ADOPTED:
|
if obj.type == EventType.DEVICE_ADOPTED:
|
||||||
@ -194,10 +214,7 @@ class ProtectData:
|
|||||||
obj.metadata.device_id
|
obj.metadata.device_id
|
||||||
)
|
)
|
||||||
if device is not None:
|
if device is not None:
|
||||||
_LOGGER.debug("New device detected: %s", device.id)
|
self._async_add_device(device)
|
||||||
async_dispatcher_send(
|
|
||||||
self._hass, _ufpd(self._entry, DISPATCH_ADOPT), device
|
|
||||||
)
|
|
||||||
elif obj.camera is not None:
|
elif obj.camera is not None:
|
||||||
self._async_signal_device_update(obj.camera)
|
self._async_signal_device_update(obj.camera)
|
||||||
elif obj.light is not None:
|
elif obj.light is not None:
|
||||||
|
@ -38,9 +38,10 @@ def _async_device_entities(
|
|||||||
klass: type[ProtectDeviceEntity],
|
klass: type[ProtectDeviceEntity],
|
||||||
model_type: ModelType,
|
model_type: ModelType,
|
||||||
descs: Sequence[ProtectRequiredKeysMixin],
|
descs: Sequence[ProtectRequiredKeysMixin],
|
||||||
|
unadopted_descs: Sequence[ProtectRequiredKeysMixin],
|
||||||
ufp_device: ProtectAdoptableDeviceModel | None = None,
|
ufp_device: ProtectAdoptableDeviceModel | None = None,
|
||||||
) -> list[ProtectDeviceEntity]:
|
) -> list[ProtectDeviceEntity]:
|
||||||
if len(descs) == 0:
|
if len(descs) + len(unadopted_descs) == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
entities: list[ProtectDeviceEntity] = []
|
entities: list[ProtectDeviceEntity] = []
|
||||||
@ -48,17 +49,36 @@ def _async_device_entities(
|
|||||||
[ufp_device] if ufp_device is not None else data.get_by_types({model_type})
|
[ufp_device] if ufp_device is not None else data.get_by_types({model_type})
|
||||||
)
|
)
|
||||||
for device in devices:
|
for device in devices:
|
||||||
|
assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock, Chime))
|
||||||
if not device.is_adopted_by_us:
|
if not device.is_adopted_by_us:
|
||||||
|
for description in unadopted_descs:
|
||||||
|
entities.append(
|
||||||
|
klass(
|
||||||
|
data,
|
||||||
|
device=device,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Adding %s entity %s for %s",
|
||||||
|
klass.__name__,
|
||||||
|
description.name,
|
||||||
|
device.display_name,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock, Chime))
|
can_write = device.can_write(data.api.bootstrap.auth_user)
|
||||||
for description in descs:
|
for description in descs:
|
||||||
if description.ufp_perm is not None:
|
if description.ufp_perm is not None:
|
||||||
can_write = device.can_write(data.api.bootstrap.auth_user)
|
|
||||||
if description.ufp_perm == PermRequired.WRITE and not can_write:
|
if description.ufp_perm == PermRequired.WRITE and not can_write:
|
||||||
continue
|
continue
|
||||||
if description.ufp_perm == PermRequired.NO_WRITE and can_write:
|
if description.ufp_perm == PermRequired.NO_WRITE and can_write:
|
||||||
continue
|
continue
|
||||||
|
if (
|
||||||
|
description.ufp_perm == PermRequired.DELETE
|
||||||
|
and not device.can_delete(data.api.bootstrap.auth_user)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
if description.ufp_required_field:
|
if description.ufp_required_field:
|
||||||
required_field = get_nested_attr(device, description.ufp_required_field)
|
required_field = get_nested_attr(device, description.ufp_required_field)
|
||||||
@ -93,10 +113,12 @@ def async_all_device_entities(
|
|||||||
lock_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
|
lock_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
|
||||||
chime_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
|
chime_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
|
||||||
all_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
|
all_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
|
||||||
|
unadopted_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
|
||||||
ufp_device: ProtectAdoptableDeviceModel | None = None,
|
ufp_device: ProtectAdoptableDeviceModel | None = None,
|
||||||
) -> list[ProtectDeviceEntity]:
|
) -> list[ProtectDeviceEntity]:
|
||||||
"""Generate a list of all the device entities."""
|
"""Generate a list of all the device entities."""
|
||||||
all_descs = list(all_descs or [])
|
all_descs = list(all_descs or [])
|
||||||
|
unadopted_descs = list(unadopted_descs or [])
|
||||||
camera_descs = list(camera_descs or []) + all_descs
|
camera_descs = list(camera_descs or []) + all_descs
|
||||||
light_descs = list(light_descs or []) + all_descs
|
light_descs = list(light_descs or []) + all_descs
|
||||||
sense_descs = list(sense_descs or []) + all_descs
|
sense_descs = list(sense_descs or []) + all_descs
|
||||||
@ -106,12 +128,24 @@ def async_all_device_entities(
|
|||||||
|
|
||||||
if ufp_device is None:
|
if ufp_device is None:
|
||||||
return (
|
return (
|
||||||
_async_device_entities(data, klass, ModelType.CAMERA, camera_descs)
|
_async_device_entities(
|
||||||
+ _async_device_entities(data, klass, ModelType.LIGHT, light_descs)
|
data, klass, ModelType.CAMERA, camera_descs, unadopted_descs
|
||||||
+ _async_device_entities(data, klass, ModelType.SENSOR, sense_descs)
|
)
|
||||||
+ _async_device_entities(data, klass, ModelType.VIEWPORT, viewer_descs)
|
+ _async_device_entities(
|
||||||
+ _async_device_entities(data, klass, ModelType.DOORLOCK, lock_descs)
|
data, klass, ModelType.LIGHT, light_descs, unadopted_descs
|
||||||
+ _async_device_entities(data, klass, ModelType.CHIME, chime_descs)
|
)
|
||||||
|
+ _async_device_entities(
|
||||||
|
data, klass, ModelType.SENSOR, sense_descs, unadopted_descs
|
||||||
|
)
|
||||||
|
+ _async_device_entities(
|
||||||
|
data, klass, ModelType.VIEWPORT, viewer_descs, unadopted_descs
|
||||||
|
)
|
||||||
|
+ _async_device_entities(
|
||||||
|
data, klass, ModelType.DOORLOCK, lock_descs, unadopted_descs
|
||||||
|
)
|
||||||
|
+ _async_device_entities(
|
||||||
|
data, klass, ModelType.CHIME, chime_descs, unadopted_descs
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
descs = []
|
descs = []
|
||||||
@ -128,9 +162,11 @@ def async_all_device_entities(
|
|||||||
elif ufp_device.model == ModelType.CHIME:
|
elif ufp_device.model == ModelType.CHIME:
|
||||||
descs = chime_descs
|
descs = chime_descs
|
||||||
|
|
||||||
if len(descs) == 0 or ufp_device.model is None:
|
if len(descs) + len(unadopted_descs) == 0 or ufp_device.model is None:
|
||||||
return []
|
return []
|
||||||
return _async_device_entities(data, klass, ufp_device.model, descs, ufp_device)
|
return _async_device_entities(
|
||||||
|
data, klass, ufp_device.model, descs, unadopted_descs, ufp_device
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProtectDeviceEntity(Entity):
|
class ProtectDeviceEntity(Entity):
|
||||||
@ -190,8 +226,9 @@ class ProtectDeviceEntity(Entity):
|
|||||||
assert isinstance(device, ProtectAdoptableDeviceModel)
|
assert isinstance(device, ProtectAdoptableDeviceModel)
|
||||||
self.device = device
|
self.device = device
|
||||||
|
|
||||||
is_connected = (
|
is_connected = self.data.last_update_success and (
|
||||||
self.data.last_update_success and self.device.state == StateType.CONNECTED
|
self.device.state == StateType.CONNECTED
|
||||||
|
or (not self.device.is_adopted_by_us and self.device.can_adopt)
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
hasattr(self, "entity_description")
|
hasattr(self, "entity_description")
|
||||||
|
@ -34,9 +34,6 @@ async def async_setup_entry(
|
|||||||
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
|
async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
|
||||||
if not device.is_adopted_by_us:
|
|
||||||
return
|
|
||||||
|
|
||||||
if device.model == ModelType.LIGHT and device.can_write(
|
if device.model == ModelType.LIGHT and device.can_write(
|
||||||
data.api.bootstrap.auth_user
|
data.api.bootstrap.auth_user
|
||||||
):
|
):
|
||||||
|
@ -34,9 +34,6 @@ async def async_setup_entry(
|
|||||||
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
|
async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
|
||||||
if not device.is_adopted_by_us:
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(device, Doorlock):
|
if isinstance(device, Doorlock):
|
||||||
async_add_entities([ProtectLock(data, device)])
|
async_add_entities([ProtectLock(data, device)])
|
||||||
|
|
||||||
|
@ -43,9 +43,6 @@ async def async_setup_entry(
|
|||||||
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
|
async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
|
||||||
if not device.is_adopted_by_us:
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(device, Camera) and device.feature_flags.has_speaker:
|
if isinstance(device, Camera) and device.feature_flags.has_speaker:
|
||||||
async_add_entities([ProtectMediaPlayer(data, device)])
|
async_add_entities([ProtectMediaPlayer(data, device)])
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ class PermRequired(int, Enum):
|
|||||||
|
|
||||||
NO_WRITE = 1
|
NO_WRITE = 1
|
||||||
WRITE = 2
|
WRITE = 2
|
||||||
|
DELETE = 3
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -320,6 +320,7 @@ async def async_setup_entry(
|
|||||||
data,
|
data,
|
||||||
ProtectPrivacyModeSwitch,
|
ProtectPrivacyModeSwitch,
|
||||||
camera_descs=[PRIVACY_MODE_SWITCH],
|
camera_descs=[PRIVACY_MODE_SWITCH],
|
||||||
|
ufp_device=device,
|
||||||
)
|
)
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock, Mock
|
||||||
|
|
||||||
from pyunifiprotect.data.devices import Chime
|
from pyunifiprotect.data.devices import Camera, Chime, Doorlock
|
||||||
|
|
||||||
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
|
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
|
||||||
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform
|
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform
|
||||||
@ -27,11 +27,11 @@ async def test_button_chime_remove(
|
|||||||
"""Test removing and re-adding a light device."""
|
"""Test removing and re-adding a light device."""
|
||||||
|
|
||||||
await init_entry(hass, ufp, [chime])
|
await init_entry(hass, ufp, [chime])
|
||||||
assert_entity_counts(hass, Platform.BUTTON, 3, 2)
|
assert_entity_counts(hass, Platform.BUTTON, 4, 2)
|
||||||
await remove_entities(hass, [chime])
|
await remove_entities(hass, [chime])
|
||||||
assert_entity_counts(hass, Platform.BUTTON, 0, 0)
|
assert_entity_counts(hass, Platform.BUTTON, 0, 0)
|
||||||
await adopt_devices(hass, ufp, [chime])
|
await adopt_devices(hass, ufp, [chime])
|
||||||
assert_entity_counts(hass, Platform.BUTTON, 3, 2)
|
assert_entity_counts(hass, Platform.BUTTON, 4, 2)
|
||||||
|
|
||||||
|
|
||||||
async def test_reboot_button(
|
async def test_reboot_button(
|
||||||
@ -42,7 +42,7 @@ async def test_reboot_button(
|
|||||||
"""Test button entity."""
|
"""Test button entity."""
|
||||||
|
|
||||||
await init_entry(hass, ufp, [chime])
|
await init_entry(hass, ufp, [chime])
|
||||||
assert_entity_counts(hass, Platform.BUTTON, 3, 2)
|
assert_entity_counts(hass, Platform.BUTTON, 4, 2)
|
||||||
|
|
||||||
ufp.api.reboot_device = AsyncMock()
|
ufp.api.reboot_device = AsyncMock()
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ async def test_chime_button(
|
|||||||
"""Test button entity."""
|
"""Test button entity."""
|
||||||
|
|
||||||
await init_entry(hass, ufp, [chime])
|
await init_entry(hass, ufp, [chime])
|
||||||
assert_entity_counts(hass, Platform.BUTTON, 3, 2)
|
assert_entity_counts(hass, Platform.BUTTON, 4, 2)
|
||||||
|
|
||||||
ufp.api.play_speaker = AsyncMock()
|
ufp.api.play_speaker = AsyncMock()
|
||||||
|
|
||||||
@ -95,3 +95,67 @@ async def test_chime_button(
|
|||||||
"button", "press", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
"button", "press", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||||
)
|
)
|
||||||
ufp.api.play_speaker.assert_called_once()
|
ufp.api.play_speaker.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_adopt_button(
|
||||||
|
hass: HomeAssistant, ufp: MockUFPFixture, doorlock: Doorlock, doorbell: Camera
|
||||||
|
):
|
||||||
|
"""Test button entity."""
|
||||||
|
|
||||||
|
doorlock._api = ufp.api
|
||||||
|
doorlock.is_adopted = False
|
||||||
|
doorlock.can_adopt = True
|
||||||
|
|
||||||
|
await init_entry(hass, ufp, [])
|
||||||
|
|
||||||
|
mock_msg = Mock()
|
||||||
|
mock_msg.changed_data = {}
|
||||||
|
mock_msg.old_obj = None
|
||||||
|
mock_msg.new_obj = doorlock
|
||||||
|
ufp.ws_msg(mock_msg)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert_entity_counts(hass, Platform.BUTTON, 1, 1)
|
||||||
|
|
||||||
|
ufp.api.adopt_device = AsyncMock()
|
||||||
|
|
||||||
|
unique_id = f"{doorlock.mac}_adopt"
|
||||||
|
entity_id = "button.test_lock_adopt_device"
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
assert entity
|
||||||
|
assert not entity.disabled
|
||||||
|
assert entity.unique_id == unique_id
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"button", "press", {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||||
|
)
|
||||||
|
ufp.api.adopt_device.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_adopt_button_removed(
|
||||||
|
hass: HomeAssistant, ufp: MockUFPFixture, doorlock: Doorlock, doorbell: Camera
|
||||||
|
):
|
||||||
|
"""Test button entity."""
|
||||||
|
|
||||||
|
entity_id = "button.test_lock_adopt_device"
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
doorlock._api = ufp.api
|
||||||
|
doorlock.is_adopted = False
|
||||||
|
doorlock.can_adopt = True
|
||||||
|
|
||||||
|
await init_entry(hass, ufp, [doorlock])
|
||||||
|
assert_entity_counts(hass, Platform.BUTTON, 1, 1)
|
||||||
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
await adopt_devices(hass, ufp, [doorlock], fully_adopt=True)
|
||||||
|
assert_entity_counts(hass, Platform.BUTTON, 2, 0)
|
||||||
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
assert entity is None
|
||||||
|
@ -56,7 +56,7 @@ async def test_migrate_reboot_button(
|
|||||||
for entity in er.async_entries_for_config_entry(registry, ufp.entry.entry_id):
|
for entity in er.async_entries_for_config_entry(registry, ufp.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) == 4
|
||||||
|
|
||||||
assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") is None
|
assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") is None
|
||||||
assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device_2") is None
|
assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device_2") is None
|
||||||
@ -135,7 +135,7 @@ async def test_migrate_reboot_button_no_device(
|
|||||||
for entity in er.async_entries_for_config_entry(registry, ufp.entry.entry_id):
|
for entity in er.async_entries_for_config_entry(registry, ufp.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) == 3
|
||||||
|
|
||||||
entity = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light2_id.lower()}")
|
entity = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light2_id.lower()}")
|
||||||
assert entity is not None
|
assert entity is not None
|
||||||
|
@ -89,6 +89,7 @@ def assert_entity_counts(
|
|||||||
e for e in entity_registry.entities if split_entity_id(e)[0] == platform.value
|
e for e in entity_registry.entities if split_entity_id(e)[0] == platform.value
|
||||||
]
|
]
|
||||||
|
|
||||||
|
print(len(entities), total)
|
||||||
assert len(entities) == total
|
assert len(entities) == total
|
||||||
assert len(hass.states.async_all(platform.value)) == enabled
|
assert len(hass.states.async_all(platform.value)) == enabled
|
||||||
|
|
||||||
@ -203,10 +204,16 @@ async def adopt_devices(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
ufp: MockUFPFixture,
|
ufp: MockUFPFixture,
|
||||||
ufp_devices: list[ProtectAdoptableDeviceModel],
|
ufp_devices: list[ProtectAdoptableDeviceModel],
|
||||||
|
fully_adopt: bool = False,
|
||||||
):
|
):
|
||||||
"""Emit WS to re-adopt give Protect devices."""
|
"""Emit WS to re-adopt give Protect devices."""
|
||||||
|
|
||||||
for ufp_device in ufp_devices:
|
for ufp_device in ufp_devices:
|
||||||
|
if fully_adopt:
|
||||||
|
ufp_device.is_adopted = True
|
||||||
|
ufp_device.is_adopted_by_other = False
|
||||||
|
ufp_device.can_adopt = False
|
||||||
|
|
||||||
mock_msg = Mock()
|
mock_msg = Mock()
|
||||||
mock_msg.changed_data = {}
|
mock_msg.changed_data = {}
|
||||||
mock_msg.new_obj = Event(
|
mock_msg.new_obj = Event(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user