mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 09:47:13 +00:00
Add nest event platform (#123042)
* Add nest event platform * Translate entities * Put motion events into a single entity type * Remove none types * Set event entity descriptions as kw only * Update translations for event entities * Add single event entity per trait type * Update translation keys
This commit is contained in:
parent
32f75597a9
commit
3e3d27f48d
@ -97,7 +97,7 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Platforms for SDM API
|
# Platforms for SDM API
|
||||||
PLATFORMS = [Platform.CAMERA, Platform.CLIMATE, Platform.SENSOR]
|
PLATFORMS = [Platform.CAMERA, Platform.CLIMATE, Platform.EVENT, Platform.SENSOR]
|
||||||
|
|
||||||
# Fetch media events with a disk backed cache, with a limit for each camera
|
# Fetch media events with a disk backed cache, with a limit for each camera
|
||||||
# device. The largest media items are mp4 clips at ~120kb each, and we target
|
# device. The largest media items are mp4 clips at ~120kb each, and we target
|
||||||
|
124
homeassistant/components/nest/event.py
Normal file
124
homeassistant/components/nest/event.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
"""Event platform for Google Nest."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from google_nest_sdm.device import Device
|
||||||
|
from google_nest_sdm.device_manager import DeviceManager
|
||||||
|
from google_nest_sdm.event import EventMessage, EventType
|
||||||
|
from google_nest_sdm.traits import TraitType
|
||||||
|
|
||||||
|
from homeassistant.components.event import (
|
||||||
|
EventDeviceClass,
|
||||||
|
EventEntity,
|
||||||
|
EventEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import DATA_DEVICE_MANAGER, DOMAIN
|
||||||
|
from .device_info import NestDeviceInfo
|
||||||
|
from .events import (
|
||||||
|
EVENT_CAMERA_MOTION,
|
||||||
|
EVENT_CAMERA_PERSON,
|
||||||
|
EVENT_CAMERA_SOUND,
|
||||||
|
EVENT_DOORBELL_CHIME,
|
||||||
|
EVENT_NAME_MAP,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True, frozen=True)
|
||||||
|
class NestEventEntityDescription(EventEntityDescription):
|
||||||
|
"""Entity description for nest event entities."""
|
||||||
|
|
||||||
|
trait_types: list[TraitType]
|
||||||
|
api_event_types: list[EventType]
|
||||||
|
event_types: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
ENTITY_DESCRIPTIONS = [
|
||||||
|
NestEventEntityDescription(
|
||||||
|
key=EVENT_DOORBELL_CHIME,
|
||||||
|
translation_key="chime",
|
||||||
|
device_class=EventDeviceClass.DOORBELL,
|
||||||
|
event_types=[EVENT_DOORBELL_CHIME],
|
||||||
|
trait_types=[TraitType.DOORBELL_CHIME],
|
||||||
|
api_event_types=[EventType.DOORBELL_CHIME],
|
||||||
|
),
|
||||||
|
NestEventEntityDescription(
|
||||||
|
key=EVENT_CAMERA_MOTION,
|
||||||
|
translation_key="motion",
|
||||||
|
device_class=EventDeviceClass.MOTION,
|
||||||
|
event_types=[EVENT_CAMERA_MOTION, EVENT_CAMERA_PERSON, EVENT_CAMERA_SOUND],
|
||||||
|
trait_types=[
|
||||||
|
TraitType.CAMERA_MOTION,
|
||||||
|
TraitType.CAMERA_PERSON,
|
||||||
|
TraitType.CAMERA_SOUND,
|
||||||
|
],
|
||||||
|
api_event_types=[
|
||||||
|
EventType.CAMERA_MOTION,
|
||||||
|
EventType.CAMERA_PERSON,
|
||||||
|
EventType.CAMERA_SOUND,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
"""Set up the sensors."""
|
||||||
|
|
||||||
|
device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][
|
||||||
|
DATA_DEVICE_MANAGER
|
||||||
|
]
|
||||||
|
async_add_entities(
|
||||||
|
NestTraitEventEntity(desc, device)
|
||||||
|
for device in device_manager.devices.values()
|
||||||
|
for desc in ENTITY_DESCRIPTIONS
|
||||||
|
if any(trait in device.traits for trait in desc.trait_types)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NestTraitEventEntity(EventEntity):
|
||||||
|
"""Nest doorbell event entity."""
|
||||||
|
|
||||||
|
entity_description: NestEventEntityDescription
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, entity_description: NestEventEntityDescription, device: Device
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the event entity."""
|
||||||
|
self.entity_description = entity_description
|
||||||
|
self._device = device
|
||||||
|
self._attr_unique_id = f"{device.name}-{entity_description.key}"
|
||||||
|
self._attr_device_info = NestDeviceInfo(device).device_info
|
||||||
|
|
||||||
|
async def _async_handle_event(self, event_message: EventMessage) -> None:
|
||||||
|
"""Handle a device event."""
|
||||||
|
if (
|
||||||
|
event_message.relation_update
|
||||||
|
or not event_message.resource_update_name
|
||||||
|
or not (events := event_message.resource_update_events)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
for api_event_type, nest_event in events.items():
|
||||||
|
if api_event_type not in self.entity_description.api_event_types:
|
||||||
|
continue
|
||||||
|
|
||||||
|
event_type = EVENT_NAME_MAP[api_event_type]
|
||||||
|
|
||||||
|
self._trigger_event(
|
||||||
|
event_type,
|
||||||
|
{"nest_event_id": nest_event.event_token},
|
||||||
|
)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
return
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Run when entity is added to attach an event listener."""
|
||||||
|
self.async_on_remove(self._device.add_event_callback(self._async_handle_event))
|
@ -72,5 +72,31 @@
|
|||||||
"title": "Legacy Works With Nest has been removed",
|
"title": "Legacy Works With Nest has been removed",
|
||||||
"description": "Legacy Works With Nest has been removed from Home Assistant, and the API shuts down as of September 2023.\n\nYou must take action to use the SDM API. Remove all `nest` configuration from `configuration.yaml` and restart Home Assistant, then see the Nest [integration instructions]({documentation_url}) for set up instructions and supported devices."
|
"description": "Legacy Works With Nest has been removed from Home Assistant, and the API shuts down as of September 2023.\n\nYou must take action to use the SDM API. Remove all `nest` configuration from `configuration.yaml` and restart Home Assistant, then see the Nest [integration instructions]({documentation_url}) for set up instructions and supported devices."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"event": {
|
||||||
|
"chime": {
|
||||||
|
"name": "Chime",
|
||||||
|
"state_attributes": {
|
||||||
|
"event_type": {
|
||||||
|
"state": {
|
||||||
|
"doorbell_chime": "[%key:component::nest::entity::event::chime::name%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"motion": {
|
||||||
|
"name": "[%key:component::event::entity_component::motion::name%]",
|
||||||
|
"state_attributes": {
|
||||||
|
"event_type": {
|
||||||
|
"state": {
|
||||||
|
"camera_motion": "[%key:component::event::entity_component::motion::name%]",
|
||||||
|
"camera_person": "Person",
|
||||||
|
"camera_sound": "Sound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
193
tests/components/nest/test_event.py
Normal file
193
tests/components/nest/test_event.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
"""Test for Nest event platform."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from google_nest_sdm.event import EventMessage, EventType
|
||||||
|
from google_nest_sdm.traits import TraitType
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
|
from .common import DEVICE_ID, CreateDevice
|
||||||
|
from .conftest import FakeSubscriber, PlatformSetup
|
||||||
|
|
||||||
|
EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..."
|
||||||
|
EVENT_ID = "FWWVQVUdGNUlTU2V4MGV2aTNXV..."
|
||||||
|
ENCODED_EVENT_ID = "WyJDalk1WTNWS2FUWndSM280WTE5WWJUVmZNRi4uLiIsICJGV1dWUVZVZEdOVWxUVTJWNE1HVjJhVE5YVi4uLiJd"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def platforms() -> list[Platform]:
|
||||||
|
"""Fixture for platforms to setup."""
|
||||||
|
return [Platform.EVENT]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def device_type() -> str:
|
||||||
|
"""Fixture for the type of device under test."""
|
||||||
|
return "sdm.devices.types.DOORBELL"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def device_traits() -> dict[str, Any]:
|
||||||
|
"""Fixture to set default device traits used when creating devices."""
|
||||||
|
return {
|
||||||
|
"sdm.devices.traits.Info": {
|
||||||
|
"customName": "Front",
|
||||||
|
},
|
||||||
|
"sdm.devices.traits.CameraLiveStream": {
|
||||||
|
"maxVideoResolution": {
|
||||||
|
"width": 640,
|
||||||
|
"height": 480,
|
||||||
|
},
|
||||||
|
"videoCodecs": ["H264"],
|
||||||
|
"audioCodecs": ["AAC"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_events(events: str) -> EventMessage:
|
||||||
|
"""Create an EventMessage for events."""
|
||||||
|
return EventMessage.create_event(
|
||||||
|
{
|
||||||
|
"eventId": "some-event-id",
|
||||||
|
"timestamp": utcnow().isoformat(timespec="seconds"),
|
||||||
|
"resourceUpdate": {
|
||||||
|
"name": DEVICE_ID,
|
||||||
|
"events": {
|
||||||
|
event: {
|
||||||
|
"eventSessionId": EVENT_SESSION_ID,
|
||||||
|
"eventId": EVENT_ID,
|
||||||
|
}
|
||||||
|
for event in events
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
(
|
||||||
|
"trait_types",
|
||||||
|
"entity_id",
|
||||||
|
"expected_attributes",
|
||||||
|
"api_event_type",
|
||||||
|
"expected_event_type",
|
||||||
|
),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
[TraitType.DOORBELL_CHIME, TraitType.CAMERA_MOTION],
|
||||||
|
"event.front_chime",
|
||||||
|
{
|
||||||
|
"device_class": "doorbell",
|
||||||
|
"event_types": ["doorbell_chime"],
|
||||||
|
"friendly_name": "Front Chime",
|
||||||
|
},
|
||||||
|
EventType.DOORBELL_CHIME,
|
||||||
|
"doorbell_chime",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[TraitType.CAMERA_MOTION, TraitType.CAMERA_PERSON, TraitType.CAMERA_SOUND],
|
||||||
|
"event.front_motion",
|
||||||
|
{
|
||||||
|
"device_class": "motion",
|
||||||
|
"event_types": ["camera_motion", "camera_person", "camera_sound"],
|
||||||
|
"friendly_name": "Front Motion",
|
||||||
|
},
|
||||||
|
EventType.CAMERA_MOTION,
|
||||||
|
"camera_motion",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[TraitType.CAMERA_MOTION, TraitType.CAMERA_PERSON, TraitType.CAMERA_SOUND],
|
||||||
|
"event.front_motion",
|
||||||
|
{
|
||||||
|
"device_class": "motion",
|
||||||
|
"event_types": ["camera_motion", "camera_person", "camera_sound"],
|
||||||
|
"friendly_name": "Front Motion",
|
||||||
|
},
|
||||||
|
EventType.CAMERA_PERSON,
|
||||||
|
"camera_person",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[TraitType.CAMERA_MOTION, TraitType.CAMERA_PERSON, TraitType.CAMERA_SOUND],
|
||||||
|
"event.front_motion",
|
||||||
|
{
|
||||||
|
"device_class": "motion",
|
||||||
|
"event_types": ["camera_motion", "camera_person", "camera_sound"],
|
||||||
|
"friendly_name": "Front Motion",
|
||||||
|
},
|
||||||
|
EventType.CAMERA_SOUND,
|
||||||
|
"camera_sound",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_receive_events(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
subscriber: FakeSubscriber,
|
||||||
|
setup_platform: PlatformSetup,
|
||||||
|
create_device: CreateDevice,
|
||||||
|
trait_types: list[TraitType],
|
||||||
|
entity_id: str,
|
||||||
|
expected_attributes: dict[str, str],
|
||||||
|
api_event_type: EventType,
|
||||||
|
expected_event_type: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test a pubsub message for a camera person event."""
|
||||||
|
create_device.create(
|
||||||
|
raw_traits={
|
||||||
|
**{trait_type: {} for trait_type in trait_types},
|
||||||
|
api_event_type: {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await setup_platform()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == "unknown"
|
||||||
|
assert state.attributes == {
|
||||||
|
**expected_attributes,
|
||||||
|
"event_type": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
await subscriber.async_receive_event(create_events([api_event_type]))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state != "unknown"
|
||||||
|
assert state.attributes == {
|
||||||
|
**expected_attributes,
|
||||||
|
"event_type": expected_event_type,
|
||||||
|
"nest_event_id": ENCODED_EVENT_ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("trait_type"), [(TraitType.DOORBELL_CHIME)])
|
||||||
|
async def test_ignore_unrelated_event(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
subscriber: FakeSubscriber,
|
||||||
|
setup_platform: PlatformSetup,
|
||||||
|
create_device: CreateDevice,
|
||||||
|
trait_type: TraitType,
|
||||||
|
) -> None:
|
||||||
|
"""Test a pubsub message for a camera person event."""
|
||||||
|
create_device.create(
|
||||||
|
raw_traits={
|
||||||
|
trait_type: {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await setup_platform()
|
||||||
|
|
||||||
|
# Device does not have traits matching this event type
|
||||||
|
await subscriber.async_receive_event(create_events([EventType.CAMERA_MOTION]))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("event.front_chime")
|
||||||
|
assert state.state == "unknown"
|
||||||
|
assert state.attributes == {
|
||||||
|
"device_class": "doorbell",
|
||||||
|
"event_type": None,
|
||||||
|
"event_types": ["doorbell_chime"],
|
||||||
|
"friendly_name": "Front Chime",
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user