diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 7f5813cda7f..4c6cb31ca4d 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -26,6 +26,7 @@ DEFAULT_TIME_BETWEEN_UPDATE = 30 DEVICE_NAME_ELECTRICITY = "Electricity Meter" DEVICE_NAME_GAS = "Gas Meter" DEVICE_NAME_WATER = "Water Meter" +DEVICE_NAME_HEAT = "Heat Meter" DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"} diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 77c40c5c292..b76736a1101 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -57,6 +57,7 @@ from .const import ( DEFAULT_TIME_BETWEEN_UPDATE, DEVICE_NAME_ELECTRICITY, DEVICE_NAME_GAS, + DEVICE_NAME_HEAT, DEVICE_NAME_WATER, DOMAIN, DSMR_PROTOCOL, @@ -75,6 +76,7 @@ class DSMRSensorEntityDescription(SensorEntityDescription): dsmr_versions: set[str] | None = None is_gas: bool = False is_water: bool = False + is_heat: bool = False obis_reference: str @@ -82,6 +84,7 @@ class MbusDeviceType(IntEnum): """Types of mbus devices (13757-3:2013).""" GAS = 3 + HEAT = 4 WATER = 7 @@ -396,6 +399,16 @@ SENSORS_MBUS_DEVICE_TYPE: dict[int, tuple[DSMRSensorEntityDescription, ...]] = { state_class=SensorStateClass.TOTAL_INCREASING, ), ), + MbusDeviceType.HEAT: ( + DSMRSensorEntityDescription( + key="heat_reading", + translation_key="heat_meter_reading", + obis_reference="MBUS_METER_READING", + is_heat=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ), MbusDeviceType.WATER: ( DSMRSensorEntityDescription( key="water_reading", @@ -490,6 +503,10 @@ def create_mbus_entities( continue type_ = int(device_type.value) + if type_ not in SENSORS_MBUS_DEVICE_TYPE: + LOGGER.warning("Unsupported MBUS_DEVICE_TYPE (%d)", type_) + continue + if identifier := getattr(device, "MBUS_EQUIPMENT_IDENTIFIER", None): serial_ = identifier.value rename_old_gas_to_mbus(hass, entry, serial_) @@ -554,7 +571,10 @@ async def async_setup_entry( ) for description in SENSORS if is_supported_description(telegram, description, dsmr_version) - and (not description.is_gas or CONF_SERIAL_ID_GAS in entry.data) + and ( + (not description.is_gas and not description.is_heat) + or CONF_SERIAL_ID_GAS in entry.data + ) ] ) async_add_entities(entities) @@ -743,6 +763,10 @@ class DSMREntity(SensorEntity): if serial_id: device_serial = serial_id device_name = DEVICE_NAME_WATER + if entity_description.is_heat: + if serial_id: + device_serial = serial_id + device_name = DEVICE_NAME_HEAT if device_serial is None: device_serial = entry.entry_id diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index c2c6d48b007..4a2951f4ed8 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -1521,6 +1521,74 @@ async def test_gas_meter_providing_energy_reading( ) +async def test_heat_meter_mbus( + hass: HomeAssistant, dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock] +) -> None: + """Test if heat meter reading is correctly parsed.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5", + "serial_id": "1234", + "serial_id_gas": None, + } + entry_options = { + "time_between_update": 0, + } + + telegram = Telegram() + telegram.add( + MBUS_DEVICE_TYPE, + CosemObject((0, 1), [{"value": "004", "unit": ""}]), + "MBUS_DEVICE_TYPE", + ) + telegram.add( + MBUS_METER_READING, + MBusObject( + (0, 1), + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": "GJ"}, + ], + ), + "MBUS_METER_READING", + ) + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options + ) + + hass.loop.set_debug(True) + 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() + + # check if gas consumption is parsed correctly + heat_consumption = hass.states.get("sensor.heat_meter_energy") + assert heat_consumption.state == "745.695" + assert ( + heat_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + ) + assert ( + heat_consumption.attributes.get("unit_of_measurement") + == UnitOfEnergy.GIGA_JOULE + ) + assert ( + heat_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) + + def test_all_obis_references_exists() -> None: """Verify that all attributes exist by name in database.""" for sensor in SENSORS: