forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add nest event platform (home-assistant#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
- Loading branch information
1 parent
32f7559
commit 3e3d27f
Showing
4 changed files
with
344 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
} |