Add nest device triggers for camera and doorbell events (#43548)
parent
2cbb93be43
commit
945a0a9f7e
13
.coveragerc
13
.coveragerc
|
@ -572,7 +572,18 @@ omit =
|
||||||
homeassistant/components/neato/vacuum.py
|
homeassistant/components/neato/vacuum.py
|
||||||
homeassistant/components/nederlandse_spoorwegen/sensor.py
|
homeassistant/components/nederlandse_spoorwegen/sensor.py
|
||||||
homeassistant/components/nello/lock.py
|
homeassistant/components/nello/lock.py
|
||||||
homeassistant/components/nest/*
|
homeassistant/components/nest/__init__.py
|
||||||
|
homeassistant/components/nest/api.py
|
||||||
|
homeassistant/components/nest/binary_sensor.py
|
||||||
|
homeassistant/components/nest/camera.py
|
||||||
|
homeassistant/components/nest/camera_legacy.py
|
||||||
|
homeassistant/components/nest/camera_sdm.py
|
||||||
|
homeassistant/components/nest/climate.py
|
||||||
|
homeassistant/components/nest/climate_legacy.py
|
||||||
|
homeassistant/components/nest/climate_sdm.py
|
||||||
|
homeassistant/components/nest/local_auth.py
|
||||||
|
homeassistant/components/nest/sensor.py
|
||||||
|
homeassistant/components/nest/sensor_legacy.py
|
||||||
homeassistant/components/netatmo/__init__.py
|
homeassistant/components/netatmo/__init__.py
|
||||||
homeassistant/components/netatmo/api.py
|
homeassistant/components/netatmo/api.py
|
||||||
homeassistant/components/netatmo/camera.py
|
homeassistant/components/netatmo/camera.py
|
||||||
|
|
|
@ -5,14 +5,7 @@ from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from google_nest_sdm.event import (
|
from google_nest_sdm.event import AsyncEventCallback, EventMessage
|
||||||
AsyncEventCallback,
|
|
||||||
CameraMotionEvent,
|
|
||||||
CameraPersonEvent,
|
|
||||||
CameraSoundEvent,
|
|
||||||
DoorbellChimeEvent,
|
|
||||||
EventMessage,
|
|
||||||
)
|
|
||||||
from google_nest_sdm.exceptions import GoogleNestException
|
from google_nest_sdm.exceptions import GoogleNestException
|
||||||
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
|
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
|
||||||
from nest import Nest
|
from nest import Nest
|
||||||
|
@ -50,24 +43,19 @@ from . import api, config_flow, local_auth
|
||||||
from .const import (
|
from .const import (
|
||||||
API_URL,
|
API_URL,
|
||||||
DATA_SDM,
|
DATA_SDM,
|
||||||
|
DATA_SUBSCRIBER,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
OAUTH2_AUTHORIZE,
|
OAUTH2_AUTHORIZE,
|
||||||
OAUTH2_TOKEN,
|
OAUTH2_TOKEN,
|
||||||
SIGNAL_NEST_UPDATE,
|
SIGNAL_NEST_UPDATE,
|
||||||
)
|
)
|
||||||
|
from .events import EVENT_NAME_MAP, NEST_EVENT
|
||||||
|
|
||||||
_CONFIGURING = {}
|
_CONFIGURING = {}
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_PROJECT_ID = "project_id"
|
CONF_PROJECT_ID = "project_id"
|
||||||
CONF_SUBSCRIBER_ID = "subscriber_id"
|
CONF_SUBSCRIBER_ID = "subscriber_id"
|
||||||
NEST_EVENT = "nest_event"
|
|
||||||
EVENT_TRAIT_MAP = {
|
|
||||||
DoorbellChimeEvent.NAME: "DoorbellChime",
|
|
||||||
CameraMotionEvent.NAME: "CameraMotion",
|
|
||||||
CameraPersonEvent.NAME: "CameraPerson",
|
|
||||||
CameraSoundEvent.NAME: "CameraSound",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Configuration for the legacy nest API
|
# Configuration for the legacy nest API
|
||||||
|
@ -206,11 +194,12 @@ class SignalUpdateCallback(AsyncEventCallback):
|
||||||
_LOGGER.debug("Ignoring event for unregistered device '%s'", device_id)
|
_LOGGER.debug("Ignoring event for unregistered device '%s'", device_id)
|
||||||
return
|
return
|
||||||
for event in events:
|
for event in events:
|
||||||
if event not in EVENT_TRAIT_MAP:
|
event_type = EVENT_NAME_MAP.get(event)
|
||||||
|
if not event_type:
|
||||||
continue
|
continue
|
||||||
message = {
|
message = {
|
||||||
"device_id": device_entry.id,
|
"device_id": device_entry.id,
|
||||||
"type": EVENT_TRAIT_MAP[event],
|
"type": event_type,
|
||||||
}
|
}
|
||||||
self._hass.bus.async_fire(NEST_EVENT, message)
|
self._hass.bus.async_fire(NEST_EVENT, message)
|
||||||
|
|
||||||
|
@ -254,7 +243,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
subscriber.stop_async()
|
subscriber.stop_async()
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = subscriber
|
hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber
|
||||||
|
|
||||||
for component in PLATFORMS:
|
for component in PLATFORMS:
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
|
@ -270,7 +259,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
# Legacy API
|
# Legacy API
|
||||||
return True
|
return True
|
||||||
|
|
||||||
subscriber = hass.data[DOMAIN][entry.entry_id]
|
subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
|
||||||
subscriber.stop_async()
|
subscriber.stop_async()
|
||||||
unload_ok = all(
|
unload_ok = all(
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
|
@ -281,7 +270,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if unload_ok:
|
if unload_ok:
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
hass.data[DOMAIN].pop(DATA_SUBSCRIBER)
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from .const import DOMAIN, SIGNAL_NEST_UPDATE
|
from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE
|
||||||
from .device_info import DeviceInfo
|
from .device_info import DeviceInfo
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -32,7 +32,7 @@ async def async_setup_sdm_entry(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the cameras."""
|
"""Set up the cameras."""
|
||||||
|
|
||||||
subscriber = hass.data[DOMAIN][entry.entry_id]
|
subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
|
||||||
try:
|
try:
|
||||||
device_manager = await subscriber.async_get_device_manager()
|
device_manager = await subscriber.async_get_device_manager()
|
||||||
except GoogleNestException as err:
|
except GoogleNestException as err:
|
||||||
|
|
|
@ -39,7 +39,7 @@ from homeassistant.exceptions import PlatformNotReady
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from .const import DOMAIN, SIGNAL_NEST_UPDATE
|
from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE
|
||||||
from .device_info import DeviceInfo
|
from .device_info import DeviceInfo
|
||||||
|
|
||||||
# Mapping for sdm.devices.traits.ThermostatMode mode field
|
# Mapping for sdm.devices.traits.ThermostatMode mode field
|
||||||
|
@ -81,7 +81,7 @@ async def async_setup_sdm_entry(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the client entities."""
|
"""Set up the client entities."""
|
||||||
|
|
||||||
subscriber = hass.data[DOMAIN][entry.entry_id]
|
subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
|
||||||
try:
|
try:
|
||||||
device_manager = await subscriber.async_get_device_manager()
|
device_manager = await subscriber.async_get_device_manager()
|
||||||
except GoogleNestException as err:
|
except GoogleNestException as err:
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
DOMAIN = "nest"
|
DOMAIN = "nest"
|
||||||
DATA_SDM = "sdm"
|
DATA_SDM = "sdm"
|
||||||
|
DATA_SUBSCRIBER = "subscriber"
|
||||||
|
|
||||||
SIGNAL_NEST_UPDATE = "nest_update"
|
SIGNAL_NEST_UPDATE = "nest_update"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
"""Provides device automations for Nest."""
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.automation import AutomationActionType
|
||||||
|
from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
|
||||||
|
from homeassistant.components.device_automation.exceptions import (
|
||||||
|
InvalidDeviceAutomationConfig,
|
||||||
|
)
|
||||||
|
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.typing import ConfigType
|
||||||
|
|
||||||
|
from .const import DATA_SUBSCRIBER, DOMAIN
|
||||||
|
from .events import DEVICE_TRAIT_TRIGGER_MAP, NEST_EVENT
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEVICE = "device"
|
||||||
|
|
||||||
|
TRIGGER_TYPES = set(DEVICE_TRAIT_TRIGGER_MAP.values())
|
||||||
|
|
||||||
|
TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str:
|
||||||
|
"""Get the nest API device_id from the HomeAssistant device_id."""
|
||||||
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||||
|
device = device_registry.async_get(device_id)
|
||||||
|
for (domain, unique_id) in device.identifiers:
|
||||||
|
if domain == DOMAIN:
|
||||||
|
return unique_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_device_trigger_types(
|
||||||
|
hass: HomeAssistant, nest_device_id: str
|
||||||
|
) -> List[str]:
|
||||||
|
"""List event triggers supported for a Nest device."""
|
||||||
|
# All devices should have already been loaded so any failures here are
|
||||||
|
# "shouldn't happen" cases
|
||||||
|
subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
|
||||||
|
device_manager = await subscriber.async_get_device_manager()
|
||||||
|
nest_device = device_manager.devices.get(nest_device_id)
|
||||||
|
if not nest_device:
|
||||||
|
raise InvalidDeviceAutomationConfig(f"Nest device not found {nest_device_id}")
|
||||||
|
|
||||||
|
# Determine the set of event types based on the supported device traits
|
||||||
|
trigger_types = []
|
||||||
|
for trait in nest_device.traits.keys():
|
||||||
|
trigger_type = DEVICE_TRAIT_TRIGGER_MAP.get(trait)
|
||||||
|
if trigger_type:
|
||||||
|
trigger_types.append(trigger_type)
|
||||||
|
return trigger_types
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
|
||||||
|
"""List device triggers for a Nest device."""
|
||||||
|
nest_device_id = await async_get_nest_device_id(hass, device_id)
|
||||||
|
if not nest_device_id:
|
||||||
|
raise InvalidDeviceAutomationConfig(f"Device not found {device_id}")
|
||||||
|
trigger_types = await async_get_device_trigger_types(hass, nest_device_id)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
CONF_PLATFORM: DEVICE,
|
||||||
|
CONF_DEVICE_ID: device_id,
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
CONF_TYPE: trigger_type,
|
||||||
|
}
|
||||||
|
for trigger_type in trigger_types
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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: NEST_EVENT,
|
||||||
|
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"
|
||||||
|
)
|
|
@ -0,0 +1,49 @@
|
||||||
|
"""Library from Pub/sub messages, events and device triggers."""
|
||||||
|
|
||||||
|
from google_nest_sdm.camera_traits import (
|
||||||
|
CameraMotionTrait,
|
||||||
|
CameraPersonTrait,
|
||||||
|
CameraSoundTrait,
|
||||||
|
)
|
||||||
|
from google_nest_sdm.doorbell_traits import DoorbellChimeTrait
|
||||||
|
from google_nest_sdm.event import (
|
||||||
|
CameraMotionEvent,
|
||||||
|
CameraPersonEvent,
|
||||||
|
CameraSoundEvent,
|
||||||
|
DoorbellChimeEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
NEST_EVENT = "nest_event"
|
||||||
|
# The nest_event namespace will fire events that are triggered from messages
|
||||||
|
# received via the Pub/Sub subscriber.
|
||||||
|
#
|
||||||
|
# An example event data payload:
|
||||||
|
# {
|
||||||
|
# "device_id": "enterprises/some/device/identifier"
|
||||||
|
# "event_type": "camera_motion"
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# The following event types are fired:
|
||||||
|
EVENT_DOORBELL_CHIME = "doorbell_chime"
|
||||||
|
EVENT_CAMERA_MOTION = "camera_motion"
|
||||||
|
EVENT_CAMERA_PERSON = "camera_person"
|
||||||
|
EVENT_CAMERA_SOUND = "camera_sound"
|
||||||
|
|
||||||
|
# Mapping of supported device traits to home assistant event types. Devices
|
||||||
|
# that support these traits will generate Pub/Sub event messages in
|
||||||
|
# the EVENT_NAME_MAP
|
||||||
|
DEVICE_TRAIT_TRIGGER_MAP = {
|
||||||
|
DoorbellChimeTrait.NAME: EVENT_DOORBELL_CHIME,
|
||||||
|
CameraMotionTrait.NAME: EVENT_CAMERA_MOTION,
|
||||||
|
CameraPersonTrait.NAME: EVENT_CAMERA_PERSON,
|
||||||
|
CameraSoundTrait.NAME: EVENT_CAMERA_SOUND,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mapping of incoming SDM Pub/Sub event message types to the home assistant
|
||||||
|
# event type to fire.
|
||||||
|
EVENT_NAME_MAP = {
|
||||||
|
DoorbellChimeEvent.NAME: EVENT_DOORBELL_CHIME,
|
||||||
|
CameraMotionEvent.NAME: EVENT_CAMERA_MOTION,
|
||||||
|
CameraPersonEvent.NAME: EVENT_CAMERA_PERSON,
|
||||||
|
CameraSoundEvent.NAME: EVENT_CAMERA_SOUND,
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from .const import DOMAIN, SIGNAL_NEST_UPDATE
|
from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE
|
||||||
from .device_info import DeviceInfo
|
from .device_info import DeviceInfo
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -38,7 +38,7 @@ async def async_setup_sdm_entry(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the sensors."""
|
"""Set up the sensors."""
|
||||||
|
|
||||||
subscriber = hass.data[DOMAIN][entry.entry_id]
|
subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
|
||||||
try:
|
try:
|
||||||
device_manager = await subscriber.async_get_device_manager()
|
device_manager = await subscriber.async_get_device_manager()
|
||||||
except GoogleNestException as err:
|
except GoogleNestException as err:
|
||||||
|
|
|
@ -7,12 +7,16 @@
|
||||||
"init": {
|
"init": {
|
||||||
"title": "Authentication Provider",
|
"title": "Authentication Provider",
|
||||||
"description": "[%key:common::config_flow::title::oauth2_pick_implementation%]",
|
"description": "[%key:common::config_flow::title::oauth2_pick_implementation%]",
|
||||||
"data": { "flow_impl": "Provider" }
|
"data": {
|
||||||
|
"flow_impl": "Provider"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"link": {
|
"link": {
|
||||||
"title": "Link Nest Account",
|
"title": "Link Nest Account",
|
||||||
"description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.",
|
"description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.",
|
||||||
"data": { "code": "[%key:common::config_flow::data::pin%]" }
|
"data": {
|
||||||
|
"code": "[%key:common::config_flow::data::pin%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
@ -31,5 +35,13 @@
|
||||||
"create_entry": {
|
"create_entry": {
|
||||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"device_automation": {
|
||||||
|
"trigger_type": {
|
||||||
|
"camera_person": "Person detected",
|
||||||
|
"camera_motion": "Motion detected",
|
||||||
|
"camera_sound": "Sound detected",
|
||||||
|
"doorbell_chime": "Doorbell pressed"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,5 +36,13 @@
|
||||||
"title": "Pick Authentication Method"
|
"title": "Pick Authentication Method"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"device_automation": {
|
||||||
|
"trigger_type": {
|
||||||
|
"camera_person": "Person detected",
|
||||||
|
"camera_motion": "Motion detected",
|
||||||
|
"camera_sound": "Sound detected",
|
||||||
|
"doorbell_chime": "Doorbell pressed"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,313 @@
|
||||||
|
"""The tests for Nest device triggers."""
|
||||||
|
from google_nest_sdm.device import Device
|
||||||
|
from google_nest_sdm.event import EventMessage
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import homeassistant.components.automation as automation
|
||||||
|
from homeassistant.components.device_automation.exceptions import (
|
||||||
|
InvalidDeviceAutomationConfig,
|
||||||
|
)
|
||||||
|
from homeassistant.components.nest import DOMAIN, NEST_EVENT
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from .common import async_setup_sdm_platform
|
||||||
|
|
||||||
|
from tests.common import (
|
||||||
|
assert_lists_same,
|
||||||
|
async_get_device_automations,
|
||||||
|
async_mock_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
DEVICE_ID = "some-device-id"
|
||||||
|
DEVICE_NAME = "My Camera"
|
||||||
|
DATA_MESSAGE = {"message": "service-called"}
|
||||||
|
|
||||||
|
|
||||||
|
def make_camera(device_id, name=DEVICE_NAME, traits={}):
|
||||||
|
"""Create a nest camera."""
|
||||||
|
traits = traits.copy()
|
||||||
|
traits.update(
|
||||||
|
{
|
||||||
|
"sdm.devices.traits.Info": {
|
||||||
|
"customName": name,
|
||||||
|
},
|
||||||
|
"sdm.devices.traits.CameraLiveStream": {
|
||||||
|
"maxVideoResolution": {
|
||||||
|
"width": 640,
|
||||||
|
"height": 480,
|
||||||
|
},
|
||||||
|
"videoCodecs": ["H264"],
|
||||||
|
"audioCodecs": ["AAC"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return Device.MakeDevice(
|
||||||
|
{
|
||||||
|
"name": device_id,
|
||||||
|
"type": "sdm.devices.types.CAMERA",
|
||||||
|
"traits": traits,
|
||||||
|
},
|
||||||
|
auth=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_camera(hass, devices=None):
|
||||||
|
"""Set up the platform and prerequisites for testing available triggers."""
|
||||||
|
if not devices:
|
||||||
|
devices = {DEVICE_ID: make_camera(device_id=DEVICE_ID)}
|
||||||
|
return await async_setup_sdm_platform(hass, "camera", devices)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_automation(hass, device_id, trigger_type):
|
||||||
|
"""Set up an automation trigger for testing triggering."""
|
||||||
|
return await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: [
|
||||||
|
{
|
||||||
|
"trigger": {
|
||||||
|
"platform": "device",
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"device_id": device_id,
|
||||||
|
"type": trigger_type,
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
"data": DATA_MESSAGE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def calls(hass):
|
||||||
|
"""Track calls to a mock service."""
|
||||||
|
return async_mock_service(hass, "test", "automation")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_triggers(hass):
|
||||||
|
"""Test we get the expected triggers from a nest."""
|
||||||
|
camera = make_camera(
|
||||||
|
device_id=DEVICE_ID,
|
||||||
|
traits={
|
||||||
|
"sdm.devices.traits.CameraMotion": {},
|
||||||
|
"sdm.devices.traits.CameraPerson": {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await async_setup_camera(hass, {DEVICE_ID: camera})
|
||||||
|
|
||||||
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||||
|
device_entry = device_registry.async_get_device(
|
||||||
|
{("nest", DEVICE_ID)}, connections={}
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_triggers = [
|
||||||
|
{
|
||||||
|
"platform": "device",
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"type": "camera_motion",
|
||||||
|
"device_id": device_entry.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "device",
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"type": "camera_person",
|
||||||
|
"device_id": device_entry.id,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
|
||||||
|
assert_lists_same(triggers, expected_triggers)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_multiple_devices(hass):
|
||||||
|
"""Test we get the expected triggers from a nest."""
|
||||||
|
camera1 = make_camera(
|
||||||
|
device_id="device-id-1",
|
||||||
|
name="Camera 1",
|
||||||
|
traits={
|
||||||
|
"sdm.devices.traits.CameraSound": {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
camera2 = make_camera(
|
||||||
|
device_id="device-id-2",
|
||||||
|
name="Camera 2",
|
||||||
|
traits={
|
||||||
|
"sdm.devices.traits.DoorbellChime": {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await async_setup_camera(hass, {"device-id-1": camera1, "device-id-2": camera2})
|
||||||
|
|
||||||
|
registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
entry1 = registry.async_get("camera.camera_1")
|
||||||
|
assert entry1.unique_id == "device-id-1-camera"
|
||||||
|
entry2 = registry.async_get("camera.camera_2")
|
||||||
|
assert entry2.unique_id == "device-id-2-camera"
|
||||||
|
|
||||||
|
triggers = await async_get_device_automations(hass, "trigger", entry1.device_id)
|
||||||
|
assert len(triggers) == 1
|
||||||
|
assert {
|
||||||
|
"platform": "device",
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"type": "camera_sound",
|
||||||
|
"device_id": entry1.device_id,
|
||||||
|
} == triggers[0]
|
||||||
|
|
||||||
|
triggers = await async_get_device_automations(hass, "trigger", entry2.device_id)
|
||||||
|
assert len(triggers) == 1
|
||||||
|
assert {
|
||||||
|
"platform": "device",
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"type": "doorbell_chime",
|
||||||
|
"device_id": entry2.device_id,
|
||||||
|
} == triggers[0]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_triggers_for_invalid_device_id(hass):
|
||||||
|
"""Get triggers for a device not found in the API."""
|
||||||
|
camera = make_camera(
|
||||||
|
device_id=DEVICE_ID,
|
||||||
|
traits={
|
||||||
|
"sdm.devices.traits.CameraMotion": {},
|
||||||
|
"sdm.devices.traits.CameraPerson": {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await async_setup_camera(hass, {DEVICE_ID: camera})
|
||||||
|
|
||||||
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||||
|
device_entry = device_registry.async_get_device(
|
||||||
|
{("nest", DEVICE_ID)}, connections={}
|
||||||
|
)
|
||||||
|
assert device_entry is not None
|
||||||
|
|
||||||
|
# Create an additional device that does not exist. Fetching supported
|
||||||
|
# triggers for an unknown device will fail.
|
||||||
|
assert len(device_entry.config_entries) == 1
|
||||||
|
config_entry_id = next(iter(device_entry.config_entries))
|
||||||
|
device_entry_2 = device_registry.async_get_or_create(
|
||||||
|
config_entry_id=config_entry_id, identifiers={(DOMAIN, "some-unknown-nest-id")}
|
||||||
|
)
|
||||||
|
assert device_entry_2 is not None
|
||||||
|
|
||||||
|
with pytest.raises(InvalidDeviceAutomationConfig):
|
||||||
|
await async_get_device_automations(hass, "trigger", device_entry_2.id)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_triggers(hass):
|
||||||
|
"""Test we get the expected triggers from a nest."""
|
||||||
|
camera = make_camera(device_id=DEVICE_ID, traits={})
|
||||||
|
await async_setup_camera(hass, {DEVICE_ID: camera})
|
||||||
|
|
||||||
|
registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
entry = registry.async_get("camera.my_camera")
|
||||||
|
assert entry.unique_id == "some-device-id-camera"
|
||||||
|
|
||||||
|
triggers = await async_get_device_automations(hass, "trigger", entry.device_id)
|
||||||
|
assert [] == triggers
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fires_on_camera_motion(hass, calls):
|
||||||
|
"""Test camera_motion triggers firing."""
|
||||||
|
assert await setup_automation(hass, DEVICE_ID, "camera_motion")
|
||||||
|
|
||||||
|
message = {"device_id": DEVICE_ID, "type": "camera_motion"}
|
||||||
|
hass.bus.async_fire(NEST_EVENT, message)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0].data == DATA_MESSAGE
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fires_on_camera_person(hass, calls):
|
||||||
|
"""Test camera_person triggers firing."""
|
||||||
|
assert await setup_automation(hass, DEVICE_ID, "camera_person")
|
||||||
|
|
||||||
|
message = {"device_id": DEVICE_ID, "type": "camera_person"}
|
||||||
|
hass.bus.async_fire(NEST_EVENT, message)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0].data == DATA_MESSAGE
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fires_on_camera_sound(hass, calls):
|
||||||
|
"""Test camera_person triggers firing."""
|
||||||
|
assert await setup_automation(hass, DEVICE_ID, "camera_sound")
|
||||||
|
|
||||||
|
message = {"device_id": DEVICE_ID, "type": "camera_sound"}
|
||||||
|
hass.bus.async_fire(NEST_EVENT, message)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0].data == DATA_MESSAGE
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fires_on_doorbell_chime(hass, calls):
|
||||||
|
"""Test doorbell_chime triggers firing."""
|
||||||
|
assert await setup_automation(hass, DEVICE_ID, "doorbell_chime")
|
||||||
|
|
||||||
|
message = {"device_id": DEVICE_ID, "type": "doorbell_chime"}
|
||||||
|
hass.bus.async_fire(NEST_EVENT, message)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0].data == DATA_MESSAGE
|
||||||
|
|
||||||
|
|
||||||
|
async def test_trigger_for_wrong_device_id(hass, calls):
|
||||||
|
"""Test for turn_on and turn_off triggers firing."""
|
||||||
|
assert await setup_automation(hass, DEVICE_ID, "camera_motion")
|
||||||
|
|
||||||
|
message = {"device_id": "wrong-device-id", "type": "camera_motion"}
|
||||||
|
hass.bus.async_fire(NEST_EVENT, message)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_trigger_for_wrong_event_type(hass, calls):
|
||||||
|
"""Test for turn_on and turn_off triggers firing."""
|
||||||
|
assert await setup_automation(hass, DEVICE_ID, "camera_motion")
|
||||||
|
|
||||||
|
message = {"device_id": DEVICE_ID, "type": "wrong-event-type"}
|
||||||
|
hass.bus.async_fire(NEST_EVENT, message)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_subscriber_automation(hass, calls):
|
||||||
|
"""Test end to end subscriber triggers automation."""
|
||||||
|
camera = make_camera(
|
||||||
|
device_id=DEVICE_ID,
|
||||||
|
traits={
|
||||||
|
"sdm.devices.traits.CameraMotion": {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
subscriber = await async_setup_camera(hass, {DEVICE_ID: camera})
|
||||||
|
|
||||||
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||||
|
device_entry = device_registry.async_get_device(
|
||||||
|
{("nest", DEVICE_ID)}, connections={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert await setup_automation(hass, device_entry.id, "camera_motion")
|
||||||
|
|
||||||
|
# Simulate a pubsub message received by the subscriber with a motion event
|
||||||
|
event = EventMessage(
|
||||||
|
{
|
||||||
|
"eventId": "some-event-id",
|
||||||
|
"timestamp": "2019-01-01T00:00:01Z",
|
||||||
|
"resourceUpdate": {
|
||||||
|
"name": DEVICE_ID,
|
||||||
|
"events": {
|
||||||
|
"sdm.devices.events.CameraMotion.Motion": {
|
||||||
|
"eventSessionId": "CjY5Y3VKaTZwR3o4Y19YbTVfMF...",
|
||||||
|
"eventId": "FWWVQVUdGNUlTU2V4MGV2aTNXV...",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth=None,
|
||||||
|
)
|
||||||
|
await subscriber.async_receive_event(event)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0].data == DATA_MESSAGE
|
|
@ -110,7 +110,7 @@ async def test_doorbell_chime_event(hass):
|
||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
assert events[0].data == {
|
assert events[0].data == {
|
||||||
"device_id": entry.device_id,
|
"device_id": entry.device_id,
|
||||||
"type": "DoorbellChime",
|
"type": "doorbell_chime",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ async def test_camera_motion_event(hass):
|
||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
assert events[0].data == {
|
assert events[0].data == {
|
||||||
"device_id": entry.device_id,
|
"device_id": entry.device_id,
|
||||||
"type": "CameraMotion",
|
"type": "camera_motion",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ async def test_camera_sound_event(hass):
|
||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
assert events[0].data == {
|
assert events[0].data == {
|
||||||
"device_id": entry.device_id,
|
"device_id": entry.device_id,
|
||||||
"type": "CameraSound",
|
"type": "camera_sound",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -182,7 +182,7 @@ async def test_camera_person_event(hass):
|
||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
assert events[0].data == {
|
assert events[0].data == {
|
||||||
"device_id": entry.device_id,
|
"device_id": entry.device_id,
|
||||||
"type": "CameraPerson",
|
"type": "camera_person",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -215,11 +215,11 @@ async def test_camera_multiple_event(hass):
|
||||||
assert len(events) == 2
|
assert len(events) == 2
|
||||||
assert events[0].data == {
|
assert events[0].data == {
|
||||||
"device_id": entry.device_id,
|
"device_id": entry.device_id,
|
||||||
"type": "CameraMotion",
|
"type": "camera_motion",
|
||||||
}
|
}
|
||||||
assert events[1].data == {
|
assert events[1].data == {
|
||||||
"device_id": entry.device_id,
|
"device_id": entry.device_id,
|
||||||
"type": "CameraPerson",
|
"type": "camera_person",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue