Add support for multiple mbus devices in dsmr (#84097)

* Add support for multiple mbus devices in dsmr

A dsmr meter can have 4 mbus devices.
Support them all and also add support for a water meter on the mbus
device.

* Apply suggestions from code review

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>

* Rewrite old gas sensor to new mbus sensor

* No force updates + fix mbus entity unique_id

* Remove old gas device

* Add additional tests

* Fix remarks from last review + move migrated 5b gas meter to new device_id

* Fix ruff error

* Last fixes

---------

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
This commit is contained in:
dupondje 2023-11-29 15:41:58 +01:00 committed by GitHub
parent 36eb858d0a
commit ba481001c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 720 additions and 146 deletions

View File

@ -29,6 +29,7 @@ DATA_TASK = "task"
DEVICE_NAME_ELECTRICITY = "Electricity Meter" DEVICE_NAME_ELECTRICITY = "Electricity Meter"
DEVICE_NAME_GAS = "Gas Meter" DEVICE_NAME_GAS = "Gas Meter"
DEVICE_NAME_WATER = "Water Meter"
DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"} DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"}

View File

@ -34,6 +34,7 @@ from homeassistant.const import (
UnitOfVolume, UnitOfVolume,
) )
from homeassistant.core import CoreState, Event, HomeAssistant, callback from homeassistant.core import CoreState, Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
@ -57,6 +58,7 @@ from .const import (
DEFAULT_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE,
DEVICE_NAME_ELECTRICITY, DEVICE_NAME_ELECTRICITY,
DEVICE_NAME_GAS, DEVICE_NAME_GAS,
DEVICE_NAME_WATER,
DOMAIN, DOMAIN,
DSMR_PROTOCOL, DSMR_PROTOCOL,
LOGGER, LOGGER,
@ -73,6 +75,7 @@ class DSMRSensorEntityDescription(SensorEntityDescription):
dsmr_versions: set[str] | None = None dsmr_versions: set[str] | None = None
is_gas: bool = False is_gas: bool = False
is_water: bool = False
obis_reference: str obis_reference: str
@ -374,28 +377,138 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
) )
def add_gas_sensor_5B(telegram: dict[str, DSMRObject]) -> DSMRSensorEntityDescription: def create_mbus_entity(
"""Return correct entity for 5B Gas meter.""" mbus: int, mtype: int, telegram: dict[str, DSMRObject]
ref = None ) -> DSMRSensorEntityDescription | None:
if obis_references.BELGIUM_MBUS1_METER_READING2 in telegram: """Create a new MBUS Entity."""
ref = obis_references.BELGIUM_MBUS1_METER_READING2 if (
elif obis_references.BELGIUM_MBUS2_METER_READING2 in telegram: mtype == 3
ref = obis_references.BELGIUM_MBUS2_METER_READING2 and (
elif obis_references.BELGIUM_MBUS3_METER_READING2 in telegram: obis_reference := getattr(
ref = obis_references.BELGIUM_MBUS3_METER_READING2 obis_references, f"BELGIUM_MBUS{mbus}_METER_READING2"
elif obis_references.BELGIUM_MBUS4_METER_READING2 in telegram: )
ref = obis_references.BELGIUM_MBUS4_METER_READING2 )
elif ref is None: in telegram
ref = obis_references.BELGIUM_MBUS1_METER_READING2 ):
return DSMRSensorEntityDescription( return DSMRSensorEntityDescription(
key="belgium_5min_gas_meter_reading", key=f"mbus{mbus}_gas_reading",
translation_key="gas_meter_reading", translation_key="gas_meter_reading",
obis_reference=ref, obis_reference=obis_reference,
dsmr_versions={"5B"}, is_gas=True,
is_gas=True, device_class=SensorDeviceClass.GAS,
device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL_INCREASING, )
) if (
mtype == 7
and (
obis_reference := getattr(
obis_references, f"BELGIUM_MBUS{mbus}_METER_READING1"
)
)
in telegram
):
return DSMRSensorEntityDescription(
key=f"mbus{mbus}_water_reading",
translation_key="water_meter_reading",
obis_reference=obis_reference,
is_water=True,
device_class=SensorDeviceClass.WATER,
state_class=SensorStateClass.TOTAL_INCREASING,
)
return None
def device_class_and_uom(
telegram: dict[str, DSMRObject],
entity_description: DSMRSensorEntityDescription,
) -> tuple[SensorDeviceClass | None, str | None]:
"""Get native unit of measurement from telegram,."""
dsmr_object = telegram[entity_description.obis_reference]
uom: str | None = getattr(dsmr_object, "unit") or None
with suppress(ValueError):
if entity_description.device_class == SensorDeviceClass.GAS and (
enery_uom := UnitOfEnergy(str(uom))
):
return (SensorDeviceClass.ENERGY, enery_uom)
if uom in UNIT_CONVERSION:
return (entity_description.device_class, UNIT_CONVERSION[uom])
return (entity_description.device_class, uom)
def rename_old_gas_to_mbus(
hass: HomeAssistant, entry: ConfigEntry, mbus_device_id: str
) -> None:
"""Rename old gas sensor to mbus variant."""
dev_reg = dr.async_get(hass)
device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, entry.entry_id)})
if device_entry_v1 is not None:
device_id = device_entry_v1.id
ent_reg = er.async_get(hass)
entries = er.async_entries_for_device(ent_reg, device_id)
for entity in entries:
if entity.unique_id.endswith("belgium_5min_gas_meter_reading"):
try:
ent_reg.async_update_entity(
entity.entity_id,
new_unique_id=mbus_device_id,
device_id=mbus_device_id,
)
except ValueError:
LOGGER.warning(
"Skip migration of %s because it already exists",
entity.entity_id,
)
else:
LOGGER.info(
"Migrated entity %s from unique id %s to %s",
entity.entity_id,
entity.unique_id,
mbus_device_id,
)
# Cleanup old device
dev_entities = er.async_entries_for_device(
ent_reg, device_id, include_disabled_entities=True
)
if not dev_entities:
dev_reg.async_remove_device(device_id)
def create_mbus_entities(
hass: HomeAssistant, telegram: dict[str, DSMRObject], entry: ConfigEntry
) -> list[DSMREntity]:
"""Create MBUS Entities."""
entities = []
for idx in range(1, 5):
if (
device_type := getattr(obis_references, f"BELGIUM_MBUS{idx}_DEVICE_TYPE")
) not in telegram:
continue
if (type_ := int(telegram[device_type].value)) not in (3, 7):
continue
if (
identifier := getattr(
obis_references,
f"BELGIUM_MBUS{idx}_EQUIPMENT_IDENTIFIER",
)
) in telegram:
serial_ = telegram[identifier].value
rename_old_gas_to_mbus(hass, entry, serial_)
else:
serial_ = ""
if description := create_mbus_entity(idx, type_, telegram):
entities.append(
DSMREntity(
description,
entry,
telegram,
*device_class_and_uom(telegram, description), # type: ignore[arg-type]
serial_,
idx,
)
)
return entities
async def async_setup_entry( async def async_setup_entry(
@ -415,25 +528,10 @@ async def async_setup_entry(
add_entities_handler() add_entities_handler()
add_entities_handler = None add_entities_handler = None
def device_class_and_uom(
telegram: dict[str, DSMRObject],
entity_description: DSMRSensorEntityDescription,
) -> tuple[SensorDeviceClass | None, str | None]:
"""Get native unit of measurement from telegram,."""
dsmr_object = telegram[entity_description.obis_reference]
uom: str | None = getattr(dsmr_object, "unit") or None
with suppress(ValueError):
if entity_description.device_class == SensorDeviceClass.GAS and (
enery_uom := UnitOfEnergy(str(uom))
):
return (SensorDeviceClass.ENERGY, enery_uom)
if uom in UNIT_CONVERSION:
return (entity_description.device_class, UNIT_CONVERSION[uom])
return (entity_description.device_class, uom)
all_sensors = SENSORS
if dsmr_version == "5B": if dsmr_version == "5B":
all_sensors += (add_gas_sensor_5B(telegram),) mbus_entities = create_mbus_entities(hass, telegram, entry)
for mbus_entity in mbus_entities:
entities.append(mbus_entity)
entities.extend( entities.extend(
[ [
@ -443,7 +541,7 @@ async def async_setup_entry(
telegram, telegram,
*device_class_and_uom(telegram, description), # type: ignore[arg-type] *device_class_and_uom(telegram, description), # type: ignore[arg-type]
) )
for description in all_sensors for description in SENSORS
if ( if (
description.dsmr_versions is None description.dsmr_versions is None
or dsmr_version in description.dsmr_versions or dsmr_version in description.dsmr_versions
@ -618,6 +716,8 @@ class DSMREntity(SensorEntity):
telegram: dict[str, DSMRObject], telegram: dict[str, DSMRObject],
device_class: SensorDeviceClass, device_class: SensorDeviceClass,
native_unit_of_measurement: str | None, native_unit_of_measurement: str | None,
serial_id: str = "",
mbus_id: int = 0,
) -> None: ) -> None:
"""Initialize entity.""" """Initialize entity."""
self.entity_description = entity_description self.entity_description = entity_description
@ -629,8 +729,15 @@ class DSMREntity(SensorEntity):
device_serial = entry.data[CONF_SERIAL_ID] device_serial = entry.data[CONF_SERIAL_ID]
device_name = DEVICE_NAME_ELECTRICITY device_name = DEVICE_NAME_ELECTRICITY
if entity_description.is_gas: if entity_description.is_gas:
device_serial = entry.data[CONF_SERIAL_ID_GAS] if serial_id:
device_serial = serial_id
else:
device_serial = entry.data[CONF_SERIAL_ID_GAS]
device_name = DEVICE_NAME_GAS device_name = DEVICE_NAME_GAS
if entity_description.is_water:
if serial_id:
device_serial = serial_id
device_name = DEVICE_NAME_WATER
if device_serial is None: if device_serial is None:
device_serial = entry.entry_id device_serial = entry.entry_id
@ -638,7 +745,13 @@ class DSMREntity(SensorEntity):
identifiers={(DOMAIN, device_serial)}, identifiers={(DOMAIN, device_serial)},
name=device_name, name=device_name,
) )
self._attr_unique_id = f"{device_serial}_{entity_description.key}" if mbus_id != 0:
if serial_id:
self._attr_unique_id = f"{device_serial}"
else:
self._attr_unique_id = f"{device_serial}_{mbus_id}"
else:
self._attr_unique_id = f"{device_serial}_{entity_description.key}"
@callback @callback
def update_data(self, telegram: dict[str, DSMRObject] | None) -> None: def update_data(self, telegram: dict[str, DSMRObject] | None) -> None:

View File

@ -147,6 +147,9 @@
}, },
"voltage_swell_l3_count": { "voltage_swell_l3_count": {
"name": "Voltage swells phase L3" "name": "Voltage swells phase L3"
},
"water_meter_reading": {
"name": "Water consumption"
} }
} }
}, },

