Add logbook describe event support to ZHA (#73077)

pull/73168/head^2
David F. Mulcahey 2022-06-07 12:49:40 -04:00 committed by GitHub
parent 73f2bca377
commit a5dc7c5f28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 307 additions and 5 deletions

View File

@ -163,7 +163,7 @@ class Channels:
def zha_send_event(self, event_data: dict[str, str | int]) -> None:
"""Relay events to hass."""
self.zha_device.hass.bus.async_fire(
"zha_event",
const.ZHA_EVENT,
{
const.ATTR_DEVICE_IEEE: str(self.zha_device.ieee),
const.ATTR_UNIQUE_ID: self.unique_id,

View File

@ -374,6 +374,7 @@ ZHA_CHANNEL_MSG_CFG_RPT = "zha_channel_configure_reporting"
ZHA_CHANNEL_MSG_DATA = "zha_channel_msg_data"
ZHA_CHANNEL_CFG_DONE = "zha_channel_cfg_done"
ZHA_CHANNEL_READS_PER_REQ = 5
ZHA_EVENT = "zha_event"
ZHA_GW_MSG = "zha_gateway_message"
ZHA_GW_MSG_DEVICE_FULL_INIT = "device_fully_initialized"
ZHA_GW_MSG_DEVICE_INFO = "device_info"

View File

@ -5,6 +5,7 @@ import asyncio
from collections.abc import Callable
from datetime import timedelta
from enum import Enum
from functools import cached_property
import logging
import random
import time
@ -280,7 +281,16 @@ class ZHADevice(LogMixin):
"""Return the gateway for this device."""
return self._zha_gateway
@property
@cached_property
def device_automation_commands(self) -> dict[str, list[tuple[str, str]]]:
"""Return the a lookup of commands to etype/sub_type."""
commands: dict[str, list[tuple[str, str]]] = {}
for etype_subtype, trigger in self.device_automation_triggers.items():
if command := trigger.get(ATTR_COMMAND):
commands.setdefault(command, []).append(etype_subtype)
return commands
@cached_property
def device_automation_triggers(self) -> dict[tuple[str, str], dict[str, str]]:
"""Return the device automation triggers for this device."""
triggers = {

View File

@ -1,4 +1,5 @@
"""Provides device automations for ZHA devices that emit events."""
import voluptuous as vol
from homeassistant.components.automation import (
@ -16,12 +17,12 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType
from . import DOMAIN
from .core.const import ZHA_EVENT
from .core.helpers import async_get_zha_device
CONF_SUBTYPE = "subtype"
DEVICE = "device"
DEVICE_IEEE = "device_ieee"
ZHA_EVENT = "zha_event"
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str}

View File

@ -0,0 +1,81 @@
"""Describe ZHA logbook events."""
from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING
from homeassistant.components.logbook.const import (
LOGBOOK_ENTRY_MESSAGE,
LOGBOOK_ENTRY_NAME,
)
from homeassistant.const import ATTR_COMMAND, ATTR_DEVICE_ID
from homeassistant.core import Event, HomeAssistant, callback
import homeassistant.helpers.device_registry as dr
from .core.const import DOMAIN as ZHA_DOMAIN, ZHA_EVENT
from .core.helpers import async_get_zha_device
if TYPE_CHECKING:
from .core.device import ZHADevice
@callback
def async_describe_events(
hass: HomeAssistant,
async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None],
) -> None:
"""Describe logbook events."""
device_registry = dr.async_get(hass)
@callback
def async_describe_zha_event(event: Event) -> dict[str, str]:
"""Describe zha logbook event."""
device: dr.DeviceEntry | None = None
device_name: str = "Unknown device"
zha_device: ZHADevice | None = None
event_data: dict = event.data
event_type: str | None = None
event_subtype: str | None = None
try:
device = device_registry.devices[event.data[ATTR_DEVICE_ID]]
if device:
device_name = device.name_by_user or device.name or "Unknown device"
zha_device = async_get_zha_device(hass, event.data[ATTR_DEVICE_ID])
except (KeyError, AttributeError):
pass
if (
zha_device
and (command := event_data.get(ATTR_COMMAND))
and (command_to_etype_subtype := zha_device.device_automation_commands)
and (etype_subtypes := command_to_etype_subtype.get(command))
):
all_triggers = zha_device.device_automation_triggers
for etype_subtype in etype_subtypes:
trigger = all_triggers[etype_subtype]
if not all(
event_data.get(key) == value for key, value in trigger.items()
):
continue
event_type, event_subtype = etype_subtype
break
if event_type is None:
event_type = event_data[ATTR_COMMAND]
if event_subtype is not None and event_subtype != event_type:
event_type = f"{event_type} - {event_subtype}"
event_type = event_type.replace("_", " ").title()
message = f"{event_type} event was fired"
if event_data["params"]:
message = f"{message} with parameters: {event_data['params']}"
return {
LOGBOOK_ENTRY_NAME: device_name,
LOGBOOK_ENTRY_MESSAGE: message,
}
async_describe_event(ZHA_DOMAIN, ZHA_EVENT, async_describe_zha_event)

View File

@ -510,7 +510,7 @@ async def test_poll_control_cluster_command(hass, poll_control_device):
checkin_mock = AsyncMock()
poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"]
cluster = poll_control_ch.cluster
events = async_capture_events(hass, "zha_event")
events = async_capture_events(hass, zha_const.ZHA_EVENT)
with mock.patch.object(poll_control_ch, "check_in_response", checkin_mock):
tsn = 22

View File

@ -17,6 +17,7 @@ from homeassistant.components.cover import (
SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER,
)
from homeassistant.components.zha.core.const import ZHA_EVENT
from homeassistant.const import (
ATTR_COMMAND,
STATE_CLOSED,
@ -410,7 +411,7 @@ async def test_cover_remote(hass, zha_device_joined_restored, zigpy_cover_remote
cluster = zigpy_cover_remote.endpoints[1].out_clusters[
closures.WindowCovering.cluster_id
]
zha_events = async_capture_events(hass, "zha_event")
zha_events = async_capture_events(hass, ZHA_EVENT)
# up command
hdr = make_zcl_header(0, global_command=False)

View File

@ -0,0 +1,208 @@
"""ZHA logbook describe events tests."""
import pytest
import zigpy.profiles.zha
import zigpy.zcl.clusters.general as general
from homeassistant.components.zha.core.const import ZHA_EVENT
from homeassistant.const import CONF_DEVICE_ID, CONF_UNIQUE_ID
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
from tests.components.logbook.common import MockRow, mock_humanify
ON = 1
OFF = 0
SHAKEN = "device_shaken"
COMMAND = "command"
COMMAND_SHAKE = "shake"
COMMAND_HOLD = "hold"
COMMAND_SINGLE = "single"
COMMAND_DOUBLE = "double"
DOUBLE_PRESS = "remote_button_double_press"
SHORT_PRESS = "remote_button_short_press"
LONG_PRESS = "remote_button_long_press"
LONG_RELEASE = "remote_button_long_release"
UP = "up"
DOWN = "down"
@pytest.fixture
async def mock_devices(hass, zigpy_device_mock, zha_device_joined):
"""IAS device fixture."""
zigpy_device = zigpy_device_mock(
{
1: {
SIG_EP_INPUT: [general.Basic.cluster_id],
SIG_EP_OUTPUT: [general.OnOff.cluster_id],
SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
}
}
)
zha_device = await zha_device_joined(zigpy_device)
zha_device.update_available(True)
await hass.async_block_till_done()
return zigpy_device, zha_device
async def test_zha_logbook_event_device_with_triggers(hass, mock_devices):
"""Test zha logbook events with device and triggers."""
zigpy_device, zha_device = mock_devices
zigpy_device.device_automation_triggers = {
(SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE},
(UP, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE, "endpoint_id": 1},
(DOWN, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE, "endpoint_id": 2},
(SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE},
(LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD},
(LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD},
}
ieee_address = str(zha_device.ieee)
ha_device_registry = dr.async_get(hass)
reg_device = ha_device_registry.async_get_device({("zha", ieee_address)})
hass.config.components.add("recorder")
assert await async_setup_component(hass, "logbook", {})
events = mock_humanify(
hass,
[
MockRow(
ZHA_EVENT,
{
CONF_DEVICE_ID: reg_device.id,
COMMAND: COMMAND_SHAKE,
"device_ieee": str(ieee_address),
CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006",
"endpoint_id": 1,
"cluster_id": 6,
"params": {
"test": "test",
},
},
),
MockRow(
ZHA_EVENT,
{
CONF_DEVICE_ID: reg_device.id,
COMMAND: COMMAND_DOUBLE,
"device_ieee": str(ieee_address),
CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006",
"endpoint_id": 1,
"cluster_id": 6,
"params": {
"test": "test",
},
},
),
MockRow(
ZHA_EVENT,
{
CONF_DEVICE_ID: reg_device.id,
COMMAND: COMMAND_DOUBLE,
"device_ieee": str(ieee_address),
CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006",
"endpoint_id": 2,
"cluster_id": 6,
"params": {
"test": "test",
},
},
),
],
)
assert events[0]["name"] == "FakeManufacturer FakeModel"
assert events[0]["domain"] == "zha"
assert (
events[0]["message"]
== "Device Shaken event was fired with parameters: {'test': 'test'}"
)
assert events[1]["name"] == "FakeManufacturer FakeModel"
assert events[1]["domain"] == "zha"
assert (
events[1]["message"]
== "Up - Remote Button Double Press event was fired with parameters: {'test': 'test'}"
)
async def test_zha_logbook_event_device_no_triggers(hass, mock_devices):
"""Test zha logbook events with device and without triggers."""
zigpy_device, zha_device = mock_devices
ieee_address = str(zha_device.ieee)
ha_device_registry = dr.async_get(hass)
reg_device = ha_device_registry.async_get_device({("zha", ieee_address)})
hass.config.components.add("recorder")
assert await async_setup_component(hass, "logbook", {})
events = mock_humanify(
hass,
[
MockRow(
ZHA_EVENT,
{
CONF_DEVICE_ID: reg_device.id,
COMMAND: COMMAND_SHAKE,
"device_ieee": str(ieee_address),
CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006",
"endpoint_id": 1,
"cluster_id": 6,
"params": {
"test": "test",
},
},
),
],
)
assert events[0]["name"] == "FakeManufacturer FakeModel"
assert events[0]["domain"] == "zha"
assert (
events[0]["message"]
== "Shake event was fired with parameters: {'test': 'test'}"
)
async def test_zha_logbook_event_device_no_device(hass, mock_devices):
"""Test zha logbook events without device and without triggers."""
hass.config.components.add("recorder")
assert await async_setup_component(hass, "logbook", {})
events = mock_humanify(
hass,
[
MockRow(
ZHA_EVENT,
{
CONF_DEVICE_ID: "non-existing-device",
COMMAND: COMMAND_SHAKE,
"device_ieee": "90:fd:9f:ff:fe:fe:d8:a1",
CONF_UNIQUE_ID: "90:fd:9f:ff:fe:fe:d8:a1:1:0x0006",
"endpoint_id": 1,
"cluster_id": 6,
"params": {
"test": "test",
},
},
),
],
)
assert events[0]["name"] == "Unknown device"
assert events[0]["domain"] == "zha"
assert (
events[0]["message"]
== "Shake event was fired with parameters: {'test': 'test'}"
)