Add Matter custom cluster sensors (Eve Energy Plug energy measurements) (#104830)

* Support for sensors from custom clusters in Matter

* lint

* no need to write state twice

* Add test for eve energy plug

* Update homeassistant/components/matter/entity.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* adjust comment

* debounce extra poll timer

* use async_call_later helper

* Update homeassistant/components/matter/entity.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* wip extend test

* Update test_sensor.py

* fix state class for sensors

* trigger (fake) event callback on all subscribers

* Update eve-energy-plug.json

* add test for additionally polled value

* adjust delay to 3 seconds

* Adjust subscribe_events to always use kwargs

* Update tests/components/matter/common.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update test_sensor.py

* remove redundant code

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Marcel van der Veldt 2023-12-04 17:21:41 +01:00 committed by GitHub
parent 7d21ed41a2
commit 516966db33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 867 additions and 15 deletions

View File

@ -97,22 +97,23 @@ class MatterAdapter:
self.config_entry.async_on_unload( self.config_entry.async_on_unload(
self.matter_client.subscribe_events( self.matter_client.subscribe_events(
endpoint_added_callback, EventType.ENDPOINT_ADDED callback=endpoint_added_callback, event_filter=EventType.ENDPOINT_ADDED
) )
) )
self.config_entry.async_on_unload( self.config_entry.async_on_unload(
self.matter_client.subscribe_events( self.matter_client.subscribe_events(
endpoint_removed_callback, EventType.ENDPOINT_REMOVED callback=endpoint_removed_callback,
event_filter=EventType.ENDPOINT_REMOVED,
) )
) )
self.config_entry.async_on_unload( self.config_entry.async_on_unload(
self.matter_client.subscribe_events( self.matter_client.subscribe_events(
node_removed_callback, EventType.NODE_REMOVED callback=node_removed_callback, event_filter=EventType.NODE_REMOVED
) )
) )
self.config_entry.async_on_unload( self.config_entry.async_on_unload(
self.matter_client.subscribe_events( self.matter_client.subscribe_events(
node_added_callback, EventType.NODE_ADDED callback=node_added_callback, event_filter=EventType.NODE_ADDED
) )
) )

View File

@ -115,8 +115,9 @@ def async_discover_entities(
attributes_to_watch=attributes_to_watch, attributes_to_watch=attributes_to_watch,
entity_description=schema.entity_description, entity_description=schema.entity_description,
entity_class=schema.entity_class, entity_class=schema.entity_class,
should_poll=schema.should_poll,
) )
# prevent re-discovery of the same attributes # prevent re-discovery of the primary attribute if not allowed
if not schema.allow_multi: if not schema.allow_multi:
discovered_attributes.update(attributes_to_watch) discovered_attributes.update(schema.required_attributes)

View File

