Add Event platform to Matter (#97219)

This commit is contained in:
Marcel van der Veldt 2023-07-26 12:19:23 +02:00 committed by GitHub
parent d7af1e2d5d
commit fd44bef39b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 483 additions and 3 deletions

View File

@ -12,6 +12,7 @@ from homeassistant.core import callback
from .binary_sensor import DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS
from .climate import DISCOVERY_SCHEMAS as CLIMATE_SENSOR_SCHEMAS
from .cover import DISCOVERY_SCHEMAS as COVER_SCHEMAS
from .event import DISCOVERY_SCHEMAS as EVENT_SCHEMAS
from .light import DISCOVERY_SCHEMAS as LIGHT_SCHEMAS
from .lock import DISCOVERY_SCHEMAS as LOCK_SCHEMAS
from .models import MatterDiscoverySchema, MatterEntityInfo
@ -22,6 +23,7 @@ DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = {
Platform.BINARY_SENSOR: BINARY_SENSOR_SCHEMAS,
Platform.CLIMATE: CLIMATE_SENSOR_SCHEMAS,
Platform.COVER: COVER_SCHEMAS,
Platform.EVENT: EVENT_SCHEMAS,
Platform.LIGHT: LIGHT_SCHEMAS,
Platform.LOCK: LOCK_SCHEMAS,
Platform.SENSOR: SENSOR_SCHEMAS,

View File

@ -0,0 +1,135 @@
"""Matter event entities from Node events."""
from __future__ import annotations
from typing import Any
from chip.clusters import Objects as clusters
from matter_server.client.models import device_types
from matter_server.common.models import EventType, MatterNodeEvent
from homeassistant.components.event import (
EventDeviceClass,
EventEntity,
EventEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .entity import MatterEntity
from .helpers import get_matter
from .models import MatterDiscoverySchema
SwitchFeature = clusters.Switch.Bitmaps.SwitchFeature
EVENT_TYPES_MAP = {
# mapping from raw event id's to translation keys
0: "switch_latched", # clusters.Switch.Events.SwitchLatched
1: "initial_press", # clusters.Switch.Events.InitialPress
2: "long_press", # clusters.Switch.Events.LongPress
3: "short_release", # clusters.Switch.Events.ShortRelease
4: "long_release", # clusters.Switch.Events.LongRelease
5: "multi_press_ongoing", # clusters.Switch.Events.MultiPressOngoing
6: "multi_press_complete", # clusters.Switch.Events.MultiPressComplete
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Matter switches from Config Entry."""
matter = get_matter(hass)
matter.register_platform_handler(Platform.EVENT, async_add_entities)
class MatterEventEntity(MatterEntity, EventEntity):
"""Representation of a Matter Event entity."""
_attr_translation_key = "push"
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize the entity."""
super().__init__(*args, **kwargs)
# fill the event types based on the features the switch supports
event_types: list[str] = []
feature_map = int(
self.get_matter_attribute_value(clusters.Switch.Attributes.FeatureMap)
)
if feature_map & SwitchFeature.kLatchingSwitch:
event_types.append("switch_latched")
if feature_map & SwitchFeature.kMomentarySwitch:
event_types.append("initial_press")
if feature_map & SwitchFeature.kMomentarySwitchRelease:
event_types.append("short_release")
if feature_map & SwitchFeature.kMomentarySwitchLongPress:
event_types.append("long_press_ongoing")
event_types.append("long_release")
if feature_map & SwitchFeature.kMomentarySwitchMultiPress:
event_types.append("multi_press_ongoing")
event_types.append("multi_press_complete")
self._attr_event_types = event_types
# the optional label attribute could be used to identify multiple buttons
# e.g. in case of a dimmer switch with 4 buttons, each button
# will have its own name, prefixed by the device name.
if labels := self.get_matter_attribute_value(
clusters.FixedLabel.Attributes.LabelList
):
for label in labels:
if label.label == "Label":
label_value: str = label.value
# in the case the label is only the label id, prettify it a bit
if label_value.isnumeric():
self._attr_name = f"Button {label_value}"
else:
self._attr_name = label_value
break
async def async_added_to_hass(self) -> None:
"""Handle being added to Home Assistant."""
await super().async_added_to_hass()
# subscribe to NodeEvent events
self._unsubscribes.append(
self.matter_client.subscribe_events(
callback=self._on_matter_node_event,
event_filter=EventType.NODE_EVENT,
node_filter=self._endpoint.node.node_id,
)
)
def _update_from_device(self) -> None:
"""Call when Node attribute(s) changed."""
@callback
def _on_matter_node_event(
self, event: EventType, data: MatterNodeEvent
) -> None: # noqa: F821
"""Call on NodeEvent."""
if data.endpoint_id != self._endpoint.endpoint_id:
return
self._trigger_event(EVENT_TYPES_MAP[data.event_id], data.data)
self.async_write_ha_state()
# Discovery schema(s) to map Matter Attributes to HA entities
DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.EVENT,
entity_description=EventEntityDescription(
key="GenericSwitch", device_class=EventDeviceClass.BUTTON, name=None
),
entity_class=MatterEventEntity,
required_attributes=(
clusters.Switch.Attributes.CurrentPosition,
clusters.Switch.Attributes.FeatureMap,
),
device_type=(device_types.GenericSwitch,),
optional_attributes=(
clusters.Switch.Attributes.NumberOfPositions,
clusters.FixedLabel.Attributes.LabelList,
),
),
]

View File

@ -6,5 +6,5 @@
"dependencies": ["websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/matter",
"iot_class": "local_push",
"requirements": ["python-matter-server==3.6.3"]
"requirements": ["python-matter-server==3.7.0"]
}

View File

@ -45,6 +45,23 @@
}
},
"entity": {
"event": {
"push": {
"state_attributes": {
"event_type": {
"state": {
"switch_latched": "Switch latched",
"initial_press": "Initial press",
"long_press": "Lomng press",
"short_release": "Short release",
"long_release": "Long release",
"multi_press_ongoing": "Multi press ongoing",
"multi_press_complete": "Multi press complete"
}
}
}
}
},
"sensor": {
"flow": {
"name": "Flow"

View File

@ -2119,7 +2119,7 @@ python-kasa[speedups]==0.5.3
# python-lirc==1.2.3
# homeassistant.components.matter
python-matter-server==3.6.3
python-matter-server==3.7.0
# homeassistant.components.xiaomi_miio
python-miio==0.5.12

View File

@ -1557,7 +1557,7 @@ python-juicenet==1.1.0
python-kasa[speedups]==0.5.3
# homeassistant.components.matter
python-matter-server==3.6.3
python-matter-server==3.7.0
# homeassistant.components.xiaomi_miio
python-miio==0.5.12

View File

@ -0,0 +1,117 @@
{
"node_id": 1,
"date_commissioned": "2023-07-06T11:13:20.917394",
"last_interview": "2023-07-06T11:13:20.917401",
"interview_version": 2,
"attributes": {
"0/29/0": [
{
"deviceType": 22,
"revision": 1
}
],
"0/29/1": [
4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 62, 63,
64, 65
],
"0/29/2": [41],
"0/29/3": [1],
"0/29/65532": 0,
"0/29/65533": 1,
"0/29/65528": [],
"0/29/65529": [],
"0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/40/0": 1,
"0/40/1": "Nabu Casa",
"0/40/2": 65521,
"0/40/3": "Mock GenericSwitch",
"0/40/4": 32768,
"0/40/5": "Mock Generic Switch",
"0/40/6": "XX",
"0/40/7": 0,
"0/40/8": "v1.0",
"0/40/9": 1,
"0/40/10": "prerelease",
"0/40/11": "20230707",
"0/40/12": "",
"0/40/13": "",
"0/40/14": "",
"0/40/15": "TEST_SN",
"0/40/16": false,
"0/40/17": true,
"0/40/18": "mock-generic-switch",
"0/40/19": {
"caseSessionsPerFabric": 3,
"subscriptionsPerFabric": 3
},
"0/40/65532": 0,
"0/40/65533": 1,
"0/40/65528": [],
"0/40/65529": [],
"0/40/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
65528, 65529, 65531, 65532, 65533
],
"1/3/65529": [0, 64],
"1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/29/0": [
{
"deviceType": 15,
"revision": 1
}
],
"1/29/1": [3, 29, 59],
"1/29/2": [],
"1/29/3": [],
"1/29/65532": 0,
"1/29/65533": 1,
"1/29/65528": [],
"1/29/65529": [],
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"1/59/65529": [],
"1/59/0": 2,
"1/59/65533": 1,
"1/59/1": 0,
"1/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/59/65532": 14,
"1/59/65528": [],
"1/64/0": [
{
"label": "Label",
"value": "1"
}
],
"2/3/65529": [0, 64],
"2/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"2/29/0": [
{
"deviceType": 15,
"revision": 1
}
],
"2/29/1": [3, 29, 59],
"2/29/2": [],
"2/29/3": [],
"2/29/65532": 0,
"2/29/65533": 1,
"2/29/65528": [],
"2/29/65529": [],
"2/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"2/59/65529": [],
"2/59/0": 2,
"2/59/65533": 1,
"2/59/1": 0,
"2/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"2/59/65532": 14,
"2/59/65528": [],
"2/64/0": [
{
"label": "Label",
"value": "Fancy Button"
}
]
},
"available": true,
"attribute_subscriptions": []
}

View File

@ -0,0 +1,81 @@
{
"node_id": 1,
"date_commissioned": "2023-07-06T11:13:20.917394",
"last_interview": "2023-07-06T11:13:20.917401",
"interview_version": 2,
"attributes": {
"0/29/0": [
{
"deviceType": 22,
"revision": 1
}
],
"0/29/1": [
4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 62, 63,
64, 65
],
"0/29/2": [41],
"0/29/3": [1],
"0/29/65532": 0,
"0/29/65533": 1,
"0/29/65528": [],
"0/29/65529": [],
"0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/40/0": 1,
"0/40/1": "Nabu Casa",
"0/40/2": 65521,
"0/40/3": "Mock GenericSwitch",
"0/40/4": 32768,
"0/40/5": "Mock Generic Switch",
"0/40/6": "XX",
"0/40/7": 0,
"0/40/8": "v1.0",
"0/40/9": 1,
"0/40/10": "prerelease",
"0/40/11": "20230707",
"0/40/12": "",
"0/40/13": "",
"0/40/14": "",
"0/40/15": "TEST_SN",
"0/40/16": false,
"0/40/17": true,
"0/40/18": "mock-generic-switch",
"0/40/19": {
"caseSessionsPerFabric": 3,
"subscriptionsPerFabric": 3
},
"0/40/65532": 0,
"0/40/65533": 1,
"0/40/65528": [],
"0/40/65529": [],
"0/40/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
65528, 65529, 65531, 65532, 65533
],
"1/3/65529": [0, 64],
"1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/29/0": [
{
"deviceType": 15,
"revision": 1
}
],
"1/29/1": [3, 29, 59],
"1/29/2": [],
"1/29/3": [],
"1/29/65532": 0,
"1/29/65533": 1,
"1/29/65528": [],
"1/29/65529": [],
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"1/59/65529": [],
"1/59/0": 2,
"1/59/65533": 1,
"1/59/1": 0,
"1/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/59/65532": 30,
"1/59/65528": []
},
"available": true,
"attribute_subscriptions": []
}

