From 79fac17c28f7d35431d6629747fd695cea6b5330 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Sat, 24 Oct 2020 13:22:23 +0200 Subject: [PATCH] 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 * Fix black Co-authored-by: Chris Talkington --- homeassistant/components/dsmr/const.py | 3 ++ homeassistant/components/dsmr/sensor.py | 64 ++++++++++++++++++++----- tests/components/dsmr/test_sensor.py | 57 +++++++++++++++++++++- 3 files changed, 110 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index ed5f8bf0ed7..edb138a60ff 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -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" diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 411088c5091..3f642ac92d9 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -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.""" diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 49e9feb80f6..7b1765ad27a 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -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