@ -5,6 +5,7 @@ from abc import abstractmethod
from collections.abc import Callable from collections.abc import Callable
from contextlib import suppress from contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
import logging import logging
from typing import TYPE_CHECKING, Any, cast from typing import TYPE_CHECKING, Any, cast
@ -12,9 +13,10 @@ from chip.clusters.Objects import ClusterAttributeDescriptor, NullValue
from matter_server.common.helpers.util import create_attribute_path from matter_server.common.helpers.util import create_attribute_path
from matter_server.common.models import EventType, ServerInfoMessage from matter_server.common.models import EventType, ServerInfoMessage
from homeassistant.core import callback from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.event import async_call_later
from .const import DOMAIN, ID_TYPE_DEVICE_ID from .const import DOMAIN, ID_TYPE_DEVICE_ID
from .helpers import get_device_id from .helpers import get_device_id
@ -27,6 +29,13 @@ if TYPE_CHECKING:
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
# For some manually polled values (e.g. custom clusters) we perform
# an additional poll as soon as a secondary value changes.
# For example update the energy consumption meter when a relay is toggled
# of an energy metering powerplug. The below constant defined the delay after
# which we poll the primary value (debounced).
EXTRA_POLL_DELAY = 3.0
@dataclass @dataclass
class MatterEntityDescription(EntityDescription): class MatterEntityDescription(EntityDescription):
@ -39,7 +48,6 @@ class MatterEntityDescription(EntityDescription):
class MatterEntity(Entity): class MatterEntity(Entity):
"""Entity class for Matter devices.""" """Entity class for Matter devices."""
_attr_should_poll = False
_attr_has_entity_name = True _attr_has_entity_name = True
def __init__( def __init__(
@ -71,6 +79,8 @@ class MatterEntity(Entity):
identifiers={(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")} identifiers={(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")}
) )
self._attr_available = self._endpoint.node.available self._attr_available = self._endpoint.node.available
self._attr_should_poll = entity_info.should_poll
self._extra_poll_timer_unsub: CALLBACK_TYPE | None = None
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Handle being added to Home Assistant.""" """Handle being added to Home Assistant."""
@ -110,15 +120,35 @@ class MatterEntity(Entity):
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass.""" """Run when entity will be removed from hass."""
if self._extra_poll_timer_unsub:
self._extra_poll_timer_unsub()
for unsub in self._unsubscribes: for unsub in self._unsubscribes:
with suppress(ValueError): with suppress(ValueError):
# suppress ValueError to prevent race conditions # suppress ValueError to prevent race conditions
unsub() unsub()
async def async_update(self) -> None:
"""Call when the entity needs to be updated."""
# manually poll/refresh the primary value
await self.matter_client.refresh_attribute(
self._endpoint.node.node_id,
self.get_matter_attribute_path(self._entity_info.primary_attribute),
)
self._update_from_device()
@callback @callback
def _on_matter_event(self, event: EventType, data: Any = None) -> None: def _on_matter_event(self, event: EventType, data: Any = None) -> None:
"""Call on update.""" """Call on update from the device."""
self._attr_available = self._endpoint.node.available self._attr_available = self._endpoint.node.available
if self._attr_should_poll:
# secondary attribute updated of a polled primary value
# enforce poll of the primary value a few seconds later
if self._extra_poll_timer_unsub:
self._extra_poll_timer_unsub()
self._extra_poll_timer_unsub = async_call_later(
self.hass, EXTRA_POLL_DELAY, self._do_extra_poll
)
return
self._update_from_device() self._update_from_device()
self.async_write_ha_state() self.async_write_ha_state()
@ -145,3 +175,9 @@ class MatterEntity(Entity):
return create_attribute_path( return create_attribute_path(
self._endpoint.endpoint_id, attribute.cluster_id, attribute.attribute_id self._endpoint.endpoint_id, attribute.cluster_id, attribute.attribute_id
) )
@callback
def _do_extra_poll(self, called_at: datetime) -> None:
"""Perform (extra) poll of primary value."""
# scheduling the regulat update is enough to perform a poll/refresh
self.async_schedule_update_ha_state(True)

View File

@ -50,6 +50,9 @@ class MatterEntityInfo:
# entity class to use to instantiate the entity # entity class to use to instantiate the entity
entity_class: type entity_class: type
# [optional] bool to specify if this primary value should be polled
should_poll: bool
@property @property
def primary_attribute(self) -> type[ClusterAttributeDescriptor]: def primary_attribute(self) -> type[ClusterAttributeDescriptor]:
"""Return Primary Attribute belonging to the entity.""" """Return Primary Attribute belonging to the entity."""
@ -106,3 +109,6 @@ class MatterDiscoverySchema:
# [optional] bool to specify if this primary value may be discovered # [optional] bool to specify if this primary value may be discovered
# by multiple platforms # by multiple platforms
allow_multi: bool = False allow_multi: bool = False
# [optional] bool to specify if this primary value should be polled
should_poll: bool = False

View File