View File

@ -0,0 +1,128 @@
"""Test Matter Event entities."""
from unittest.mock import MagicMock
from matter_server.client.models.node import MatterNode
from matter_server.common.models import EventType, MatterNodeEvent
import pytest
from homeassistant.components.event import (
ATTR_EVENT_TYPE,
ATTR_EVENT_TYPES,
)
from homeassistant.core import HomeAssistant
from .common import (
setup_integration_with_node_fixture,
trigger_subscription_callback,
)
@pytest.fixture(name="generic_switch_node")
async def switch_node_fixture(
hass: HomeAssistant, matter_client: MagicMock
) -> MatterNode:
"""Fixture for a GenericSwitch node."""
return await setup_integration_with_node_fixture(
hass, "generic-switch", matter_client
)
@pytest.fixture(name="generic_switch_multi_node")
async def multi_switch_node_fixture(
hass: HomeAssistant, matter_client: MagicMock
) -> MatterNode:
"""Fixture for a GenericSwitch node with multiple buttons."""
return await setup_integration_with_node_fixture(
hass, "generic-switch-multi", matter_client
)
# This tests needs to be adjusted to remove lingering tasks
@pytest.mark.parametrize("expected_lingering_tasks", [True])
async def test_generic_switch_node(
hass: HomeAssistant,
matter_client: MagicMock,
generic_switch_node: MatterNode,
) -> None:
"""Test event entity for a GenericSwitch node."""
state = hass.states.get("event.mock_generic_switch")
assert state
assert state.state == "unknown"
# the switch endpoint has no label so the entity name should be the device itself
assert state.name == "Mock Generic Switch"
# check event_types from featuremap 30
assert state.attributes[ATTR_EVENT_TYPES] == [
"initial_press",
"short_release",
"long_press_ongoing",
"long_release",
"multi_press_ongoing",
"multi_press_complete",
]
# trigger firing a new event from the device
await trigger_subscription_callback(
hass,
matter_client,
EventType.NODE_EVENT,
MatterNodeEvent(
node_id=generic_switch_node.node_id,
endpoint_id=1,
cluster_id=59,
event_id=1,
event_number=0,
priority=1,
timestamp=0,
timestamp_type=0,
data=None,
),
)
state = hass.states.get("event.mock_generic_switch")
assert state.attributes[ATTR_EVENT_TYPE] == "initial_press"
# trigger firing a multi press event
await trigger_subscription_callback(
hass,
matter_client,
EventType.NODE_EVENT,
MatterNodeEvent(
node_id=generic_switch_node.node_id,
endpoint_id=1,
cluster_id=59,
event_id=5,
event_number=0,
priority=1,
timestamp=0,
timestamp_type=0,
data={"NewPosition": 3},
),
)
state = hass.states.get("event.mock_generic_switch")
assert state.attributes[ATTR_EVENT_TYPE] == "multi_press_ongoing"
assert state.attributes["NewPosition"] == 3
# This tests needs to be adjusted to remove lingering tasks
@pytest.mark.parametrize("expected_lingering_tasks", [True])
async def test_generic_switch_multi_node(
hass: HomeAssistant,
matter_client: MagicMock,
generic_switch_multi_node: MatterNode,
) -> None:
"""Test event entity for a GenericSwitch node with multiple buttons."""
state_button_1 = hass.states.get("event.mock_generic_switch_button_1")
assert state_button_1
assert state_button_1.state == "unknown"
# name should be 'DeviceName Button 1' due to the label set to just '1'
assert state_button_1.name == "Mock Generic Switch Button 1"
# check event_types from featuremap 14
assert state_button_1.attributes[ATTR_EVENT_TYPES] == [
"initial_press",
"short_release",
"long_press_ongoing",
"long_release",
]
# check button 2
state_button_1 = hass.states.get("event.mock_generic_switch_fancy_button")
assert state_button_1
assert state_button_1.state == "unknown"
# name should be 'DeviceName Fancy Button' due to the label set to 'Fancy Button'
assert state_button_1.name == "Mock Generic Switch Fancy Button"