View File

@ -0,0 +1,212 @@
"""Tests for the DSMR integration."""
import datetime
from decimal import Decimal
from homeassistant.components.dsmr.const import DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
async def test_migrate_gas_to_mbus(
hass: HomeAssistant, entity_registry: er.EntityRegistry, dsmr_connection_fixture
) -> None:
"""Test migration of unique_id."""
(connection_factory, transport, protocol) = dsmr_connection_fixture
from dsmr_parser.obis_references import (
BELGIUM_MBUS1_DEVICE_TYPE,
BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER,
BELGIUM_MBUS1_METER_READING2,
)
from dsmr_parser.objects import CosemObject, MBusObject
mock_entry = MockConfigEntry(
domain=DOMAIN,
unique_id="/dev/ttyUSB0",
data={
"port": "/dev/ttyUSB0",
"dsmr_version": "5B",
"precision": 4,
"reconnect_interval": 30,
"serial_id": "1234",
"serial_id_gas": "37464C4F32313139303333373331",
},
options={
"time_between_update": 0,
},
)
mock_entry.add_to_hass(hass)
old_unique_id = "37464C4F32313139303333373331_belgium_5min_gas_meter_reading"
device_registry = hass.helpers.device_registry.async_get(hass)
device = device_registry.async_get_or_create(
config_entry_id=mock_entry.entry_id,
identifiers={(DOMAIN, mock_entry.entry_id)},
name="Gas Meter",
)
await hass.async_block_till_done()
entity: er.RegistryEntry = entity_registry.async_get_or_create(
suggested_object_id="gas_meter_reading",
disabled_by=None,
domain=SENSOR_DOMAIN,
platform=DOMAIN,
device_id=device.id,
unique_id=old_unique_id,
config_entry=mock_entry,
)
assert entity.unique_id == old_unique_id
await hass.async_block_till_done()
telegram = {
BELGIUM_MBUS1_DEVICE_TYPE: CosemObject(
BELGIUM_MBUS1_DEVICE_TYPE, [{"value": "003", "unit": ""}]
),
BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: CosemObject(
BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER,
[{"value": "37464C4F32313139303333373331", "unit": ""}],
),
BELGIUM_MBUS1_METER_READING2: MBusObject(
BELGIUM_MBUS1_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": "m3"},
],
),
}
assert await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
telegram_callback = connection_factory.call_args_list[0][0][2]
# simulate a telegram pushed from the smartmeter and parsed by dsmr_parser
telegram_callback(telegram)
# after receiving telegram entities need to have the chance to be created
await hass.async_block_till_done()
dev_entities = er.async_entries_for_device(
entity_registry, device.id, include_disabled_entities=True
)
assert not dev_entities
assert (
entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id)
is None
)
assert (
entity_registry.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, "37464C4F32313139303333373331"
)
== "sensor.gas_meter_reading"
)
async def test_migrate_gas_to_mbus_exists(
hass: HomeAssistant, entity_registry: er.EntityRegistry, dsmr_connection_fixture
) -> None:
"""Test migration of unique_id."""
(connection_factory, transport, protocol) = dsmr_connection_fixture
from dsmr_parser.obis_references import (
BELGIUM_MBUS1_DEVICE_TYPE,
BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER,
BELGIUM_MBUS1_METER_READING2,
)
from dsmr_parser.objects import CosemObject, MBusObject
mock_entry = MockConfigEntry(
domain=DOMAIN,
unique_id="/dev/ttyUSB0",
data={
"port": "/dev/ttyUSB0",
"dsmr_version": "5B",
"precision": 4,
"reconnect_interval": 30,
"serial_id": "1234",
"serial_id_gas": "37464C4F32313139303333373331",
},
options={
"time_between_update": 0,
},
)
mock_entry.add_to_hass(hass)
old_unique_id = "37464C4F32313139303333373331_belgium_5min_gas_meter_reading"
device_registry = hass.helpers.device_registry.async_get(hass)
device = device_registry.async_get_or_create(
config_entry_id=mock_entry.entry_id,
identifiers={(DOMAIN, mock_entry.entry_id)},
name="Gas Meter",
)
await hass.async_block_till_done()
entity: er.RegistryEntry = entity_registry.async_get_or_create(
suggested_object_id="gas_meter_reading",
disabled_by=None,
domain=SENSOR_DOMAIN,
platform=DOMAIN,
device_id=device.id,
unique_id=old_unique_id,
config_entry=mock_entry,
)
assert entity.unique_id == old_unique_id
device2 = device_registry.async_get_or_create(
config_entry_id=mock_entry.entry_id,
identifiers={(DOMAIN, "37464C4F32313139303333373331")},
name="Gas Meter",
)
await hass.async_block_till_done()
entity_registry.async_get_or_create(
suggested_object_id="gas_meter_reading_alt",
disabled_by=None,
domain=SENSOR_DOMAIN,
platform=DOMAIN,
device_id=device2.id,
unique_id="37464C4F32313139303333373331",
config_entry=mock_entry,
)
await hass.async_block_till_done()
telegram = {
BELGIUM_MBUS1_DEVICE_TYPE: CosemObject(
BELGIUM_MBUS1_DEVICE_TYPE, [{"value": "003", "unit": ""}]
),
BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: CosemObject(
BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER,
[{"value": "37464C4F32313139303333373331", "unit": ""}],
),
BELGIUM_MBUS1_METER_READING2: MBusObject(
BELGIUM_MBUS1_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": "m3"},
],
),
}
assert await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
telegram_callback = connection_factory.call_args_list[0][0][2]
# simulate a telegram pushed from the smartmeter and parsed by dsmr_parser
telegram_callback(telegram)
# after receiving telegram entities need to have the chance to be created
await hass.async_block_till_done()
assert (
entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id)
== "sensor.gas_meter_reading"
)