@ -5,6 +5,7 @@ from dataclasses import dataclass
from chip.clusters import Objects as clusters from chip.clusters import Objects as clusters
from chip.clusters.Types import Nullable, NullValue from chip.clusters.Types import Nullable, NullValue
from matter_server.client.models.clusters import EveEnergyCluster
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -18,6 +19,10 @@ from homeassistant.const import (
PERCENTAGE, PERCENTAGE,
EntityCategory, EntityCategory,
Platform, Platform,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfPower,
UnitOfPressure, UnitOfPressure,
UnitOfTemperature, UnitOfTemperature,
UnitOfVolumeFlowRate, UnitOfVolumeFlowRate,
@ -48,7 +53,6 @@ class MatterSensorEntityDescription(SensorEntityDescription, MatterEntityDescrip
class MatterSensor(MatterEntity, SensorEntity): class MatterSensor(MatterEntity, SensorEntity):
"""Representation of a Matter sensor.""" """Representation of a Matter sensor."""
_attr_state_class = SensorStateClass.MEASUREMENT
entity_description: MatterSensorEntityDescription entity_description: MatterSensorEntityDescription
@callback @callback
@ -72,6 +76,7 @@ DISCOVERY_SCHEMAS = [
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
measurement_to_ha=lambda x: x / 100, measurement_to_ha=lambda x: x / 100,
state_class=SensorStateClass.MEASUREMENT,
), ),
entity_class=MatterSensor, entity_class=MatterSensor,
required_attributes=(clusters.TemperatureMeasurement.Attributes.MeasuredValue,), required_attributes=(clusters.TemperatureMeasurement.Attributes.MeasuredValue,),
@ -83,6 +88,7 @@ DISCOVERY_SCHEMAS = [
native_unit_of_measurement=UnitOfPressure.KPA, native_unit_of_measurement=UnitOfPressure.KPA,
device_class=SensorDeviceClass.PRESSURE, device_class=SensorDeviceClass.PRESSURE,
measurement_to_ha=lambda x: x / 10, measurement_to_ha=lambda x: x / 10,
state_class=SensorStateClass.MEASUREMENT,
), ),
entity_class=MatterSensor, entity_class=MatterSensor,
required_attributes=(clusters.PressureMeasurement.Attributes.MeasuredValue,), required_attributes=(clusters.PressureMeasurement.Attributes.MeasuredValue,),
@ -94,6 +100,7 @@ DISCOVERY_SCHEMAS = [
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
translation_key="flow", translation_key="flow",
measurement_to_ha=lambda x: x / 10, measurement_to_ha=lambda x: x / 10,
state_class=SensorStateClass.MEASUREMENT,
), ),
entity_class=MatterSensor, entity_class=MatterSensor,
required_attributes=(clusters.FlowMeasurement.Attributes.MeasuredValue,), required_attributes=(clusters.FlowMeasurement.Attributes.MeasuredValue,),
@ -105,6 +112,7 @@ DISCOVERY_SCHEMAS = [
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
measurement_to_ha=lambda x: x / 100, measurement_to_ha=lambda x: x / 100,
state_class=SensorStateClass.MEASUREMENT,
), ),
entity_class=MatterSensor, entity_class=MatterSensor,
required_attributes=( required_attributes=(
@ -118,6 +126,7 @@ DISCOVERY_SCHEMAS = [
native_unit_of_measurement=LIGHT_LUX, native_unit_of_measurement=LIGHT_LUX,
device_class=SensorDeviceClass.ILLUMINANCE, device_class=SensorDeviceClass.ILLUMINANCE,
measurement_to_ha=lambda x: round(pow(10, ((x - 1) / 10000)), 1), measurement_to_ha=lambda x: round(pow(10, ((x - 1) / 10000)), 1),
state_class=SensorStateClass.MEASUREMENT,
), ),
entity_class=MatterSensor, entity_class=MatterSensor,
required_attributes=(clusters.IlluminanceMeasurement.Attributes.MeasuredValue,), required_attributes=(clusters.IlluminanceMeasurement.Attributes.MeasuredValue,),
@ -131,8 +140,71 @@ DISCOVERY_SCHEMAS = [
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
# value has double precision # value has double precision
measurement_to_ha=lambda x: int(x / 2), measurement_to_ha=lambda x: int(x / 2),
state_class=SensorStateClass.MEASUREMENT,
), ),
entity_class=MatterSensor, entity_class=MatterSensor,
required_attributes=(clusters.PowerSource.Attributes.BatPercentRemaining,), required_attributes=(clusters.PowerSource.Attributes.BatPercentRemaining,),
), ),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EveEnergySensorWatt",
device_class=SensorDeviceClass.POWER,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfPower.WATT,
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(EveEnergyCluster.Attributes.Watt,),
# Add OnOff Attribute as optional attribute to poll
# the primary value when the relay is toggled
optional_attributes=(clusters.OnOff.Attributes.OnOff,),
should_poll=True,
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EveEnergySensorVoltage",
device_class=SensorDeviceClass.VOLTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(EveEnergyCluster.Attributes.Voltage,),
should_poll=True,
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EveEnergySensorWattAccumulated",
device_class=SensorDeviceClass.ENERGY,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=3,
state_class=SensorStateClass.TOTAL_INCREASING,
),
entity_class=MatterSensor,
required_attributes=(EveEnergyCluster.Attributes.WattAccumulated,),
should_poll=True,
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EveEnergySensorWattCurrent",
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(EveEnergyCluster.Attributes.Current,),
# Add OnOff Attribute as optional attribute to poll
# the primary value when the relay is toggled
optional_attributes=(clusters.OnOff.Attributes.OnOff,),
should_poll=True,
),
] ]

View File

@ -71,6 +71,10 @@ async def trigger_subscription_callback(
data: Any = None, data: Any = None,
) -> None: ) -> None:
"""Trigger a subscription callback.""" """Trigger a subscription callback."""
callback = client.subscribe_events.call_args.kwargs["callback"] # trigger callback on all subscribers
callback(event, data) for sub in client.subscribe_events.call_args_list:
callback = sub.kwargs["callback"]
event_filter = sub.kwargs.get("event_filter")
if event_filter in (None, event):
callback(event, data)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -0,0 +1,649 @@
{
"node_id": 83,
"date_commissioned": "2023-11-30T14:39:37.020026",
"last_interview": "2023-11-30T14:39:37.020029",
"interview_version": 5,
"available": true,
"is_bridge": false,
"attributes": {
"0/29/0": [
{
"0": 22,
"1": 1
}
],
"0/29/1": [29, 31, 40, 42, 48, 49, 51, 53, 60, 62, 63],
"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/31/0": [
{
"254": 1
},
{
"254": 2
},
{
"1": 5,
"2": 2,
"3": [112233],
"4": null,
"254": 5
}
],
"0/31/1": [],
"0/31/2": 4,
"0/31/3": 3,
"0/31/4": 3,
"0/31/65532": 0,
"0/31/65533": 1,
"0/31/65528": [],
"0/31/65529": [],
"0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
"0/40/0": 1,
"0/40/1": "Eve Systems",
"0/40/2": 4874,
"0/40/3": "Eve Energy Plug",
"0/40/4": 80,
"0/40/5": "",
"0/40/6": "XX",
"0/40/7": 1,
"0/40/8": "1.1",
"0/40/9": 6650,
"0/40/10": "3.2.1",
"0/40/15": "RV44L1A00081",
"0/40/18": "26E8F90561D17C42",
"0/40/19": {
"0": 3,
"1": 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, 15, 18, 19, 65528, 65529, 65531, 65532,
65533
],
"0/42/0": [
{
"1": 2312386028615903905,
"2": 0,
"254": 1
}
],
"0/42/1": true,
"0/42/2": 1,
"0/42/3": null,
"0/42/65532": 0,
"0/42/65533": 1,
"0/42/65528": [],
"0/42/65529": [0],
"0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/48/0": 0,
"0/48/1": {
"0": 60,
"1": 900
},
"0/48/2": 0,
"0/48/3": 0,
"0/48/4": true,
"0/48/65532": 0,
"0/48/65533": 1,
"0/48/65528": [1, 3, 5],
"0/48/65529": [0, 2, 4],
"0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
"0/49/0": 1,
"0/49/1": [
{
"0": "cfUKbvsdfsBjT+0=",
"1": true
}
],
"0/49/2": 10,
"0/49/3": 20,
"0/49/4": true,
"0/49/5": 0,
"0/49/6": "cfUKbvBjdsffwT+0=",
"0/49/7": null,
"0/49/65532": 2,
"0/49/65533": 1,
"0/49/65528": [1, 5, 7],
"0/49/65529": [0, 3, 4, 6, 8],
"0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533],
"0/51/0": [
{
"0": "ieee802154",
"1": true,
"2": null,
"3": null,
"4": "ymtKI/b4u+4=",
"5": [],
"6": [
"/oAAAAA13414AAADIa0oj9vi77g==",
"/XH1Cm71434wAAB8TZpoASmxuw==",
"/RtUBAb134134mAAAPypryIKqshA=="
],
"7": 4
}
],
"0/51/1": 95,
"0/51/2": 268574,
"0/51/3": 4406,
"0/51/5": [],
"0/51/6": [],
"0/51/7": [],
"0/51/8": false,
"0/51/65532": 0,
"0/51/65533": 1,
"0/51/65528": [],
"0/51/65529": [0],
"0/51/65531": [0, 1, 2, 3, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533],
"0/53/0": 25,
"0/53/1": 5,
"0/53/2": "MyHome23",
"0/53/3": 14707,
"0/53/4": 8211480967175688173,
"0/53/5": "QP1x9Qfwefu8AAA",
"0/53/6": 0,
"0/53/7": [
{
"0": 13418684826835773064,
"1": 9,
"2": 3072,
"3": 56455,
"4": 84272,
"5": 1,
"6": -89,
"7": -88,
"8": 16,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 3054316089463545304,
"1": 2,
"2": 12288,
"3": 17170,
"4": 58113,
"5": 3,
"6": -45,
"7": -46,
"8": 0,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 3650476115380598997,
"1": 13,
"2": 15360,
"3": 172475,
"4": 65759,
"5": 3,
"6": -17,
"7": -18,
"8": 12,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 11968039652259981925,
"1": 21,
"2": 21504,
"3": 127929,
"4": 55363,
"5": 3,
"6": -74,
"7": -72,
"8": 3,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 17156405262946673420,
"1": 22,
"2": 22528,
"3": 22063,
"4": 137698,
"5": 1,
"6": -92,
"7": -92,
"8": 34,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 17782243871947087975,
"1": 18,
"2": 23552,
"3": 157044,
"4": 122272,
"5": 2,
"6": -81,
"7": -82,
"8": 3,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 8276316979900166010,
"1": 17,
"2": 31744,
"3": 486113,
"4": 298427,
"5": 2,
"6": -83,
"7": -82,
"8": 0,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
},
{
"0": 9121696247933828996,
"1": 48,
"2": 53248,
"3": 651530,
"4": 161559,
"5": 3,
"6": -70,
"7": -71,
"8": 15,
"9": 0,
"10": true,
"11": true,
"12": true,
"13": false
}
],
"0/53/8": [
{
"0": 13418684826835773064,
"1": 3072,
"2": 3,
"3": 15,
"4": 1,
"5": 1,
"6": 1,
"7": 9,
"8": true,
"9": true
},
{
"0": 0,
"1": 7168,
"2": 7,
"3": 21,
"4": 1,
"5": 0,
"6": 0,
"7": 76,
"8": true,
"9": false
},
{
"0": 0,
"1": 10240,
"2": 10,
"3": 21,
"4": 1,
"5": 0,
"6": 0,
"7": 243,
"8": true,
"9": false
},
{
"0": 3054316089463545304,
"1": 12288,
"2": 12,
"3": 15,
"4": 1,
"5": 3,
"6": 3,
"7": 2,
"8": true,
"9": true
},
{
"0": 3650476115380598997,
"1": 15360,
"2": 15,
"3": 12,
"4": 1,
"5": 3,
"6": 3,
"7": 14,
"8": true,
"9": true
},
{
"0": 11968039652259981925,
"1": 21504,
"2": 21,
"3": 15,
"4": 1,
"5": 3,
"6": 2,
"7": 22,
"8": true,
"9": true
},
{
"0": 17156405262946673420,
"1": 22528,
"2": 22,
"3": 52,
"4": 1,
"5": 1,
"6": 0,
"7": 23,
"8": true,
"9": true
},
{
"0": 17782243871947087975,
"1": 23552,
"2": 23,
"3": 15,
"4": 1,
"5": 2,
"6": 2,
"7": 19,
"8": true,
"9": true
},
{
"0": 0,
"1": 29696,
"2": 29,
"3": 21,
"4": 1,
"5": 0,
"6": 0,
"7": 31,
"8": true,
"9": false
},
{
"0": 8276316979900166010,
"1": 31744,
"2": 31,
"3": 52,
"4": 1,
"5": 2,
"6": 2,
"7": 18,
"8": true,
"9": true
},
{
"0": 0,
"1": 39936,
"2": 39,
"3": 52,
"4": 1,
"5": 0,
"6": 0,
"7": 31,
"8": true,
"9": false
},
{
"0": 9121696247933828996,
"1": 53248,
"2": 52,
"3": 15,
"4": 1,
"5": 3,
"6": 3,
"7": 48,
"8": true,
"9": true
},
{
"0": 14585833336497290222,
"1": 54272,
"2": 53,
"3": 63,
"4": 0,
"5": 0,
"6": 0,
"7": 0,
"8": true,
"9": false
}
],
"0/53/9": 1828774034,
"0/53/10": 68,
"0/53/11": 237,
"0/53/12": 170,
"0/53/13": 23,
"0/53/14": 2,
"0/53/15": 1,
"0/53/16": 2,
"0/53/17": 0,
"0/53/18": 0,
"0/53/19": 2,
"0/53/20": 0,
"0/53/21": 0,
"0/53/22": 293884,
"0/53/23": 278934,
"0/53/24": 14950,
"0/53/25": 278894,
"0/53/26": 278468,
"0/53/27": 14990,
"0/53/28": 293844,
"0/53/29": 0,
"0/53/30": 40,
"0/53/31": 0,
"0/53/32": 0,
"0/53/33": 65244,
"0/53/34": 426,
"0/53/35": 0,
"0/53/36": 87,
"0/53/37": 0,
"0/53/38": 0,
"0/53/39": 6687540,
"0/53/40": 142626,
"0/53/41": 106835,
"0/53/42": 246171,
"0/53/43": 0,
"0/53/44": 541,
"0/53/45": 40,
"0/53/46": 0,
"0/53/47": 0,
"0/53/48": 6360718,
"0/53/49": 2141,
"0/53/50": 35259,
"0/53/51": 4374,
"0/53/52": 0,
"0/53/53": 568,
"0/53/54": 18599,
"0/53/55": 19143,
"0/53/59": {
"0": 672,
"1": 8335
},
"0/53/60": "AB//wA==",
"0/53/61": {
"0": true,
"1": false,
"2": true,
"3": true,
"4": true,
"5": true,
"6": false,
"7": true,
"8": true,
"9": true,
"10": true,
"11": true
},
"0/53/62": [0, 0, 0, 0],
"0/53/65532": 15,
"0/53/65533": 1,
"0/53/65528": [],
"0/53/65529": [0],
"0/53/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 59,
60, 61, 62, 65528, 65529, 65531, 65532, 65533
],
"0/60/0": 0,
"0/60/1": null,
"0/60/2": null,
"0/60/65532": 0,
"0/60/65533": 1,
"0/60/65528": [],
"0/60/65529": [0, 1, 2],
"0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
"0/62/0": [
{
"254": 1
},
{
"254": 2
},
{
"1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRUxgkBwEkCAEwCUEEg58CF25hrI1R598dXwRapPCYUjahad5XkJMrA0tZb8HXO67XlyD4L+1ljtb6IAHhxjOGew2jNVSQDH1aqRGsODcKNQEoARgkAgE2AwQCBAEYMAQUkpBmmh0G57MnnxYDgxZuAZBezjYwBRTphWiJ/NqGe3Cx3Nj8H02NgGioSRgwC0CCOOCnKlhpegJmaH8vSIO38MQcJq+qV85UPPqaYc8dakaAnASvYeurP41Jw4KrCqyLMNRhUwqeyKoql6iQFKNAGA==",
"2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEYztrLK2UY1ORHUEFLO7PDfVjw/MnMDNX5kjdHHDU7npeITnSyg/kxxUM+pD7ccxfDuHQKHbBq9+qbJi8oGik8DcKNQEpARgkAmAwBBTphWiJ/NqGe3Cx3Nj8H02NgGioSTAFFMnf5ZkBCRaBluhSmLJkvcVXxHxTGDALQOOcZAL8XEktvE5sjrUmFNhkP2g3Ef+4BHtogItdZYyA9E/WbzW25E0UxZInwjjIzH3YimDUZVoEWGML8NV2kCEY",
"254": 5
}
],
"0/62/1": [
{
"1": "BIbR4Iu8CNIdxKRkSjTb1LKY3nzCbFVwDrjkRe4WDorCiMZHJmypZW24wBgAHxNo8D00QWw29llu8FH1eOtmHIo=",
"2": 4937,
"3": 1,
"4": 3878431683,
"5": "Thuis",
"254": 1
},
{
"1": "BLlk4ui4wSQ+xz89jB5nBRQUVYdY9H2dBUawGXVUxa2bsKh2k8CHijv1tkz1dThPXA9UK8jOAZ+7Mi+y7BPuAcg=",
"2": 4996,
"3": 2,
"4": 3763070728,
"5": "",
"254": 2
},
{
"1": "BAg5aeR7RuFKZhukCxMGglCd00dKlhxGq8BbjeyZClKz5kN2Ytzav0xWsiWEEb3s9uvMIYFoQYULnSJvOMTcD14=",
"2": 65521,
"3": 1,
"4": 83,
"5": "",
"254": 5
}
],
"0/62/2": 5,
"0/62/3": 3,
"0/62/4": [
"FTABAQAkAgE3AycUxofpv3kE1HwkFQEYJgS2Ty8rJgU2gxAtNwYnFMaH6b95BNR8JBUBGCQHASQIATAJQQSG0eCLvAjSHcSkZEo029SymN58wmxVcA645EXuFg6KwojGRyZsqWVtuMAYAB8TaPA9NEFsNvZZbvBR9XjrZhyKNwo1ASkBGCQCYDAEFNnFRJ+9qQIJtsM+LRdMdmCY3bQ4MAUU2cVEn72pAgm2wz4tF0x2YJjdtDgYMAtAFDv6Ouh7ugAGLiCjBQaEXCIAe0AkaaN8dBPskCZXOODjuZ1DCr4/f5IYg0rN2zFDUDTvG3GCxoI1+A7BvSjiNRg=",
"FTABAQAkAgE3AycUjuqR8vTQCmEkFQIYJgTFTy8rJgVFgxAtNwYnFI7qkfL00AphJBUCGCQHASQIATAJQQS5ZOLouMEkPsc/PYweZwUUFFWHWPR9nQVGsBl1VMWtm7CodpPAh4o79bZM9XU4T1wPVCvIzgGfuzIvsuwT7gHINwo1ASkBGCQCYDAEFKEEplpzAvCzsc5ga6CFmqmsv5onMAUUoQSmWnMC8LOxzmBroIWaqay/micYMAtAYkkA8OZFIGpxBEYYT+3A7Okba4WOq4NtwctIIZvCM48VU8pxQNjVvHMcJWPOP1Wh2Bw1VH7/Sg9lt9DL4DAwjBg=",
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEECDlp5HtG4UpmG6QLEwaCUJ3TR0qWHEarwFuN7JkKUrPmQ3Zi3Nq/TFayJYQRvez268whgWhBhQudIm84xNwPXjcKNQEpARgkAmAwBBTJ3+WZAQkWgZboUpiyZL3FV8R8UzAFFMnf5ZkBCRaBluhSmLJkvcVXxHxTGDALQO9QSAdvJkM6b/wIc07MCw1ma46lTyGYG8nvpn0ICI73nuD3QeaWwGIQTkVGEpzF+TuDK7gtTz7YUrR+PSnvMk8Y"
],
"0/62/5": 5,
"0/62/65532": 0,
"0/62/65533": 1,
"0/62/65528": [1, 3, 5, 8],
"0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11],
"0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533],
"0/63/0": [],
"0/63/1": [],
"0/63/2": 3,
"0/63/3": 3,
"0/63/65532": 0,
"0/63/65533": 1,
"0/63/65528": [2, 5],
"0/63/65529": [0, 1, 3, 4],
"0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"1/3/0": 0,
"1/3/1": 2,
"1/3/65532": 0,
"1/3/65533": 4,
"1/3/65528": [],
"1/3/65529": [0],
"1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/4/0": 128,
"1/4/65532": 1,
"1/4/65533": 4,
"1/4/65528": [0, 1, 2, 3],
"1/4/65529": [0, 1, 2, 3, 4, 5],
"1/4/65531": [0, 65528, 65529, 65531, 65532, 65533],
"1/6/0": false,
"1/6/16384": true,
"1/6/16385": 0,
"1/6/16386": 0,
"1/6/16387": null,
"1/6/65532": 1,
"1/6/65533": 4,
"1/6/65528": [],
"1/6/65529": [0, 1, 2, 64, 65, 66],
"1/6/65531": [
0, 16384, 16385, 16386, 16387, 65528, 65529, 65531, 65532, 65533
],
"1/29/0": [
{
"0": 266,
"1": 1
}
],
"1/29/1": [3, 4, 6, 29, 319486977],
"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/319486977/319422464": "AAFQCwIAAAMC+xkEDFJWNDRMMUEwMDA4MZwBAP8EAQIA1PkBAWABZNAEAAAAAEUFBQAAAABGCQUAAAAOAABCBkkGBQwIEIABRBEFFAAFAzwAAAAAAAAAAAAAAEcRBSoh/CGWImgjeAAAADwAAABIBgUAAAAAAEoGBQAAAAAA",
"1/319486977/319422466": "BEZiAQAAAAAAAAAABgsCDAINAgcCDgEBAn4PABAAWgAAs8c+AQEA",
"1/319486977/319422467": "EgtaAAB74T4BDwAANwkAAAAA",
"1/319486977/319422471": 0,
"1/319486977/319422472": 238.8000030517578,
"1/319486977/319422473": 0.0,
"1/319486977/319422474": 0.0,
"1/319486977/319422475": 0.2200000286102295,
"1/319486977/319422476": 0,
"1/319486977/319422478": 0,
"1/319486977/319422481": false,
"1/319486977/319422482": 54272,
"1/319486977/65533": 1,
"1/319486977/65528": [],
"1/319486977/65529": [],
"1/319486977/65531": [
65528, 65529, 65531, 319422464, 319422465, 319422466, 319422467,
319422468, 319422469, 319422471, 319422472, 319422473, 319422474,
319422475, 319422476, 319422478, 319422481, 319422482, 65533
]
},
"attribute_subscriptions": [],
"last_subscription_attempt": 0
}

