Add unique_id and device_info to DSMR entities (#42279)

* Add unique ids and device info

* Fix tests

* Add tests

* Apply suggestions from code review

Co-authored-by: Chris Talkington <chris@talkingtontech.com>

* Fix black

Co-authored-by: Chris Talkington <chris@talkingtontech.com>
This commit is contained in:
Rob Bierbooms 2020-10-24 13:22:23 +02:00 committed by GitHub
parent 6efb782871
commit 79fac17c28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 110 additions and 14 deletions

View file

@ -18,6 +18,9 @@ DEFAULT_RECONNECT_INTERVAL = 30
DATA_TASK = "task"
DEVICE_NAME_ENERGY = "Energy Meter"
DEVICE_NAME_GAS = "Gas Meter"
ICON_GAS = "mdi:fire"
ICON_POWER = "mdi:flash"
ICON_POWER_FAILURE = "mdi:flash-off"

View file

@ -3,6 +3,7 @@ import asyncio
from asyncio import CancelledError
from functools import partial
import logging
from typing import Dict
from dsmr_parser import obis_references as obis_ref
from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
@ -26,11 +27,15 @@ from .const import (
CONF_DSMR_VERSION,
CONF_PRECISION,
CONF_RECONNECT_INTERVAL,
CONF_SERIAL_ID,
CONF_SERIAL_ID_GAS,
DATA_TASK,
DEFAULT_DSMR_VERSION,
DEFAULT_PORT,
DEFAULT_PRECISION,
DEFAULT_RECONNECT_INTERVAL,
DEVICE_NAME_ENERGY,
DEVICE_NAME_GAS,
DOMAIN,
ICON_GAS,
ICON_POWER,
@ -106,21 +111,37 @@ async def async_setup_entry(
]
# Generate device entities
devices = [DSMREntity(name, obis, config) for name, obis in obis_mapping]
devices = [
DSMREntity(name, DEVICE_NAME_ENERGY, config[CONF_SERIAL_ID], obis, config)
for name, obis in obis_mapping
]
# Protocol version specific obis
if dsmr_version in ("4", "5"):
gas_obis = obis_ref.HOURLY_GAS_METER_READING
elif dsmr_version in ("5B",):
gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING
else:
gas_obis = obis_ref.GAS_METER_READING
if CONF_SERIAL_ID_GAS in config:
if dsmr_version in ("4", "5"):
gas_obis = obis_ref.HOURLY_GAS_METER_READING
elif dsmr_version in ("5B",):
gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING
else:
gas_obis = obis_ref.GAS_METER_READING
# Add gas meter reading and derivative for usage
devices += [
DSMREntity("Gas Consumption", gas_obis, config),
DerivativeDSMREntity("Hourly Gas Consumption", gas_obis, config),
]
# Add gas meter reading and derivative for usage
devices += [
DSMREntity(
"Gas Consumption",
DEVICE_NAME_GAS,
config[CONF_SERIAL_ID_GAS],
gas_obis,
config,
),
DerivativeDSMREntity(
"Hourly Gas Consumption",
DEVICE_NAME_GAS,
config[CONF_SERIAL_ID_GAS],
gas_obis,
config,
),
]
async_add_entities(devices)
@ -209,13 +230,17 @@ async def async_setup_entry(
class DSMREntity(Entity):
"""Entity reading values from DSMR telegram."""
def __init__(self, name, obis, config):
def __init__(self, name, device_name, device_serial, obis, config):
"""Initialize entity."""
self._name = name
self._obis = obis
self._config = config
self.telegram = {}
self._device_name = device_name
self._device_serial = device_serial
self._unique_id = f"{device_serial}_{name}".replace(" ", "_")
@callback
def update_data(self, telegram):
"""Update data."""
@ -273,6 +298,19 @@ class DSMREntity(Entity):
"""Return the unit of measurement of this entity, if any."""
return self.get_dsmr_object_attr("unit")
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id
@property
def device_info(self) -> Dict[str, any]:
"""Return the device information."""
return {
"identifiers": {(DOMAIN, self._device_serial)},
"name": self._device_name,
}
@property
def force_update(self):
"""Force update."""

View file

@ -74,6 +74,8 @@ async def test_default_setup(hass, dsmr_connection_fixture):
"dsmr_version": "2.2",
"precision": 4,
"reconnect_interval": 30,
"serial_id": "1234",
"serial_id_gas": "5678",
}
telegram = {
@ -98,6 +100,16 @@ async def test_default_setup(hass, dsmr_connection_fixture):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
registry = await hass.helpers.entity_registry.async_get_registry()
entry = registry.async_get("sensor.power_consumption")
assert entry
assert entry.unique_id == "1234_Power_Consumption"
entry = registry.async_get("sensor.gas_consumption")
assert entry
assert entry.unique_id == "5678_Gas_Consumption"
telegram_callback = connection_factory.call_args_list[0][0][2]
# make sure entities have been created and return 'unknown' state
@ -129,13 +141,42 @@ async def test_default_setup(hass, dsmr_connection_fixture):
assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS
async def test_setup_only_energy(hass, dsmr_connection_fixture):
"""Test the default setup."""
entry_data = {
"port": "/dev/ttyUSB0",
"dsmr_version": "2.2",
"precision": 4,
"reconnect_interval": 30,
"serial_id": "1234",
}
mock_entry = MockConfigEntry(
domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data
)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
registry = await hass.helpers.entity_registry.async_get_registry()
entry = registry.async_get("sensor.power_consumption")
assert entry
assert entry.unique_id == "1234_Power_Consumption"
entry = registry.async_get("sensor.gas_consumption")
assert not entry
async def test_derivative():
"""Test calculation of derivative value."""
from dsmr_parser.objects import MBusObject
config = {"platform": "dsmr"}
entity = DerivativeDSMREntity("test", "1.0.0", config)
entity = DerivativeDSMREntity("test", "test_device", "5678", "1.0.0", config)
await entity.async_update()
assert entity.state is None, "initial state not unknown"
@ -184,6 +225,8 @@ async def test_v4_meter(hass, dsmr_connection_fixture):
"dsmr_version": "4",
"precision": 4,
"reconnect_interval": 30,
"serial_id": "1234",
"serial_id_gas": "5678",
}
telegram = {
@ -239,6 +282,8 @@ async def test_v5_meter(hass, dsmr_connection_fixture):
"dsmr_version": "5",
"precision": 4,
"reconnect_interval": 30,
"serial_id": "1234",
"serial_id_gas": "5678",
}
telegram = {
@ -294,6 +339,8 @@ async def test_belgian_meter(hass, dsmr_connection_fixture):
"dsmr_version": "5B",
"precision": 4,
"reconnect_interval": 30,
"serial_id": "1234",
"serial_id_gas": "5678",
}
telegram = {
@ -346,6 +393,8 @@ async def test_belgian_meter_low(hass, dsmr_connection_fixture):
"dsmr_version": "5B",
"precision": 4,
"reconnect_interval": 30,
"serial_id": "1234",
"serial_id_gas": "5678",
}
telegram = {ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0002", "unit": ""}])}
@ -383,6 +432,8 @@ async def test_tcp(hass, dsmr_connection_fixture):
"dsmr_version": "2.2",
"precision": 4,
"reconnect_interval": 30,
"serial_id": "1234",
"serial_id_gas": "5678",
}
mock_entry = MockConfigEntry(
@ -407,6 +458,8 @@ async def test_connection_errors_retry(hass, dsmr_connection_fixture):
"dsmr_version": "2.2",
"precision": 4,
"reconnect_interval": 0,
"serial_id": "1234",
"serial_id_gas": "5678",
}
# override the mock to have it fail the first time and succeed after
@ -442,6 +495,8 @@ async def test_reconnect(hass, dsmr_connection_fixture):
"dsmr_version": "2.2",
"precision": 4,
"reconnect_interval": 0,
"serial_id": "1234",
"serial_id_gas": "5678",
}
# mock waiting coroutine while connection lasts