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:
parent
6efb782871
commit
79fac17c28
3 changed files with 110 additions and 14 deletions
|
@ -18,6 +18,9 @@ DEFAULT_RECONNECT_INTERVAL = 30
|
||||||
|
|
||||||
DATA_TASK = "task"
|
DATA_TASK = "task"
|
||||||
|
|
||||||
|
DEVICE_NAME_ENERGY = "Energy Meter"
|
||||||
|
DEVICE_NAME_GAS = "Gas Meter"
|
||||||
|
|
||||||
ICON_GAS = "mdi:fire"
|
ICON_GAS = "mdi:fire"
|
||||||
ICON_POWER = "mdi:flash"
|
ICON_POWER = "mdi:flash"
|
||||||
ICON_POWER_FAILURE = "mdi:flash-off"
|
ICON_POWER_FAILURE = "mdi:flash-off"
|
||||||
|
|
|
@ -3,6 +3,7 @@ import asyncio
|
||||||
from asyncio import CancelledError
|
from asyncio import CancelledError
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from dsmr_parser import obis_references as obis_ref
|
from dsmr_parser import obis_references as obis_ref
|
||||||
from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
|
from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
|
||||||
|
@ -26,11 +27,15 @@ from .const import (
|
||||||
CONF_DSMR_VERSION,
|
CONF_DSMR_VERSION,
|
||||||
CONF_PRECISION,
|
CONF_PRECISION,
|
||||||
CONF_RECONNECT_INTERVAL,
|
CONF_RECONNECT_INTERVAL,
|
||||||
|
CONF_SERIAL_ID,
|
||||||
|
CONF_SERIAL_ID_GAS,
|
||||||
DATA_TASK,
|
DATA_TASK,
|
||||||
DEFAULT_DSMR_VERSION,
|
DEFAULT_DSMR_VERSION,
|
||||||
DEFAULT_PORT,
|
DEFAULT_PORT,
|
||||||
DEFAULT_PRECISION,
|
DEFAULT_PRECISION,
|
||||||
DEFAULT_RECONNECT_INTERVAL,
|
DEFAULT_RECONNECT_INTERVAL,
|
||||||
|
DEVICE_NAME_ENERGY,
|
||||||
|
DEVICE_NAME_GAS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
ICON_GAS,
|
ICON_GAS,
|
||||||
ICON_POWER,
|
ICON_POWER,
|
||||||
|
@ -106,21 +111,37 @@ async def async_setup_entry(
|
||||||
]
|
]
|
||||||
|
|
||||||
# Generate device entities
|
# 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
|
# Protocol version specific obis
|
||||||
if dsmr_version in ("4", "5"):
|
if CONF_SERIAL_ID_GAS in config:
|
||||||
gas_obis = obis_ref.HOURLY_GAS_METER_READING
|
if dsmr_version in ("4", "5"):
|
||||||
elif dsmr_version in ("5B",):
|
gas_obis = obis_ref.HOURLY_GAS_METER_READING
|
||||||
gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING
|
elif dsmr_version in ("5B",):
|
||||||
else:
|
gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING
|
||||||
gas_obis = obis_ref.GAS_METER_READING
|
else:
|
||||||
|
gas_obis = obis_ref.GAS_METER_READING
|
||||||
|
|
||||||
# Add gas meter reading and derivative for usage
|
# Add gas meter reading and derivative for usage
|
||||||
devices += [
|
devices += [
|
||||||
DSMREntity("Gas Consumption", gas_obis, config),
|
DSMREntity(
|
||||||
DerivativeDSMREntity("Hourly Gas Consumption", gas_obis, config),
|
"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)
|
async_add_entities(devices)
|
||||||
|
|
||||||
|
@ -209,13 +230,17 @@ async def async_setup_entry(
|
||||||
class DSMREntity(Entity):
|
class DSMREntity(Entity):
|
||||||
"""Entity reading values from DSMR telegram."""
|
"""Entity reading values from DSMR telegram."""
|
||||||
|
|
||||||
def __init__(self, name, obis, config):
|
def __init__(self, name, device_name, device_serial, obis, config):
|
||||||
"""Initialize entity."""
|
"""Initialize entity."""
|
||||||
self._name = name
|
self._name = name
|
||||||
self._obis = obis
|
self._obis = obis
|
||||||
self._config = config
|
self._config = config
|
||||||
self.telegram = {}
|
self.telegram = {}
|
||||||
|
|
||||||
|
self._device_name = device_name
|
||||||
|
self._device_serial = device_serial
|
||||||
|
self._unique_id = f"{device_serial}_{name}".replace(" ", "_")
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def update_data(self, telegram):
|
def update_data(self, telegram):
|
||||||
"""Update data."""
|
"""Update data."""
|
||||||
|
@ -273,6 +298,19 @@ class DSMREntity(Entity):
|
||||||
"""Return the unit of measurement of this entity, if any."""
|
"""Return the unit of measurement of this entity, if any."""
|
||||||
return self.get_dsmr_object_attr("unit")
|
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
|
@property
|
||||||
def force_update(self):
|
def force_update(self):
|
||||||
"""Force update."""
|
"""Force update."""
|
||||||
|
|
|
@ -74,6 +74,8 @@ async def test_default_setup(hass, dsmr_connection_fixture):
|
||||||
"dsmr_version": "2.2",
|
"dsmr_version": "2.2",
|
||||||
"precision": 4,
|
"precision": 4,
|
||||||
"reconnect_interval": 30,
|
"reconnect_interval": 30,
|
||||||
|
"serial_id": "1234",
|
||||||
|
"serial_id_gas": "5678",
|
||||||
}
|
}
|
||||||
|
|
||||||
telegram = {
|
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.config_entries.async_setup(mock_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
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]
|
telegram_callback = connection_factory.call_args_list[0][0][2]
|
||||||
|
|
||||||
# make sure entities have been created and return 'unknown' state
|
# 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
|
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():
|
async def test_derivative():
|
||||||
"""Test calculation of derivative value."""
|
"""Test calculation of derivative value."""
|
||||||
from dsmr_parser.objects import MBusObject
|
from dsmr_parser.objects import MBusObject
|
||||||
|
|
||||||
config = {"platform": "dsmr"}
|
config = {"platform": "dsmr"}
|
||||||
|
|
||||||
entity = DerivativeDSMREntity("test", "1.0.0", config)
|
entity = DerivativeDSMREntity("test", "test_device", "5678", "1.0.0", config)
|
||||||
await entity.async_update()
|
await entity.async_update()
|
||||||
|
|
||||||
assert entity.state is None, "initial state not unknown"
|
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",
|
"dsmr_version": "4",
|
||||||
"precision": 4,
|
"precision": 4,
|
||||||
"reconnect_interval": 30,
|
"reconnect_interval": 30,
|
||||||
|
"serial_id": "1234",
|
||||||
|
"serial_id_gas": "5678",
|
||||||
}
|
}
|
||||||
|
|
||||||
telegram = {
|
telegram = {
|
||||||
|
@ -239,6 +282,8 @@ async def test_v5_meter(hass, dsmr_connection_fixture):
|
||||||
"dsmr_version": "5",
|
"dsmr_version": "5",
|
||||||
"precision": 4,
|
"precision": 4,
|
||||||
"reconnect_interval": 30,
|
"reconnect_interval": 30,
|
||||||
|
"serial_id": "1234",
|
||||||
|
"serial_id_gas": "5678",
|
||||||
}
|
}
|
||||||
|
|
||||||
telegram = {
|
telegram = {
|
||||||
|
@ -294,6 +339,8 @@ async def test_belgian_meter(hass, dsmr_connection_fixture):
|
||||||
"dsmr_version": "5B",
|
"dsmr_version": "5B",
|
||||||
"precision": 4,
|
"precision": 4,
|
||||||
"reconnect_interval": 30,
|
"reconnect_interval": 30,
|
||||||
|
"serial_id": "1234",
|
||||||
|
"serial_id_gas": "5678",
|
||||||
}
|
}
|
||||||
|
|
||||||
telegram = {
|
telegram = {
|
||||||
|
@ -346,6 +393,8 @@ async def test_belgian_meter_low(hass, dsmr_connection_fixture):
|
||||||
"dsmr_version": "5B",
|
"dsmr_version": "5B",
|
||||||
"precision": 4,
|
"precision": 4,
|
||||||
"reconnect_interval": 30,
|
"reconnect_interval": 30,
|
||||||
|
"serial_id": "1234",
|
||||||
|
"serial_id_gas": "5678",
|
||||||
}
|
}
|
||||||
|
|
||||||
telegram = {ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0002", "unit": ""}])}
|
telegram = {ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0002", "unit": ""}])}
|
||||||
|
@ -383,6 +432,8 @@ async def test_tcp(hass, dsmr_connection_fixture):
|
||||||
"dsmr_version": "2.2",
|
"dsmr_version": "2.2",
|
||||||
"precision": 4,
|
"precision": 4,
|
||||||
"reconnect_interval": 30,
|
"reconnect_interval": 30,
|
||||||
|
"serial_id": "1234",
|
||||||
|
"serial_id_gas": "5678",
|
||||||
}
|
}
|
||||||
|
|
||||||
mock_entry = MockConfigEntry(
|
mock_entry = MockConfigEntry(
|
||||||
|
@ -407,6 +458,8 @@ async def test_connection_errors_retry(hass, dsmr_connection_fixture):
|
||||||
"dsmr_version": "2.2",
|
"dsmr_version": "2.2",
|
||||||
"precision": 4,
|
"precision": 4,
|
||||||
"reconnect_interval": 0,
|
"reconnect_interval": 0,
|
||||||
|
"serial_id": "1234",
|
||||||
|
"serial_id_gas": "5678",
|
||||||
}
|
}
|
||||||
|
|
||||||
# override the mock to have it fail the first time and succeed after
|
# 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",
|
"dsmr_version": "2.2",
|
||||||
"precision": 4,
|
"precision": 4,
|
||||||
"reconnect_interval": 0,
|
"reconnect_interval": 0,
|
||||||
|
"serial_id": "1234",
|
||||||
|
"serial_id_gas": "5678",
|
||||||
}
|
}
|
||||||
|
|
||||||
# mock waiting coroutine while connection lasts
|
# mock waiting coroutine while connection lasts
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue