Add "Summation Delivered" Sensor for SmartEnergy metering ZHA channel (#56666)

This commit is contained in:
Alexei Chetroi 2021-09-29 12:35:20 -04:00 committed by GitHub
parent 60eb426451
commit dbba2c4afe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 785 additions and 103 deletions

View File

@ -148,6 +148,10 @@ class ZigbeeChannel(LogMixin):
"""Return the status of the channel."""
return self._status
def __hash__(self) -> int:
"""Make this a hashable."""
return hash(self._unique_id)
@callback
def async_send_signal(self, signal: str, *args: Any) -> None:
"""Send a signal through hass dispatcher."""

View File

@ -1,19 +1,13 @@
"""Smart energy channels module for Zigbee Home Automation."""
from __future__ import annotations
import enum
from functools import partialmethod
from zigpy.zcl.clusters import smartenergy
from homeassistant.const import (
POWER_WATT,
TIME_HOURS,
TIME_SECONDS,
VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE,
VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR,
)
from homeassistant.core import callback
from .. import registries, typing as zha_typing
from ..const import REPORT_CONFIG_DEFAULT
from ..const import REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_OP
from .base import ZigbeeChannel
@ -61,59 +55,101 @@ class Messaging(ZigbeeChannel):
class Metering(ZigbeeChannel):
"""Metering channel."""
REPORT_CONFIG = ({"attr": "instantaneous_demand", "config": REPORT_CONFIG_DEFAULT},)
REPORT_CONFIG = (
{"attr": "instantaneous_demand", "config": REPORT_CONFIG_OP},
{"attr": "current_summ_delivered", "config": REPORT_CONFIG_DEFAULT},
{"attr": "status", "config": REPORT_CONFIG_ASAP},
)
ZCL_INIT_ATTRS = {
"divisor": True,
"multiplier": True,
"unit_of_measure": True,
"demand_formatting": True,
"divisor": True,
"metering_device_type": True,
"multiplier": True,
"summa_formatting": True,
"unit_of_measure": True,
}
unit_of_measure_map = {
0x00: POWER_WATT,
0x01: VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR,
0x02: VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE,
0x03: f"ccf/{TIME_HOURS}",
0x04: f"US gal/{TIME_HOURS}",
0x05: f"IMP gal/{TIME_HOURS}",
0x06: f"BTU/{TIME_HOURS}",
0x07: f"l/{TIME_HOURS}",
0x08: "kPa",
0x09: "kPa",
0x0A: f"mcf/{TIME_HOURS}",
0x0B: "unitless",
0x0C: f"MJ/{TIME_SECONDS}",
metering_device_type = {
0: "Electric Metering",
1: "Gas Metering",
2: "Water Metering",
3: "Thermal Metering",
4: "Pressure Metering",
5: "Heat Metering",
6: "Cooling Metering",
128: "Mirrored Gas Metering",
129: "Mirrored Water Metering",
130: "Mirrored Thermal Metering",
131: "Mirrored Pressure Metering",
132: "Mirrored Heat Metering",
133: "Mirrored Cooling Metering",
}
class DeviceStatusElectric(enum.IntFlag):
"""Metering Device Status."""
NO_ALARMS = 0
CHECK_METER = 1
LOW_BATTERY = 2
TAMPER_DETECT = 4
POWER_FAILURE = 8
POWER_QUALITY = 16
LEAK_DETECT = 32 # Really?
SERVICE_DISCONNECT = 64
RESERVED = 128
class DeviceStatusDefault(enum.IntFlag):
"""Metering Device Status."""
NO_ALARMS = 0
class FormatSelector(enum.IntEnum):
"""Format specified selector."""
DEMAND = 0
SUMMATION = 1
def __init__(
self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType
) -> None:
"""Initialize Metering."""
super().__init__(cluster, ch_pool)
self._format_spec = None
self._summa_format = None
@property
def divisor(self) -> int:
"""Return divisor for the value."""
return self.cluster.get("divisor") or 1
@property
def device_type(self) -> int | None:
"""Return metering device type."""
dev_type = self.cluster.get("metering_device_type")
if dev_type is None:
return None
return self.metering_device_type.get(dev_type, dev_type)
@property
def multiplier(self) -> int:
"""Return multiplier for the value."""
return self.cluster.get("multiplier") or 1
@callback
def attribute_updated(self, attrid: int, value: int) -> None:
"""Handle attribute update from Metering cluster."""
if None in (self.multiplier, self.divisor, self._format_spec):
return
super().attribute_updated(attrid, value)
@property
def status(self) -> int | None:
"""Return metering device status."""
status = self.cluster.get("status")
if status is None:
return None
if self.cluster.get("metering_device_type") == 0:
# Electric metering device type
return self.DeviceStatusElectric(status)
return self.DeviceStatusDefault(status)
@property
def unit_of_measurement(self) -> str:
"""Return unit of measurement."""
uom = self.cluster.get("unit_of_measure", 0x7F)
return self.unit_of_measure_map.get(uom & 0x7F, "unknown")
return self.cluster.get("unit_of_measure")
async def async_initialize_channel_specific(self, from_cache: bool) -> None:
"""Fetch config from device and updates format specifier."""
@ -121,29 +157,49 @@ class Metering(ZigbeeChannel):
fmting = self.cluster.get(
"demand_formatting", 0xF9
) # 1 digit to the right, 15 digits to the left
self._format_spec = self.get_formatting(fmting)
r_digits = int(fmting & 0x07) # digits to the right of decimal point
l_digits = (fmting >> 3) & 0x0F # digits to the left of decimal point
fmting = self.cluster.get(
"summa_formatting", 0xF9
) # 1 digit to the right, 15 digits to the left
self._summa_format = self.get_formatting(fmting)
@staticmethod
def get_formatting(formatting: int) -> str:
"""Return a formatting string, given the formatting value.
Bits 0 to 2: Number of Digits to the right of the Decimal Point.
Bits 3 to 6: Number of Digits to the left of the Decimal Point.
Bit 7: If set, suppress leading zeros.
"""
r_digits = int(formatting & 0x07) # digits to the right of decimal point
l_digits = (formatting >> 3) & 0x0F # digits to the left of decimal point
if l_digits == 0:
l_digits = 15
width = r_digits + l_digits + (1 if r_digits > 0 else 0)
if fmting & 0x80:
self._format_spec = "{:" + str(width) + "." + str(r_digits) + "f}"
else:
self._format_spec = "{:0" + str(width) + "." + str(r_digits) + "f}"
if formatting & 0x80:
# suppress leading 0
return f"{{:{width}.{r_digits}f}}"
def formatter_function(self, value: int) -> int | float:
return f"{{:0{width}.{r_digits}f}}"
def _formatter_function(self, selector: FormatSelector, value: int) -> int | float:
"""Return formatted value for display."""
value = value * self.multiplier / self.divisor
if self.unit_of_measurement == POWER_WATT:
if self.unit_of_measurement == 0:
# Zigbee spec power unit is kW, but we show the value in W
value_watt = value * 1000
if value_watt < 100:
return round(value_watt, 1)
return round(value_watt)
if selector == self.FormatSelector.SUMMATION:
return self._summa_format.format(value).lstrip()
return self._format_spec.format(value).lstrip()
demand_formatter = partialmethod(_formatter_function, FormatSelector.DEMAND)
summa_formatter = partialmethod(_formatter_function, FormatSelector.SUMMATION)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Prepayment.cluster_id)
class Prepayment(ZigbeeChannel):

View File

@ -46,7 +46,8 @@ async def async_add_entities(
"""Add entities helper."""
if not entities:
return
to_add = [ent_cls(*args) for ent_cls, args in entities]
to_add = [ent_cls.create_entity(*args) for ent_cls, args in entities]
to_add = [entity for entity in to_add if entity is not None]
_async_add_entities(to_add, update_before_add=update_before_add)
entities.clear()
@ -63,6 +64,7 @@ class ProbeEndpoint:
"""Process an endpoint on a zigpy device."""
self.discover_by_device_type(channel_pool)
self.discover_by_cluster_id(channel_pool)
self.discover_multi_entities(channel_pool)
@callback
def discover_by_device_type(self, channel_pool: zha_typing.ChannelPoolType) -> None:
@ -159,6 +161,31 @@ class ProbeEndpoint:
channel = channel_class(cluster, ep_channels)
self.probe_single_cluster(component, channel, ep_channels)
@staticmethod
@callback
def discover_multi_entities(channel_pool: zha_typing.ChannelPoolType) -> None:
"""Process an endpoint on and discover multiple entities."""
remaining_channels = channel_pool.unclaimed_channels()
for channel in remaining_channels:
unique_id = f"{channel_pool.unique_id}-{channel.cluster.cluster_id}"
matches, claimed = zha_regs.ZHA_ENTITIES.get_multi_entity(
channel_pool.manufacturer,
channel_pool.model,
channel,
remaining_channels,
)
if not claimed:
continue
channel_pool.claim_channels(claimed)
for component, ent_classes_list in matches.items():
for entity_class in ent_classes_list:
channel_pool.async_new_entity(
component, entity_class, unique_id, claimed
)
def initialize(self, hass: HomeAssistant) -> None:
"""Update device overrides config."""
zha_config = hass.data[zha_const.DATA_ZHA].get(zha_const.DATA_ZHA_CONFIG, {})

View File

@ -84,7 +84,6 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {
zcl.clusters.measurement.RelativeHumidity.cluster_id: SENSOR,
zcl.clusters.measurement.TemperatureMeasurement.cluster_id: SENSOR,
zcl.clusters.security.IasZone.cluster_id: BINARY_SENSOR,
zcl.clusters.smartenergy.Metering.cluster_id: SENSOR,
}
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {
@ -247,7 +246,9 @@ class ZHAEntityRegistry:
def __init__(self):
"""Initialize Registry instance."""
self._strict_registry: RegistryDictType = collections.defaultdict(dict)
self._loose_registry: RegistryDictType = collections.defaultdict(dict)
self._multi_entity_registry: RegistryDictType = collections.defaultdict(
lambda: collections.defaultdict(list)
)
self._group_registry: GroupRegistryDictType = {}
def get_entity(
@ -267,6 +268,27 @@ class ZHAEntityRegistry:
return default, []
def get_multi_entity(
self,
manufacturer: str,
model: str,
primary_channel: ChannelType,
aux_channels: list[ChannelType],
components: set | None = None,
) -> tuple[dict[str, list[CALLABLE_T]], list[ChannelType]]:
"""Match ZHA Channels to potentially multiple ZHA Entity classes."""
result: dict[str, list[CALLABLE_T]] = collections.defaultdict(list)
claimed: set[ChannelType] = set()
for component in components or self._multi_entity_registry:
matches = self._multi_entity_registry[component]
for match in sorted(matches, key=lambda x: x.weight, reverse=True):
if match.strict_matched(manufacturer, model, [primary_channel]):
claimed |= set(match.claim_channels(aux_channels))
ent_classes = self._multi_entity_registry[component][match]
result[component].extend(ent_classes)
return result, list(claimed)
def get_group_entity(self, component: str) -> CALLABLE_T:
"""Match a ZHA group to a ZHA Entity class."""
return self._group_registry.get(component)
@ -296,7 +318,7 @@ class ZHAEntityRegistry:
return decorator
def loose_match(
def multipass_match(
self,
component: str,
channel_names: Callable | set[str] | str = None,
@ -316,7 +338,7 @@ class ZHAEntityRegistry:
All non empty fields of a match rule must match.
"""
self._loose_registry[component][rule] = zha_entity
self._multi_entity_registry[component][rule].append(zha_entity)
return zha_entity
return decorator

View File

@ -41,12 +41,16 @@ UPDATE_GROUP_FROM_CHILD_DELAY = 0.5
class BaseZhaEntity(LogMixin, entity.Entity):
"""A base class for ZHA entities."""
_unique_id_suffix: str | None = None
def __init__(self, unique_id: str, zha_device: ZhaDeviceType, **kwargs) -> None:
"""Init ZHA entity."""
self._name: str = ""
self._force_update: bool = False
self._should_poll: bool = False
self._unique_id: str = unique_id
if self._unique_id_suffix:
self._unique_id += f"-{self._unique_id_suffix}"
self._state: Any = None
self._extra_state_attributes: dict[str, Any] = {}
self._zha_device: ZhaDeviceType = zha_device
@ -142,6 +146,16 @@ class BaseZhaEntity(LogMixin, entity.Entity):
class ZhaEntity(BaseZhaEntity, RestoreEntity):
"""A base class for non group ZHA entities."""
def __init_subclass__(cls, id_suffix: str | None = None, **kwargs) -> None:
"""Initialize subclass.
:param id_suffix: suffix to add to the unique_id of the entity. Used for multi
entities using the same channel/cluster id for the entity.
"""
super().__init_subclass__(**kwargs)
if id_suffix:
cls._unique_id_suffix = id_suffix
def __init__(
self,
unique_id: str,
@ -155,10 +169,26 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity):
ch_names = [ch.cluster.ep_attribute for ch in channels]
ch_names = ", ".join(sorted(ch_names))
self._name: str = f"{zha_device.name} {ieeetail} {ch_names}"
if self._unique_id_suffix:
self._name += f" {self._unique_id_suffix}"
self.cluster_channels: dict[str, ChannelType] = {}
for channel in channels:
self.cluster_channels[channel.name] = channel
@classmethod
def create_entity(
cls,
unique_id: str,
zha_device: ZhaDeviceType,
channels: list[ChannelType],
**kwargs,
) -> ZhaEntity | None:
"""Entity Factory.
Return entity if it is a supported configuration, otherwise return None
"""
return cls(unique_id, zha_device, channels, **kwargs)
@property
def available(self) -> bool:
"""Return entity availability."""
@ -238,6 +268,16 @@ class ZhaGroupEntity(BaseZhaEntity):
"""Return entity availability."""
return self._available
@classmethod
def create_entity(
cls, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs
) -> ZhaGroupEntity | None:
"""Group Entity Factory.
Return entity if it is a supported configuration, otherwise return None
"""
return cls(entity_ids, unique_id, group_id, zha_device, **kwargs)
async def _handle_group_membership_changed(self):
"""Handle group membership changed."""
# Make sure we don't call remove twice as members are removed

View File

@ -16,17 +16,28 @@ from homeassistant.components.sensor import (
DEVICE_CLASS_TEMPERATURE,
DOMAIN,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
SensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_ENERGY,
ENERGY_KILO_WATT_HOUR,
LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
PRESSURE_HPA,
TEMP_CELSIUS,
TIME_HOURS,
TIME_SECONDS,
VOLUME_CUBIC_FEET,
VOLUME_CUBIC_METERS,
VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE,
VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR,
VOLUME_GALLONS,
VOLUME_LITERS,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -72,6 +83,7 @@ BATTERY_SIZES = {
CHANNEL_ST_HUMIDITY_CLUSTER = f"channel_0x{SMARTTHINGS_HUMIDITY_CLUSTER:04x}"
STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN)
MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, DOMAIN)
async def async_setup_entry(
@ -262,21 +274,100 @@ class Illuminance(Sensor):
return round(pow(10, ((value - 1) / 10000)), 1)
@STRICT_MATCH(channel_names=CHANNEL_SMARTENERGY_METERING)
@MULTI_MATCH(channel_names=CHANNEL_SMARTENERGY_METERING)
class SmartEnergyMetering(Sensor):
"""Metering sensor."""
SENSOR_ATTR = "instantaneous_demand"
_device_class = DEVICE_CLASS_POWER
SENSOR_ATTR: int | str = "instantaneous_demand"
_device_class: str | None = DEVICE_CLASS_POWER
_state_class: str | None = STATE_CLASS_MEASUREMENT
unit_of_measure_map = {
0x00: POWER_WATT,
0x01: VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR,
0x02: VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE,
0x03: f"100 {VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR}",
0x04: f"US {VOLUME_GALLONS}/{TIME_HOURS}",
0x05: f"IMP {VOLUME_GALLONS}/{TIME_HOURS}",
0x06: f"BTU/{TIME_HOURS}",
0x07: f"l/{TIME_HOURS}",
0x08: "kPa", # gauge
0x09: "kPa", # absolute
0x0A: f"1000 {VOLUME_GALLONS}/{TIME_HOURS}",
0x0B: "unitless",
0x0C: f"MJ/{TIME_SECONDS}",
}
@classmethod
def create_entity(
cls,
unique_id: str,
zha_device: ZhaDeviceType,
channels: list[ChannelType],
**kwargs,
) -> ZhaEntity | None:
"""Entity Factory.
Return entity if it is a supported configuration, otherwise return None
"""
se_channel = channels[0]
if cls.SENSOR_ATTR in se_channel.cluster.unsupported_attributes:
return None
return cls(unique_id, zha_device, channels, **kwargs)
def formatter(self, value: int) -> int | float:
"""Pass through channel formatter."""
return self._channel.formatter_function(value)
return self._channel.demand_formatter(value)
@property
def native_unit_of_measurement(self) -> str:
"""Return Unit of measurement."""
return self._channel.unit_of_measurement
return self.unit_of_measure_map.get(self._channel.unit_of_measurement)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return device state attrs for battery sensors."""
attrs = {}
if self._channel.device_type is not None:
attrs["device_type"] = self._channel.device_type
status = self._channel.status
if status is not None:
attrs["status"] = str(status)[len(status.__class__.__name__) + 1 :]
return attrs
@MULTI_MATCH(channel_names=CHANNEL_SMARTENERGY_METERING)
class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered"):
"""Smart Energy Metering summation sensor."""
SENSOR_ATTR: int | str = "current_summ_delivered"
_device_class: str | None = DEVICE_CLASS_ENERGY
_state_class: str = STATE_CLASS_TOTAL_INCREASING
unit_of_measure_map = {
0x00: ENERGY_KILO_WATT_HOUR,
0x01: VOLUME_CUBIC_METERS,
0x02: VOLUME_CUBIC_FEET,
0x03: f"100 {VOLUME_CUBIC_FEET}",
0x04: f"US {VOLUME_GALLONS}",
0x05: f"IMP {VOLUME_GALLONS}",
0x06: "BTU",
0x07: VOLUME_LITERS,
0x08: "kPa", # gauge
0x09: "kPa", # absolute
0x0A: f"1000 {VOLUME_CUBIC_FEET}",
0x0B: "unitless",
0x0C: "MJ",
}
def formatter(self, value: int) -> int | float:
"""Numeric pass-through formatter."""
if self._channel.unit_of_measurement != 0:
return self._channel.summa_formatter(value)
cooked = float(self._channel.multiplier * value) / self._channel.divisor
return round(cooked, 3)
@STRICT_MATCH(channel_names=CHANNEL_PRESSURE)

View File

@ -109,6 +109,18 @@ async def send_attributes_report(hass, cluster: zigpy.zcl.Cluster, attributes: d
async def find_entity_id(domain, zha_device, hass):
"""Find the entity id under the testing.
This is used to get the entity id in order to get the state from the state
machine so that we can test state changes.
"""
entities = await find_entity_ids(domain, zha_device, hass)
if not entities:
return None
return entities[0]
async def find_entity_ids(domain, zha_device, hass):
"""Find the entity ids under the testing.
This is used to get the entity id in order to get the state from the state
machine so that we can test state changes.
"""
@ -118,10 +130,11 @@ async def find_entity_id(domain, zha_device, hass):
enitiy_ids = hass.states.async_entity_ids(domain)
await hass.async_block_till_done()
res = []
for entity_id in enitiy_ids:
if entity_id.startswith(head):
return entity_id
return None
res.append(entity_id)
return res
def async_find_group_entity_id(hass, domain, group):

View File

@ -41,6 +41,7 @@ from .zha_devices_list import (
)
NO_TAIL_ID = re.compile("_\\d$")
UNIQUE_ID_HD = re.compile(r"^(([\da-fA-F]{2}:){7}[\da-fA-F]{2}-\d{1,3})", re.X)
@pytest.fixture
@ -102,12 +103,6 @@ async def test_devices(
finally:
zha_channels.ChannelPool.async_new_entity = orig_new_entity
entity_ids = hass_disable_services.states.async_entity_ids()
await hass_disable_services.async_block_till_done()
zha_entity_ids = {
ent for ent in entity_ids if ent.split(".")[0] in zha_const.PLATFORMS
}
if cluster_identify:
called = int(zha_device_joined_restored.name == "zha_device_joined")
assert cluster_identify.request.call_count == called
@ -128,26 +123,49 @@ async def test_devices(
event_channels = {
ch.id for pool in zha_dev.channels.pools for ch in pool.client_channels.values()
}
entity_map = device[DEV_SIG_ENT_MAP]
assert zha_entity_ids == {
e[DEV_SIG_ENT_MAP_ID]
for e in entity_map.values()
if not e.get("default_match", False)
}
assert event_channels == set(device[DEV_SIG_EVT_CHANNELS])
# build a dict of entity_class -> (component, unique_id, channels) tuple
ha_ent_info = {}
for call in _dispatch.call_args_list:
_, component, entity_cls, unique_id, channels = call[0]
key = (component, unique_id)
entity_id = entity_registry.async_get_entity_id(component, "zha", unique_id)
unique_id_head = UNIQUE_ID_HD.match(unique_id).group(0) # ieee + endpoint_id
ha_ent_info[(unique_id_head, entity_cls.__name__)] = (
component,
unique_id,
channels,
)
assert key in entity_map
assert entity_id is not None
no_tail_id = NO_TAIL_ID.sub("", entity_map[key][DEV_SIG_ENT_MAP_ID])
assert entity_id.startswith(no_tail_id)
assert {ch.name for ch in channels} == set(entity_map[key][DEV_SIG_CHANNELS])
assert entity_cls.__name__ == entity_map[key][DEV_SIG_ENT_MAP_CLASS]
for comp_id, ent_info in device[DEV_SIG_ENT_MAP].items():
component, unique_id = comp_id
no_tail_id = NO_TAIL_ID.sub("", ent_info[DEV_SIG_ENT_MAP_ID])
ha_entity_id = entity_registry.async_get_entity_id(component, "zha", unique_id)
assert ha_entity_id is not None
assert ha_entity_id.startswith(no_tail_id)
test_ent_class = ent_info[DEV_SIG_ENT_MAP_CLASS]
test_unique_id_head = UNIQUE_ID_HD.match(unique_id).group(0)
assert (test_unique_id_head, test_ent_class) in ha_ent_info
ha_comp, ha_unique_id, ha_channels = ha_ent_info[
(test_unique_id_head, test_ent_class)
]
assert component is ha_comp
# unique_id used for discover is the same for "multi entities"
assert unique_id.startswith(ha_unique_id)
assert {ch.name for ch in ha_channels} == set(ent_info[DEV_SIG_CHANNELS])
assert _dispatch.call_count == len(device[DEV_SIG_ENT_MAP])
entity_ids = hass_disable_services.states.async_entity_ids()
await hass_disable_services.async_block_till_done()
zha_entity_ids = {
ent for ent in entity_ids if ent.split(".")[0] in zha_const.PLATFORMS
}
assert zha_entity_ids == {
e[DEV_SIG_ENT_MAP_ID] for e in device[DEV_SIG_ENT_MAP].values()
}
def _get_first_identify_cluster(zigpy_device):
@ -279,21 +297,35 @@ async def test_discover_endpoint(device_info, channels_mock, hass):
assert device_info[DEV_SIG_EVT_CHANNELS] == sorted(
ch.id for pool in channels.pools for ch in pool.client_channels.values()
)
assert new_ent.call_count == len(
[
device_info
for device_info in device_info[DEV_SIG_ENT_MAP].values()
if not device_info.get("default_match", False)
]
)
assert new_ent.call_count == len(list(device_info[DEV_SIG_ENT_MAP].values()))
for call_args in new_ent.call_args_list:
comp, ent_cls, unique_id, channels = call_args[0]
map_id = (comp, unique_id)
assert map_id in device_info[DEV_SIG_ENT_MAP]
entity_info = device_info[DEV_SIG_ENT_MAP][map_id]
assert {ch.name for ch in channels} == set(entity_info[DEV_SIG_CHANNELS])
assert ent_cls.__name__ == entity_info[DEV_SIG_ENT_MAP_CLASS]
# build a dict of entity_class -> (component, unique_id, channels) tuple
ha_ent_info = {}
for call in new_ent.call_args_list:
component, entity_cls, unique_id, channels = call[0]
unique_id_head = UNIQUE_ID_HD.match(unique_id).group(0) # ieee + endpoint_id
ha_ent_info[(unique_id_head, entity_cls.__name__)] = (
component,
unique_id,
channels,
)
for comp_id, ent_info in device_info[DEV_SIG_ENT_MAP].items():
component, unique_id = comp_id
test_ent_class = ent_info[DEV_SIG_ENT_MAP_CLASS]
test_unique_id_head = UNIQUE_ID_HD.match(unique_id).group(0)
assert (test_unique_id_head, test_ent_class) in ha_ent_info
ha_comp, ha_unique_id, ha_channels = ha_ent_info[
(test_unique_id_head, test_ent_class)
]
assert component is ha_comp
# unique_id used for discover is the same for "multi entities"
assert unique_id.startswith(ha_unique_id)
assert {ch.name for ch in ha_channels} == set(ent_info[DEV_SIG_CHANNELS])
assert new_ent.call_count == len(device_info[DEV_SIG_ENT_MAP])
def _ch_mock(cluster):

View File

@ -314,3 +314,74 @@ def test_weighted_match(channel, entity_registry, manufacturer, model, match_nam
assert match.__name__ == match_name
assert claimed == [ch_on_off]
def test_multi_sensor_match(channel, entity_registry):
"""Test multi-entity match."""
s = mock.sentinel
@entity_registry.multipass_match(
s.binary_sensor,
channel_names="smartenergy_metering",
)
class SmartEnergySensor2:
pass
ch_se = channel("smartenergy_metering", 0x0702)
ch_illuminati = channel("illuminance", 0x0401)
match, claimed = entity_registry.get_multi_entity(
"manufacturer",
"model",
primary_channel=ch_illuminati,
aux_channels=[ch_se, ch_illuminati],
)
assert s.binary_sensor not in match
assert s.component not in match
assert set(claimed) == set()
match, claimed = entity_registry.get_multi_entity(
"manufacturer",
"model",
primary_channel=ch_se,
aux_channels=[ch_se, ch_illuminati],
)
assert s.binary_sensor in match
assert s.component not in match
assert set(claimed) == {ch_se}
assert {cls.__name__ for cls in match[s.binary_sensor]} == {
SmartEnergySensor2.__name__
}
@entity_registry.multipass_match(
s.component, channel_names="smartenergy_metering", aux_channels="illuminance"
)
class SmartEnergySensor1:
pass
@entity_registry.multipass_match(
s.binary_sensor,
channel_names="smartenergy_metering",
aux_channels="illuminance",
)
class SmartEnergySensor3:
pass
match, claimed = entity_registry.get_multi_entity(
"manufacturer",
"model",
primary_channel=ch_se,
aux_channels={ch_se, ch_illuminati},
)
assert s.binary_sensor in match
assert s.component in match
assert set(claimed) == {ch_se, ch_illuminati}
assert {cls.__name__ for cls in match[s.binary_sensor]} == {
SmartEnergySensor2.__name__,
SmartEnergySensor3.__name__,
}
assert {cls.__name__ for cls in match[s.component]} == {SmartEnergySensor1.__name__}

View File

@ -11,10 +11,13 @@ import zigpy.zcl.clusters.smartenergy as smartenergy
from homeassistant.components.sensor import DOMAIN
import homeassistant.config as config_util
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
CONF_UNIT_SYSTEM,
CONF_UNIT_SYSTEM_IMPERIAL,
CONF_UNIT_SYSTEM_METRIC,
DEVICE_CLASS_ENERGY,
ENERGY_KILO_WATT_HOUR,
LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
@ -23,6 +26,8 @@ from homeassistant.const import (
STATE_UNKNOWN,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
VOLUME_CUBIC_FEET,
VOLUME_CUBIC_METERS,
)
from homeassistant.helpers import restore_state
from homeassistant.util import dt as dt_util
@ -31,11 +36,14 @@ from .common import (
async_enable_traffic,
async_test_rejoin,
find_entity_id,
find_entity_ids,
send_attribute_report,
send_attributes_report,
)
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE
ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_e769900a_{}"
async def async_test_humidity(hass, cluster, entity_id):
"""Test humidity sensor."""
@ -65,9 +73,38 @@ async def async_test_illuminance(hass, cluster, entity_id):
async def async_test_metering(hass, cluster, entity_id):
"""Test metering sensor."""
"""Test Smart Energy metering sensor."""
await send_attributes_report(hass, cluster, {1025: 1, 1024: 12345, 1026: 100})
assert_state(hass, entity_id, "12345.0", "unknown")
assert_state(hass, entity_id, "12345.0", None)
assert hass.states.get(entity_id).attributes["status"] == "NO_ALARMS"
assert hass.states.get(entity_id).attributes["device_type"] == "Electric Metering"
await send_attributes_report(hass, cluster, {1024: 12346, "status": 64 + 8})
assert_state(hass, entity_id, "12346.0", None)
assert (
hass.states.get(entity_id).attributes["status"]
== "SERVICE_DISCONNECT|POWER_FAILURE"
)
await send_attributes_report(
hass, cluster, {"status": 32, "metering_device_type": 1}
)
# currently only statuses for electric meters are supported
assert hass.states.get(entity_id).attributes["status"] == "<bitmap8.32: 32>"
async def async_test_smart_energy_summation(hass, cluster, entity_id):
"""Test SmartEnergy Summation delivered sensro."""
await send_attributes_report(
hass, cluster, {1025: 1, "current_summ_delivered": 12321, 1026: 100}
)
assert_state(hass, entity_id, "12.32", VOLUME_CUBIC_METERS)
assert hass.states.get(entity_id).attributes["status"] == "NO_ALARMS"
assert hass.states.get(entity_id).attributes["device_type"] == "Electric Metering"
assert (
hass.states.get(entity_id).attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY
)
async def async_test_electrical_measurement(hass, cluster, entity_id):
@ -106,40 +143,81 @@ async def async_test_powerconfiguration(hass, cluster, entity_id):
@pytest.mark.parametrize(
"cluster_id, test_func, report_count, read_plug",
"cluster_id, entity_suffix, test_func, report_count, read_plug, unsupported_attrs",
(
(measurement.RelativeHumidity.cluster_id, async_test_humidity, 1, None),
(
measurement.RelativeHumidity.cluster_id,
"humidity",
async_test_humidity,
1,
None,
None,
),
(
measurement.TemperatureMeasurement.cluster_id,
"temperature",
async_test_temperature,
1,
None,
None,
),
(
measurement.PressureMeasurement.cluster_id,
"pressure",
async_test_pressure,
1,
None,
None,
),
(measurement.PressureMeasurement.cluster_id, async_test_pressure, 1, None),
(
measurement.IlluminanceMeasurement.cluster_id,
"illuminance",
async_test_illuminance,
1,
None,
None,
),
(
smartenergy.Metering.cluster_id,
"smartenergy_metering",
async_test_metering,
1,
{
"demand_formatting": 0xF9,
"divisor": 1,
"metering_device_type": 0x00,
"multiplier": 1,
"status": 0x00,
},
{"current_summ_delivered"},
),
(
smartenergy.Metering.cluster_id,
"smartenergy_metering_summation_delivered",
async_test_smart_energy_summation,
1,
{
"demand_formatting": 0xF9,
"divisor": 1000,
"metering_device_type": 0x00,
"multiplier": 1,
"status": 0x00,
"summa_formatting": 0b1_0111_010,
"unit_of_measure": 0x01,
},
{"instaneneous_demand"},
),
(
homeautomation.ElectricalMeasurement.cluster_id,
"electrical_measurement",
async_test_electrical_measurement,
1,
None,
None,
),
(
general.PowerConfiguration.cluster_id,
"power",
async_test_powerconfiguration,
2,
{
@ -147,6 +225,7 @@ async def async_test_powerconfiguration(hass, cluster, entity_id):
"battery_voltage": 29,
"battery_quantity": 3,
},
None,
),
),
)
@ -155,9 +234,11 @@ async def test_sensor(
zigpy_device_mock,
zha_device_joined_restored,
cluster_id,
entity_suffix,
test_func,
report_count,
read_plug,
unsupported_attrs,
):
"""Test zha sensor platform."""
@ -171,12 +252,15 @@ async def test_sensor(
}
)
cluster = zigpy_device.endpoints[1].in_clusters[cluster_id]
if unsupported_attrs:
for attr in unsupported_attrs:
cluster.add_unsupported_attribute(attr)
if cluster_id == smartenergy.Metering.cluster_id:
# this one is mains powered
zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100
cluster.PLUGGED_ATTR_READS = read_plug
zha_device = await zha_device_joined_restored(zigpy_device)
entity_id = await find_entity_id(DOMAIN, zha_device, hass)
entity_id = ENTITY_ID_PREFIX.format(entity_suffix)
await async_enable_traffic(hass, [zha_device], enabled=False)
await hass.async_block_till_done()
@ -372,3 +456,197 @@ async def test_electrical_measurement_init(
assert channel.divisor == 10
assert channel.multiplier == 20
assert hass.states.get(entity_id).state == "60.0"
@pytest.mark.parametrize(
"cluster_id, unsupported_attributes, entity_ids, missing_entity_ids",
(
(
smartenergy.Metering.cluster_id,
{
"instantaneous_demand",
},
{
"smartenergy_metering_summation_delivered",
},
{
"smartenergy_metering",
},
),
(
smartenergy.Metering.cluster_id,
{"instantaneous_demand", "current_summ_delivered"},
{},
{
"smartenergy_metering_summation_delivered",
"smartenergy_metering",
},
),
(
smartenergy.Metering.cluster_id,
{},
{
"smartenergy_metering_summation_delivered",
"smartenergy_metering",
},
{},
),
),
)
async def test_unsupported_attributes_sensor(
hass,
zigpy_device_mock,
zha_device_joined_restored,
cluster_id,
unsupported_attributes,
entity_ids,
missing_entity_ids,
):
"""Test zha sensor platform."""
entity_ids = {ENTITY_ID_PREFIX.format(e) for e in entity_ids}
missing_entity_ids = {ENTITY_ID_PREFIX.format(e) for e in missing_entity_ids}
zigpy_device = zigpy_device_mock(
{
1: {
SIG_EP_INPUT: [cluster_id, general.Basic.cluster_id],
SIG_EP_OUTPUT: [],
SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
}
}
)
cluster = zigpy_device.endpoints[1].in_clusters[cluster_id]
if cluster_id == smartenergy.Metering.cluster_id:
# this one is mains powered
zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100
for attr in unsupported_attributes:
cluster.add_unsupported_attribute(attr)
zha_device = await zha_device_joined_restored(zigpy_device)
await async_enable_traffic(hass, [zha_device], enabled=False)
await hass.async_block_till_done()
present_entity_ids = set(await find_entity_ids(DOMAIN, zha_device, hass))
assert present_entity_ids == entity_ids
assert missing_entity_ids not in present_entity_ids
@pytest.mark.parametrize(
"raw_uom, raw_value, expected_state, expected_uom",
(
(
1,
12320,
"1.23",
VOLUME_CUBIC_METERS,
),
(
1,
1232000,
"123.20",
VOLUME_CUBIC_METERS,
),
(
3,
2340,
"0.23",
f"100 {VOLUME_CUBIC_FEET}",
),
(
3,
2360,
"0.24",
f"100 {VOLUME_CUBIC_FEET}",
),
(
8,
23660,
"2.37",
"kPa",
),
(
0,
9366,
"0.937",
ENERGY_KILO_WATT_HOUR,
),
(
0,
999,
"0.1",
ENERGY_KILO_WATT_HOUR,
),
(
0,
10091,
"1.009",
ENERGY_KILO_WATT_HOUR,
),
(
0,
10099,
"1.01",
ENERGY_KILO_WATT_HOUR,
),
(
0,
100999,
"10.1",
ENERGY_KILO_WATT_HOUR,
),
(
0,
100023,
"10.002",
ENERGY_KILO_WATT_HOUR,
),
(
0,
102456,
"10.246",
ENERGY_KILO_WATT_HOUR,
),
),
)
async def test_se_summation_uom(
hass,
zigpy_device_mock,
zha_device_joined,
raw_uom,
raw_value,
expected_state,
expected_uom,
):
"""Test zha smart energy summation."""
entity_id = ENTITY_ID_PREFIX.format("smartenergy_metering_summation_delivered")
zigpy_device = zigpy_device_mock(
{
1: {
SIG_EP_INPUT: [
smartenergy.Metering.cluster_id,
general.Basic.cluster_id,
],
SIG_EP_OUTPUT: [],
SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.SIMPLE_SENSOR,
}
}
)
zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100
cluster = zigpy_device.endpoints[1].in_clusters[smartenergy.Metering.cluster_id]
for attr in ("instanteneous_demand",):
cluster.add_unsupported_attribute(attr)
cluster.PLUGGED_ATTR_READS = {
"current_summ_delivered": raw_value,
"demand_formatting": 0xF9,
"divisor": 10000,
"metering_device_type": 0x00,
"multiplier": 1,
"status": 0x00,
"summa_formatting": 0b1_0111_010,
"unit_of_measure": raw_uom,
}
await zha_device_joined(zigpy_device)
assert_state(hass, entity_id, expected_state, expected_uom)

View File

@ -118,6 +118,7 @@ DEVICES = [
DEV_SIG_ENTITIES: [
"sensor.centralite_3210_l_77665544_electrical_measurement",
"sensor.centralite_3210_l_77665544_smartenergy_metering",
"sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered",
"switch.centralite_3210_l_77665544_on_off",
],
DEV_SIG_ENT_MAP: {
@ -131,6 +132,11 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering",
DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_smartenergy_metering",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): {
DEV_SIG_CHANNELS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered",
},
("sensor", "00:11:22:33:44:55:66:77-1-2820"): {
DEV_SIG_CHANNELS: ["electrical_measurement"],
DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement",
@ -391,6 +397,7 @@ DEVICES = [
},
DEV_SIG_ENTITIES: [
"sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering",
"sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering_summation_delivered",
"switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off",
],
DEV_SIG_ENT_MAP: {
@ -404,6 +411,11 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering",
DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): {
DEV_SIG_CHANNELS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering_summation_delivered",
},
},
DEV_SIG_EVT_CHANNELS: ["4:0x0019"],
SIG_MANUFACTURER: "ClimaxTechnology",
@ -943,6 +955,7 @@ DEVICES = [
DEV_SIG_ENTITIES: [
"light.jasco_products_45852_77665544_level_on_off",
"sensor.jasco_products_45852_77665544_smartenergy_metering",
"sensor.jasco_products_45852_77665544_smartenergy_metering_summation_delivered",
],
DEV_SIG_ENT_MAP: {
("light", "00:11:22:33:44:55:66:77-1"): {
@ -955,6 +968,11 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering",
DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_smartenergy_metering",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): {
DEV_SIG_CHANNELS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_smartenergy_metering_summation_delivered",
},
},
DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"],
SIG_MANUFACTURER: "Jasco Products",
@ -982,6 +1000,7 @@ DEVICES = [
DEV_SIG_ENTITIES: [
"light.jasco_products_45856_77665544_on_off",
"sensor.jasco_products_45856_77665544_smartenergy_metering",
"sensor.jasco_products_45856_77665544_smartenergy_metering_summation_delivered",
],
DEV_SIG_ENT_MAP: {
("light", "00:11:22:33:44:55:66:77-1"): {
@ -994,6 +1013,11 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering",
DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_smartenergy_metering",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): {
DEV_SIG_CHANNELS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_smartenergy_metering_summation_delivered",
},
},
DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"],
SIG_MANUFACTURER: "Jasco Products",
@ -1021,6 +1045,7 @@ DEVICES = [
DEV_SIG_ENTITIES: [
"light.jasco_products_45857_77665544_level_on_off",
"sensor.jasco_products_45857_77665544_smartenergy_metering",
"sensor.jasco_products_45857_77665544_smartenergy_metering_summation_delivered",
],
DEV_SIG_ENT_MAP: {
("light", "00:11:22:33:44:55:66:77-1"): {
@ -1033,6 +1058,11 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering",
DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_smartenergy_metering",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): {
DEV_SIG_CHANNELS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_smartenergy_metering_summation_delivered",
},
},
DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"],
SIG_MANUFACTURER: "Jasco Products",
@ -2782,12 +2812,6 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "IASZone",
DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_multi_77665544_ias_zone",
},
("binary_sensor", "00:11:22:33:44:55:66:77-1-64514"): {
DEV_SIG_CHANNELS: ["manufacturer_specific"],
DEV_SIG_ENT_MAP_CLASS: "BinaryInput",
DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_multi_77665544_manufacturer_specific",
"default_match": True,
},
},
DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
SIG_MANUFACTURER: "Samjin",
@ -2925,6 +2949,7 @@ DEVICES = [
"light.sercomm_corp_sz_esw01_77665544_on_off",
"sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement",
"sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering",
"sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered",
],
DEV_SIG_ENT_MAP: {
("light", "00:11:22:33:44:55:66:77-1"): {
@ -2937,6 +2962,11 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering",
DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): {
DEV_SIG_CHANNELS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered",
},
("sensor", "00:11:22:33:44:55:66:77-1-2820"): {
DEV_SIG_CHANNELS: ["electrical_measurement"],
DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement",
@ -3423,6 +3453,7 @@ DEVICES = [
DEV_SIG_ENTITIES: [
"light.sengled_e11_g13_77665544_level_on_off",
"sensor.sengled_e11_g13_77665544_smartenergy_metering",
"sensor.sengled_e11_g13_77665544_smartenergy_metering_summation_delivered",
],
DEV_SIG_ENT_MAP: {
("light", "00:11:22:33:44:55:66:77-1"): {
@ -3435,6 +3466,11 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering",
DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_smartenergy_metering",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): {
DEV_SIG_CHANNELS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_smartenergy_metering_summation_delivered",
},
},
DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
SIG_MANUFACTURER: "sengled",
@ -3455,6 +3491,7 @@ DEVICES = [
DEV_SIG_ENTITIES: [
"light.sengled_e12_n14_77665544_level_on_off",
"sensor.sengled_e12_n14_77665544_smartenergy_metering",
"sensor.sengled_e12_n14_77665544_smartenergy_metering_sumaiton_delivered",
],
DEV_SIG_ENT_MAP: {
("light", "00:11:22:33:44:55:66:77-1"): {
@ -3467,6 +3504,11 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering",
DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_smartenergy_metering",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): {
DEV_SIG_CHANNELS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_smartenergy_metering_summation_delivered",
},
},
DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
SIG_MANUFACTURER: "sengled",
@ -3487,6 +3529,7 @@ DEVICES = [
DEV_SIG_ENTITIES: [
"light.sengled_z01_a19nae26_77665544_level_light_color_on_off",
"sensor.sengled_z01_a19nae26_77665544_smartenergy_metering",
"sensor.sengled_z01_a19nae26_77665544_smartenergy_metering_summation_delivered",
],
DEV_SIG_ENT_MAP: {
("light", "00:11:22:33:44:55:66:77-1"): {
@ -3499,6 +3542,11 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering",
DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): {
DEV_SIG_CHANNELS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering_summation_delivered",
},
},
DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
SIG_MANUFACTURER: "sengled",