diff --git a/custom_components/vivint/__init__.py b/custom_components/vivint/__init__.py index aea1c24..aa1332f 100644 --- a/custom_components/vivint/__init__.py +++ b/custom_components/vivint/__init__.py @@ -1,17 +1,19 @@ """The Vivint integration.""" import asyncio -import logging from aiohttp import ClientResponseError from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.const import ATTR_DEVICE_ID, ATTR_DOMAIN +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry +from vivintpy.devices import VivintDevice +from vivintpy.devices.camera import DOORBELL_DING, MOTION_DETECTED, Camera +from vivintpy.enums import CapabilityCategoryType from vivintpy.exceptions import VivintSkyApiAuthenticationError, VivintSkyApiError -from .const import DOMAIN -from .hub import VivintHub - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, EVENT_TYPE +from .hub import VivintHub, get_device_id PLATFORMS = [ "alarm_control_panel", @@ -25,6 +27,8 @@ "switch", ] +ATTR_TYPE = "type" + async def async_setup(hass: HomeAssistant, config: dict): """Set up the Vivint domain.""" @@ -46,6 +50,38 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except (VivintSkyApiError, ClientResponseError) as ex: raise ConfigEntryNotReady from ex + dev_reg = await device_registry.async_get_registry(hass) + + @callback + def async_on_device_event(event_type: str, viv_device: VivintDevice) -> None: + """Relay Vivint device event to hass.""" + device = dev_reg.async_get_device({get_device_id(viv_device)}) + hass.bus.async_fire( + EVENT_TYPE, + { + ATTR_TYPE: event_type, + ATTR_DOMAIN: DOMAIN, + ATTR_DEVICE_ID: device.id, + }, + ) + + for system in hub.account.systems: + for alarm_panel in system.alarm_panels: + for device in alarm_panel.get_devices([Camera]): + device.on( + MOTION_DETECTED, + lambda event: async_on_device_event( + MOTION_DETECTED, event["device"] + ), + ) + if CapabilityCategoryType.DOORBELL in device.capabilities.keys(): + device.on( + DOORBELL_DING, + lambda event: async_on_device_event( + DOORBELL_DING, event["device"] + ), + ) + for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) diff --git a/custom_components/vivint/const.py b/custom_components/vivint/const.py index ebe6ad2..320e343 100644 --- a/custom_components/vivint/const.py +++ b/custom_components/vivint/const.py @@ -1,5 +1,6 @@ """Constants for the Vivint integration.""" DOMAIN = "vivint" +EVENT_TYPE = f"{DOMAIN}_event" RTSP_STREAM_DIRECT = 0 RTSP_STREAM_INTERNAL = 1 diff --git a/custom_components/vivint/device_trigger.py b/custom_components/vivint/device_trigger.py new file mode 100644 index 0000000..5b02b76 --- /dev/null +++ b/custom_components/vivint/device_trigger.py @@ -0,0 +1,98 @@ +"""Provides device triggers for Vivint.""" +from typing import List, Optional + +import voluptuous as vol +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.homeassistant.triggers import event as event_trigger +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.typing import ConfigType +from vivintpy.devices import VivintDevice +from vivintpy.devices.camera import DOORBELL_DING, MOTION_DETECTED, Camera +from vivintpy.enums import CapabilityCategoryType + +from .const import DOMAIN, EVENT_TYPE +from .hub import VivintHub + +TRIGGER_TYPES = {MOTION_DETECTED, DOORBELL_DING} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_vivint_device( + hass: HomeAssistant, device_id: str +) -> Optional[VivintDevice]: + """Get a Vivint device for the given device registry id.""" + device_registry: DeviceRegistry = ( + await hass.helpers.device_registry.async_get_registry() + ) + registry_device = device_registry.async_get(device_id) + identifier = list(list(registry_device.identifiers)[0])[1] + [panel_id, vivint_device_id] = [int(item) for item in identifier.split("-")] + for config_entry_id in registry_device.config_entries: + hub: VivintHub = hass.data[DOMAIN].get(config_entry_id) + for system in hub.account.systems: + if system.id != panel_id: + continue + for alarm_panel in system.alarm_panels: + for device in alarm_panel.devices: + if device.id == vivint_device_id: + return device + return None + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """Return a list of triggers.""" + device = await async_get_vivint_device(hass, device_id) + + triggers = [] + + if device and isinstance(device, Camera): + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: MOTION_DETECTED, + } + ) + if CapabilityCategoryType.DOORBELL in device.capabilities.keys(): + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: DOORBELL_DING, + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + event_config = event_trigger.TRIGGER_SCHEMA( + { + event_trigger.CONF_PLATFORM: "event", + event_trigger.CONF_EVENT_TYPE: EVENT_TYPE, + event_trigger.CONF_EVENT_DATA: { + CONF_DEVICE_ID: config[CONF_DEVICE_ID], + CONF_TYPE: config[CONF_TYPE], + }, + } + ) + return await event_trigger.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) diff --git a/custom_components/vivint/hub.py b/custom_components/vivint/hub.py index 561df34..1ed41e3 100644 --- a/custom_components/vivint/hub.py +++ b/custom_components/vivint/hub.py @@ -1,9 +1,8 @@ """A wrapper 'hub' for the Vivint API and base entity for common attributes.""" import logging from datetime import timedelta -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable, Dict, Optional, Tuple -import vivintpy.account from aiohttp import ClientResponseError from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback @@ -11,7 +10,10 @@ CoordinatorEntity, DataUpdateCoordinator, ) +from vivintpy.account import Account +from vivintpy.devices import VivintDevice from vivintpy.devices.alarm_panel import AlarmPanel +from vivintpy.entity import UPDATE from vivintpy.exceptions import VivintSkyApiAuthenticationError, VivintSkyApiError from .const import DOMAIN @@ -21,6 +23,12 @@ UPDATE_INTERVAL = 300 +@callback +def get_device_id(device: VivintDevice) -> Tuple[str, str]: + """Get device registry identifier for device.""" + return (DOMAIN, f"{device.panel_id}-{device.id}") + + class VivintHub: """A Vivint hub wrapper class.""" @@ -30,7 +38,7 @@ def __init__( """Initialize the Vivint hub.""" self._data = data self.undo_listener = undo_listener - self.account: vivintpy.account.Account = None + self.account: Account = None self.logged_in = False async def _async_update_data(): @@ -50,7 +58,7 @@ async def login( ): """Login to Vivint.""" self.logged_in = False - self.account = vivintpy.account.Account( + self.account = Account( username=self._data[CONF_USERNAME], password=self._data[CONF_PASSWORD] ) try: @@ -71,42 +79,32 @@ async def login( class VivintEntity(CoordinatorEntity): """Generic Vivint entity representing common data and methods.""" - def __init__(self, device: vivintpy.devices.VivintDevice, hub: VivintHub): + def __init__(self, device: VivintDevice, hub: VivintHub): """Pass coordinator to CoordinatorEntity.""" super().__init__(hub.coordinator) self.device = device self.hub = hub @callback - def _update_callback(self) -> None: + def _update_callback(self, _) -> None: """Call from dispatcher when state changes.""" self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Set up a listener for the entity.""" await super().async_added_to_hass() - self.device.add_update_callback(self._update_callback) + self.device.on(UPDATE, self._update_callback) @property def name(self): """Return the name of this entity.""" return self.device.name - # @property - # def unique_id(self): - # """Return a unique ID.""" - # return f"{self.robot.serial}-{self.entity_type}" - - # @property - # def available(self): - # """Return availability.""" - # return self.hub.logged_in - @property def device_info(self) -> Dict[str, Any]: """Return the device information for a Vivint device.""" return { - "identifiers": {(DOMAIN, self.device.serial_number or self.unique_id)}, + "identifiers": {get_device_id(self.device)}, "name": self.device.name, "manufacturer": self.device.manufacturer, "model": self.device.model, diff --git a/custom_components/vivint/manifest.json b/custom_components/vivint/manifest.json index 76eb70d..1bda260 100644 --- a/custom_components/vivint/manifest.json +++ b/custom_components/vivint/manifest.json @@ -1,11 +1,11 @@ { "domain": "vivint", "name": "Vivint", - "version": "2021.3.3", + "version": "2021.3.4", "config_flow": true, "documentation": "https://github.com/natekspencer/hacs-vivint", "issue_tracker": "https://github.com/natekspencer/hacs-vivint/issues", - "requirements": ["vivintpy==2021.3.3"], + "requirements": ["vivintpy==2021.3.5"], "dependencies": ["ffmpeg"], "codeowners": ["@natekspencer"] } diff --git a/custom_components/vivint/strings.json b/custom_components/vivint/strings.json index a393db4..ca3d1ac 100644 --- a/custom_components/vivint/strings.json +++ b/custom_components/vivint/strings.json @@ -26,5 +26,11 @@ } } } + }, + "device_automation": { + "trigger_type": { + "doorbell_ding": "Doorbell pressed", + "motion_detected": "Motion detected" + } } } diff --git a/custom_components/vivint/translations/en.json b/custom_components/vivint/translations/en.json index ad8d79a..1b6653b 100644 --- a/custom_components/vivint/translations/en.json +++ b/custom_components/vivint/translations/en.json @@ -26,5 +26,11 @@ } } } + }, + "device_automation": { + "trigger_type": { + "doorbell_ding": "Doorbell pressed", + "motion_detected": "Motion detected" + } } }