Skip to content

Commit

Permalink
Bump vivintpy to 2021.3.5
Browse files Browse the repository at this point in the history
Add support for event and device triggers on cameras (motion/doorbell)
  • Loading branch information
natekspencer committed Mar 19, 2021
1 parent 1312a7e commit 61c8a66
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 26 deletions.
48 changes: 42 additions & 6 deletions custom_components/vivint/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -25,6 +27,8 @@
"switch",
]

ATTR_TYPE = "type"


async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Vivint domain."""
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions custom_components/vivint/const.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Constants for the Vivint integration."""
DOMAIN = "vivint"
EVENT_TYPE = f"{DOMAIN}_event"

RTSP_STREAM_DIRECT = 0
RTSP_STREAM_INTERNAL = 1
Expand Down
98 changes: 98 additions & 0 deletions custom_components/vivint/device_trigger.py
Original file line number Diff line number Diff line change
@@ -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"
)
34 changes: 16 additions & 18 deletions custom_components/vivint/hub.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
"""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
from homeassistant.helpers.update_coordinator import (
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
Expand All @@ -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."""

Expand All @@ -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():
Expand All @@ -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:
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions custom_components/vivint/manifest.json
Original file line number Diff line number Diff line change
@@ -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"]
}
6 changes: 6 additions & 0 deletions custom_components/vivint/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,11 @@
}
}
}
},
"device_automation": {
"trigger_type": {
"doorbell_ding": "Doorbell pressed",
"motion_detected": "Motion detected"
}
}
}
6 changes: 6 additions & 0 deletions custom_components/vivint/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,11 @@
}
}
}
},
"device_automation": {
"trigger_type": {
"doorbell_ding": "Doorbell pressed",
"motion_detected": "Motion detected"
}
}
}

0 comments on commit 61c8a66

Please sign in to comment.