View File

@ -145,9 +145,12 @@ async def test_node_added_subscription(
) -> None: ) -> None:
"""Test subscription to new devices work.""" """Test subscription to new devices work."""
assert matter_client.subscribe_events.call_count == 4 assert matter_client.subscribe_events.call_count == 4
assert matter_client.subscribe_events.call_args[0][1] == EventType.NODE_ADDED assert (
matter_client.subscribe_events.call_args.kwargs["event_filter"]
== EventType.NODE_ADDED
)
node_added_callback = matter_client.subscribe_events.call_args[0][0] node_added_callback = matter_client.subscribe_events.call_args.kwargs["callback"]
node_data = load_and_parse_node_fixture("onoff-light") node_data = load_and_parse_node_fixture("onoff-light")
node = MatterNode( node = MatterNode(
dataclass_from_dict( dataclass_from_dict(

View File

@ -1,5 +1,6 @@
"""Test Matter sensors.""" """Test Matter sensors."""
from unittest.mock import MagicMock from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock, patch
from matter_server.client.models.node import MatterNode from matter_server.client.models.node import MatterNode
import pytest import pytest
@ -14,6 +15,8 @@ from .common import (
trigger_subscription_callback, trigger_subscription_callback,
) )
from tests.common import async_fire_time_changed
@pytest.fixture(name="flow_sensor_node") @pytest.fixture(name="flow_sensor_node")
async def flow_sensor_node_fixture( async def flow_sensor_node_fixture(
@ -63,6 +66,16 @@ async def temperature_sensor_node_fixture(
) )
@pytest.fixture(name="eve_energy_plug_node")
async def eve_energy_plug_node_fixture(
hass: HomeAssistant, matter_client: MagicMock
) -> MatterNode:
"""Fixture for a Eve Energy Plug node."""
return await setup_integration_with_node_fixture(
hass, "eve-energy-plug", matter_client
)
# This tests needs to be adjusted to remove lingering tasks # This tests needs to be adjusted to remove lingering tasks
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
async def test_sensor_null_value( async def test_sensor_null_value(
@ -208,3 +221,70 @@ async def test_battery_sensor(
assert entry assert entry
assert entry.entity_category == EntityCategory.DIAGNOSTIC assert entry.entity_category == EntityCategory.DIAGNOSTIC
# This tests needs to be adjusted to remove lingering tasks
@pytest.mark.parametrize("expected_lingering_tasks", [True])
async def test_eve_energy_sensors(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
matter_client: MagicMock,
eve_energy_plug_node: MatterNode,
) -> None:
"""Test Energy sensors created from Eve Energy custom cluster."""
# power sensor
entity_id = "sensor.eve_energy_plug_power"
state = hass.states.get(entity_id)
assert state
assert state.state == "0.0"
assert state.attributes["unit_of_measurement"] == "W"
assert state.attributes["device_class"] == "power"
assert state.attributes["friendly_name"] == "Eve Energy Plug Power"
# voltage sensor
entity_id = "sensor.eve_energy_plug_voltage"
state = hass.states.get(entity_id)
assert state
assert state.state == "238.800003051758"
assert state.attributes["unit_of_measurement"] == "V"
assert state.attributes["device_class"] == "voltage"
assert state.attributes["friendly_name"] == "Eve Energy Plug Voltage"
# energy sensor
entity_id = "sensor.eve_energy_plug_energy"
state = hass.states.get(entity_id)
assert state
assert state.state == "0.220000028610229"
assert state.attributes["unit_of_measurement"] == "kWh"
assert state.attributes["device_class"] == "energy"
assert state.attributes["friendly_name"] == "Eve Energy Plug Energy"
assert state.attributes["state_class"] == "total_increasing"
# current sensor
entity_id = "sensor.eve_energy_plug_current"
state = hass.states.get(entity_id)
assert state
assert state.state == "0.0"
assert state.attributes["unit_of_measurement"] == "A"
assert state.attributes["device_class"] == "current"
assert state.attributes["friendly_name"] == "Eve Energy Plug Current"
# test if the sensor gets polled on interval
eve_energy_plug_node.update_attribute("1/319486977/319422472", 237.0)
async_fire_time_changed(hass, datetime.now(UTC) + timedelta(seconds=31))
await hass.async_block_till_done()
entity_id = "sensor.eve_energy_plug_voltage"
state = hass.states.get(entity_id)
assert state
assert state.state == "237.0"
# test extra poll triggered when secondary value (switch state) changes
set_node_attribute(eve_energy_plug_node, 1, 6, 0, True)
eve_energy_plug_node.update_attribute("1/319486977/319422474", 5.0)
with patch("homeassistant.components.matter.entity.EXTRA_POLL_DELAY", 0.0):
await trigger_subscription_callback(hass, matter_client)
await hass.async_block_till_done()
entity_id = "sensor.eve_energy_plug_power"
state = hass.states.get(entity_id)
assert state
assert state.state == "5.0"