View File

@ -8,21 +8,8 @@ import asyncio
import datetime import datetime
from decimal import Decimal from decimal import Decimal
from itertools import chain, repeat from itertools import chain, repeat
from typing import Literal
from unittest.mock import DEFAULT, MagicMock from unittest.mock import DEFAULT, MagicMock
from dsmr_parser.obis_references import (
BELGIUM_MBUS1_METER_READING1,
BELGIUM_MBUS1_METER_READING2,
BELGIUM_MBUS2_METER_READING1,
BELGIUM_MBUS2_METER_READING2,
BELGIUM_MBUS3_METER_READING1,
BELGIUM_MBUS3_METER_READING2,
BELGIUM_MBUS4_METER_READING1,
BELGIUM_MBUS4_METER_READING2,
)
import pytest
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
ATTR_OPTIONS, ATTR_OPTIONS,
@ -145,8 +132,8 @@ async def test_default_setup(
# simulate a telegram pushed from the smartmeter and parsed by dsmr_parser # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser
telegram_callback(telegram) telegram_callback(telegram)
# after receiving telegram entities need to have the chance to update # after receiving telegram entities need to have the chance to be created
await asyncio.sleep(0) await hass.async_block_till_done()
# ensure entities have new state value after incoming telegram # ensure entities have new state value after incoming telegram
power_consumption = hass.states.get("sensor.electricity_meter_power_consumption") power_consumption = hass.states.get("sensor.electricity_meter_power_consumption")
@ -495,10 +482,18 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No
from dsmr_parser.obis_references import ( from dsmr_parser.obis_references import (
BELGIUM_CURRENT_AVERAGE_DEMAND, BELGIUM_CURRENT_AVERAGE_DEMAND,
BELGIUM_MAXIMUM_DEMAND_MONTH, BELGIUM_MAXIMUM_DEMAND_MONTH,
BELGIUM_MBUS1_DEVICE_TYPE,
BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER,
BELGIUM_MBUS1_METER_READING2, BELGIUM_MBUS1_METER_READING2,
BELGIUM_MBUS2_METER_READING2, BELGIUM_MBUS2_DEVICE_TYPE,
BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER,
BELGIUM_MBUS2_METER_READING1,
BELGIUM_MBUS3_DEVICE_TYPE,
BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER,
BELGIUM_MBUS3_METER_READING2, BELGIUM_MBUS3_METER_READING2,
BELGIUM_MBUS4_METER_READING2, BELGIUM_MBUS4_DEVICE_TYPE,
BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER,
BELGIUM_MBUS4_METER_READING1,
ELECTRICITY_ACTIVE_TARIFF, ELECTRICITY_ACTIVE_TARIFF,
) )
from dsmr_parser.objects import CosemObject, MBusObject from dsmr_parser.objects import CosemObject, MBusObject
@ -509,41 +504,13 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No
"precision": 4, "precision": 4,
"reconnect_interval": 30, "reconnect_interval": 30,
"serial_id": "1234", "serial_id": "1234",
"serial_id_gas": "5678", "serial_id_gas": None,
} }
entry_options = { entry_options = {
"time_between_update": 0, "time_between_update": 0,
} }
telegram = { telegram = {
BELGIUM_MBUS1_METER_READING2: MBusObject(
BELGIUM_MBUS1_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": "m3"},
],
),
BELGIUM_MBUS2_METER_READING2: MBusObject(
BELGIUM_MBUS2_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642214)},
{"value": Decimal(745.696), "unit": "m3"},
],
),
BELGIUM_MBUS3_METER_READING2: MBusObject(
BELGIUM_MBUS3_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642215)},
{"value": Decimal(745.697), "unit": "m3"},
],
),
BELGIUM_MBUS4_METER_READING2: MBusObject(
BELGIUM_MBUS4_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642216)},
{"value": Decimal(745.698), "unit": "m3"},
],
),
BELGIUM_CURRENT_AVERAGE_DEMAND: CosemObject( BELGIUM_CURRENT_AVERAGE_DEMAND: CosemObject(
BELGIUM_CURRENT_AVERAGE_DEMAND, BELGIUM_CURRENT_AVERAGE_DEMAND,
[{"value": Decimal(1.75), "unit": "kW"}], [{"value": Decimal(1.75), "unit": "kW"}],
@ -555,6 +522,62 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No
{"value": Decimal(4.11), "unit": "kW"}, {"value": Decimal(4.11), "unit": "kW"},
], ],
), ),
BELGIUM_MBUS1_DEVICE_TYPE: CosemObject(
BELGIUM_MBUS1_DEVICE_TYPE, [{"value": "003", "unit": ""}]
),
BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: CosemObject(
BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER,
[{"value": "37464C4F32313139303333373331", "unit": ""}],
),
BELGIUM_MBUS1_METER_READING2: MBusObject(
BELGIUM_MBUS1_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": "m3"},
],
),
BELGIUM_MBUS2_DEVICE_TYPE: CosemObject(
BELGIUM_MBUS2_DEVICE_TYPE, [{"value": "007", "unit": ""}]
),
BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: CosemObject(
BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER,
[{"value": "37464C4F32313139303333373332", "unit": ""}],
),
BELGIUM_MBUS2_METER_READING1: MBusObject(
BELGIUM_MBUS2_METER_READING1,
[
{"value": datetime.datetime.fromtimestamp(1551642214)},
{"value": Decimal(678.695), "unit": "m3"},
],
),
BELGIUM_MBUS3_DEVICE_TYPE: CosemObject(
BELGIUM_MBUS3_DEVICE_TYPE, [{"value": "003", "unit": ""}]
),
BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER: CosemObject(
BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER,
[{"value": "37464C4F32313139303333373333", "unit": ""}],
),
BELGIUM_MBUS3_METER_READING2: MBusObject(
BELGIUM_MBUS3_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642215)},
{"value": Decimal(12.12), "unit": "m3"},
],
),
BELGIUM_MBUS4_DEVICE_TYPE: CosemObject(
BELGIUM_MBUS4_DEVICE_TYPE, [{"value": "007", "unit": ""}]
),
BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER: CosemObject(
BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER,
[{"value": "37464C4F32313139303333373334", "unit": ""}],
),
BELGIUM_MBUS4_METER_READING1: MBusObject(
BELGIUM_MBUS4_METER_READING1,
[
{"value": datetime.datetime.fromtimestamp(1551642216)},
{"value": Decimal(13.13), "unit": "m3"},
],
),
ELECTRICITY_ACTIVE_TARIFF: CosemObject( ELECTRICITY_ACTIVE_TARIFF: CosemObject(
ELECTRICITY_ACTIVE_TARIFF, [{"value": "0001", "unit": ""}] ELECTRICITY_ACTIVE_TARIFF, [{"value": "0001", "unit": ""}]
), ),
@ -600,7 +623,7 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No
assert max_demand.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.KILO_WATT assert max_demand.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.KILO_WATT
assert max_demand.attributes.get(ATTR_STATE_CLASS) is None assert max_demand.attributes.get(ATTR_STATE_CLASS) is None
# check if gas consumption is parsed correctly # check if gas consumption mbus1 is parsed correctly
gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption")
assert gas_consumption.state == "745.695" assert gas_consumption.state == "745.695"
assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS
@ -613,48 +636,69 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No
== UnitOfVolume.CUBIC_METERS == UnitOfVolume.CUBIC_METERS
) )
# check if water usage mbus2 is parsed correctly
water_consumption = hass.states.get("sensor.water_meter_water_consumption")
assert water_consumption.state == "678.695"
assert (
water_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER
)
assert (
water_consumption.attributes.get(ATTR_STATE_CLASS)
== SensorStateClass.TOTAL_INCREASING
)
assert (
water_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfVolume.CUBIC_METERS
)
@pytest.mark.parametrize( # check if gas consumption mbus1 is parsed correctly
("key1", "key2", "key3", "gas_value"), gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption_2")
[ assert gas_consumption.state == "12.12"
( assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS
BELGIUM_MBUS1_METER_READING1, assert (
BELGIUM_MBUS2_METER_READING2, gas_consumption.attributes.get(ATTR_STATE_CLASS)
BELGIUM_MBUS3_METER_READING1, == SensorStateClass.TOTAL_INCREASING
"745.696", )
), assert (
( gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
BELGIUM_MBUS1_METER_READING2, == UnitOfVolume.CUBIC_METERS
BELGIUM_MBUS2_METER_READING1, )
BELGIUM_MBUS3_METER_READING2,
"745.695", # check if water usage mbus2 is parsed correctly
), water_consumption = hass.states.get("sensor.water_meter_water_consumption_2")
( assert water_consumption.state == "13.13"
BELGIUM_MBUS4_METER_READING2, assert (
BELGIUM_MBUS2_METER_READING1, water_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER
BELGIUM_MBUS3_METER_READING1, )
"745.695", assert (
), water_consumption.attributes.get(ATTR_STATE_CLASS)
( == SensorStateClass.TOTAL_INCREASING
BELGIUM_MBUS4_METER_READING1, )
BELGIUM_MBUS2_METER_READING1, assert (
BELGIUM_MBUS3_METER_READING2, water_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
"745.697", == UnitOfVolume.CUBIC_METERS
), )
],
)
async def test_belgian_meter_alt( async def test_belgian_meter_alt(hass: HomeAssistant, dsmr_connection_fixture) -> None:
hass: HomeAssistant,
dsmr_connection_fixture,
key1: Literal,
key2: Literal,
key3: Literal,
gas_value: str,
) -> None:
"""Test if Belgian meter is correctly parsed.""" """Test if Belgian meter is correctly parsed."""
(connection_factory, transport, protocol) = dsmr_connection_fixture (connection_factory, transport, protocol) = dsmr_connection_fixture
from dsmr_parser.objects import MBusObject from dsmr_parser.obis_references import (
BELGIUM_MBUS1_DEVICE_TYPE,
BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER,
BELGIUM_MBUS1_METER_READING1,
BELGIUM_MBUS2_DEVICE_TYPE,
BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER,
BELGIUM_MBUS2_METER_READING2,
BELGIUM_MBUS3_DEVICE_TYPE,
BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER,
BELGIUM_MBUS3_METER_READING1,
BELGIUM_MBUS4_DEVICE_TYPE,
BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER,
BELGIUM_MBUS4_METER_READING2,
)
from dsmr_parser.objects import CosemObject, MBusObject
entry_data = { entry_data = {
"port": "/dev/ttyUSB0", "port": "/dev/ttyUSB0",
@ -662,32 +706,67 @@ async def test_belgian_meter_alt(
"precision": 4, "precision": 4,
"reconnect_interval": 30, "reconnect_interval": 30,
"serial_id": "1234", "serial_id": "1234",
"serial_id_gas": "5678", "serial_id_gas": None,
} }
entry_options = { entry_options = {
"time_between_update": 0, "time_between_update": 0,
} }
telegram = { telegram = {
key1: MBusObject( BELGIUM_MBUS1_DEVICE_TYPE: CosemObject(
key1, BELGIUM_MBUS1_DEVICE_TYPE, [{"value": "007", "unit": ""}]
[
{"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": "m3"},
],
), ),
key2: MBusObject( BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: CosemObject(
key2, BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER,
[ [{"value": "37464C4F32313139303333373331", "unit": ""}],
{"value": datetime.datetime.fromtimestamp(1551642214)},
{"value": Decimal(745.696), "unit": "m3"},
],
), ),
key3: MBusObject( BELGIUM_MBUS1_METER_READING1: MBusObject(
key3, BELGIUM_MBUS1_METER_READING1,
[ [
{"value": datetime.datetime.fromtimestamp(1551642215)}, {"value": datetime.datetime.fromtimestamp(1551642215)},
{"value": Decimal(745.697), "unit": "m3"}, {"value": Decimal(123.456), "unit": "m3"},
],
),
BELGIUM_MBUS2_DEVICE_TYPE: CosemObject(
BELGIUM_MBUS2_DEVICE_TYPE, [{"value": "003", "unit": ""}]
),
BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: CosemObject(
BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER,
[{"value": "37464C4F32313139303333373332", "unit": ""}],
),
BELGIUM_MBUS2_METER_READING2: MBusObject(
BELGIUM_MBUS2_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642216)},
{"value": Decimal(678.901), "unit": "m3"},
],
),
BELGIUM_MBUS3_DEVICE_TYPE: CosemObject(
BELGIUM_MBUS3_DEVICE_TYPE, [{"value": "007", "unit": ""}]
),
BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER: CosemObject(
BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER,
[{"value": "37464C4F32313139303333373333", "unit": ""}],
),
BELGIUM_MBUS3_METER_READING1: MBusObject(
BELGIUM_MBUS3_METER_READING1,
[
{"value": datetime.datetime.fromtimestamp(1551642217)},
{"value": Decimal(12.12), "unit": "m3"},
],
),
BELGIUM_MBUS4_DEVICE_TYPE: CosemObject(
BELGIUM_MBUS4_DEVICE_TYPE, [{"value": "003", "unit": ""}]
),
BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER: CosemObject(
BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER,
[{"value": "37464C4F32313139303333373334", "unit": ""}],
),
BELGIUM_MBUS4_METER_READING2: MBusObject(
BELGIUM_MBUS4_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642218)},
{"value": Decimal(13.13), "unit": "m3"},
], ],
), ),
} }
@ -709,9 +788,24 @@ async def test_belgian_meter_alt(
# after receiving telegram entities need to have the chance to be created # after receiving telegram entities need to have the chance to be created
await hass.async_block_till_done() await hass.async_block_till_done()
# check if gas consumption is parsed correctly # check if water usage mbus1 is parsed correctly
water_consumption = hass.states.get("sensor.water_meter_water_consumption")
assert water_consumption.state == "123.456"
assert (
water_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER
)
assert (
water_consumption.attributes.get(ATTR_STATE_CLASS)
== SensorStateClass.TOTAL_INCREASING
)
assert (
water_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfVolume.CUBIC_METERS
)
# check if gas consumption mbus2 is parsed correctly
gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption")
assert gas_consumption.state == gas_value assert gas_consumption.state == "678.901"
assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS
assert ( assert (
gas_consumption.attributes.get(ATTR_STATE_CLASS) gas_consumption.attributes.get(ATTR_STATE_CLASS)
@ -722,6 +816,157 @@ async def test_belgian_meter_alt(
== UnitOfVolume.CUBIC_METERS == UnitOfVolume.CUBIC_METERS
) )
# check if water usage mbus3 is parsed correctly
water_consumption = hass.states.get("sensor.water_meter_water_consumption_2")
assert water_consumption.state == "12.12"
assert (
water_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER
)
assert (
water_consumption.attributes.get(ATTR_STATE_CLASS)
== SensorStateClass.TOTAL_INCREASING
)
assert (
water_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfVolume.CUBIC_METERS
)
# check if gas consumption mbus4 is parsed correctly
gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption_2")
assert gas_consumption.state == "13.13"
assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS
assert (
gas_consumption.attributes.get(ATTR_STATE_CLASS)
== SensorStateClass.TOTAL_INCREASING
)
assert (
gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfVolume.CUBIC_METERS
)
async def test_belgian_meter_mbus(hass: HomeAssistant, dsmr_connection_fixture) -> None:
"""Test if Belgian meter is correctly parsed."""
(connection_factory, transport, protocol) = dsmr_connection_fixture
from dsmr_parser.obis_references import (
BELGIUM_MBUS1_DEVICE_TYPE,
BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER,
BELGIUM_MBUS2_DEVICE_TYPE,
BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER,
BELGIUM_MBUS3_DEVICE_TYPE,
BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER,
BELGIUM_MBUS3_METER_READING2,
BELGIUM_MBUS4_DEVICE_TYPE,
BELGIUM_MBUS4_METER_READING1,
ELECTRICITY_ACTIVE_TARIFF,
)
from dsmr_parser.objects import CosemObject, MBusObject
entry_data = {
"port": "/dev/ttyUSB0",
"dsmr_version": "5B",
"precision": 4,
"reconnect_interval": 30,
"serial_id": "1234",
"serial_id_gas": None,
}
entry_options = {
"time_between_update": 0,
}
telegram = {
ELECTRICITY_ACTIVE_TARIFF: CosemObject(
ELECTRICITY_ACTIVE_TARIFF, [{"value": "0003", "unit": ""}]
),
BELGIUM_MBUS1_DEVICE_TYPE: CosemObject(
BELGIUM_MBUS1_DEVICE_TYPE, [{"value": "006", "unit": ""}]
),
BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: CosemObject(
BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER,
[{"value": "37464C4F32313139303333373331", "unit": ""}],
),
BELGIUM_MBUS2_DEVICE_TYPE: CosemObject(
BELGIUM_MBUS2_DEVICE_TYPE, [{"value": "003", "unit": ""}]
),
BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: CosemObject(
BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER,
[{"value": "37464C4F32313139303333373332", "unit": ""}],
),
BELGIUM_MBUS3_DEVICE_TYPE: CosemObject(
BELGIUM_MBUS3_DEVICE_TYPE, [{"value": "007", "unit": ""}]
),
BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER: CosemObject(
BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER,
[{"value": "37464C4F32313139303333373333", "unit": ""}],
),
BELGIUM_MBUS3_METER_READING2: MBusObject(
BELGIUM_MBUS3_METER_READING2,
[
{"value": datetime.datetime.fromtimestamp(1551642217)},
{"value": Decimal(12.12), "unit": "m3"},
],
),
BELGIUM_MBUS4_DEVICE_TYPE: CosemObject(
BELGIUM_MBUS4_DEVICE_TYPE, [{"value": "007", "unit": ""}]
),
BELGIUM_MBUS4_METER_READING1: MBusObject(
BELGIUM_MBUS4_METER_READING1,
[
{"value": datetime.datetime.fromtimestamp(1551642218)},
{"value": Decimal(13.13), "unit": "m3"},
],
),
}
mock_entry = MockConfigEntry(
domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options
)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
telegram_callback = connection_factory.call_args_list[0][0][2]
# simulate a telegram pushed from the smartmeter and parsed by dsmr_parser
telegram_callback(telegram)
# after receiving telegram entities need to have the chance to be created
await hass.async_block_till_done()
# tariff should be translated in human readable and have no unit
active_tariff = hass.states.get("sensor.electricity_meter_active_tariff")
assert active_tariff.state == "unknown"
# check if gas consumption mbus2 is parsed correctly
gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption")
assert gas_consumption is None
# check if water usage mbus3 is parsed correctly
water_consumption = hass.states.get("sensor.water_meter_water_consumption_2")
assert water_consumption is None
# check if gas consumption mbus4 is parsed correctly
gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption_2")
assert gas_consumption is None
# check if gas consumption mbus4 is parsed correctly
water_consumption = hass.states.get("sensor.water_meter_water_consumption")
assert water_consumption.state == "13.13"
assert (
water_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER
)
assert (
water_consumption.attributes.get(ATTR_STATE_CLASS)
== SensorStateClass.TOTAL_INCREASING
)
assert (
water_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfVolume.CUBIC_METERS
)
async def test_belgian_meter_low(hass: HomeAssistant, dsmr_connection_fixture) -> None: async def test_belgian_meter_low(hass: HomeAssistant, dsmr_connection_fixture) -> None:
"""Test if Belgian meter is correctly parsed.""" """Test if Belgian meter is correctly parsed."""