Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for Dutch Smart Meter (also known as Smartmeter or P1 port)."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from asyncio import CancelledError
7 from collections.abc import Callable, Generator
8 from contextlib import suppress
9 from dataclasses import dataclass
10 from datetime import timedelta
11 from enum import IntEnum
12 from functools import partial
13 
14 from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader
15 from dsmr_parser.clients.rfxtrx_protocol import (
16  create_rfxtrx_dsmr_reader,
17  create_rfxtrx_tcp_dsmr_reader,
18 )
19 from dsmr_parser.objects import DSMRObject, MbusDevice, Telegram
20 import serial
21 
23  SensorDeviceClass,
24  SensorEntity,
25  SensorEntityDescription,
26  SensorStateClass,
27 )
28 from homeassistant.config_entries import ConfigEntry
29 from homeassistant.const import (
30  CONF_HOST,
31  CONF_PORT,
32  CONF_PROTOCOL,
33  EVENT_HOMEASSISTANT_STOP,
34  EntityCategory,
35  UnitOfEnergy,
36  UnitOfVolume,
37 )
38 from homeassistant.core import CoreState, Event, HomeAssistant, callback
39 from homeassistant.helpers import device_registry as dr, entity_registry as er
40 from homeassistant.helpers.device_registry import DeviceInfo
42  async_dispatcher_connect,
43  async_dispatcher_send,
44 )
45 from homeassistant.helpers.entity_platform import AddEntitiesCallback
46 from homeassistant.helpers.typing import StateType
47 from homeassistant.util import Throttle
48 
49 from . import DsmrConfigEntry
50 from .const import (
51  CONF_DSMR_VERSION,
52  CONF_SERIAL_ID,
53  CONF_SERIAL_ID_GAS,
54  CONF_TIME_BETWEEN_UPDATE,
55  DEFAULT_PRECISION,
56  DEFAULT_RECONNECT_INTERVAL,
57  DEFAULT_TIME_BETWEEN_UPDATE,
58  DEVICE_NAME_ELECTRICITY,
59  DEVICE_NAME_GAS,
60  DEVICE_NAME_HEAT,
61  DEVICE_NAME_WATER,
62  DOMAIN,
63  DSMR_PROTOCOL,
64  LOGGER,
65 )
66 
67 EVENT_FIRST_TELEGRAM = "dsmr_first_telegram_{}"
68 
69 UNIT_CONVERSION = {"m3": UnitOfVolume.CUBIC_METERS}
70 
71 
72 @dataclass(frozen=True, kw_only=True)
74  """Represents an DSMR Sensor."""
75 
76  dsmr_versions: set[str] | None = None
77  is_gas: bool = False
78  is_water: bool = False
79  is_heat: bool = False
80  obis_reference: str
81 
82 
83 class MbusDeviceType(IntEnum):
84  """Types of mbus devices (13757-3:2013)."""
85 
86  GAS = 3
87  HEAT = 4
88  WATER = 7
89 
90 
91 SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
93  key="timestamp",
94  obis_reference="P1_MESSAGE_TIMESTAMP",
95  device_class=SensorDeviceClass.TIMESTAMP,
96  entity_category=EntityCategory.DIAGNOSTIC,
97  entity_registry_enabled_default=False,
98  ),
100  key="current_electricity_usage",
101  translation_key="current_electricity_usage",
102  obis_reference="CURRENT_ELECTRICITY_USAGE",
103  device_class=SensorDeviceClass.POWER,
104  state_class=SensorStateClass.MEASUREMENT,
105  ),
107  key="current_electricity_delivery",
108  translation_key="current_electricity_delivery",
109  obis_reference="CURRENT_ELECTRICITY_DELIVERY",
110  device_class=SensorDeviceClass.POWER,
111  state_class=SensorStateClass.MEASUREMENT,
112  ),
114  key="electricity_active_tariff",
115  translation_key="electricity_active_tariff",
116  obis_reference="ELECTRICITY_ACTIVE_TARIFF",
117  dsmr_versions={"2.2", "4", "5", "5B", "5L"},
118  device_class=SensorDeviceClass.ENUM,
119  options=["low", "normal"],
120  ),
122  key="electricity_used_tariff_1",
123  translation_key="electricity_used_tariff_1",
124  obis_reference="ELECTRICITY_USED_TARIFF_1",
125  dsmr_versions={"2.2", "4", "5", "5B", "5L"},
126  device_class=SensorDeviceClass.ENERGY,
127  state_class=SensorStateClass.TOTAL_INCREASING,
128  ),
130  key="electricity_used_tariff_2",
131  translation_key="electricity_used_tariff_2",
132  obis_reference="ELECTRICITY_USED_TARIFF_2",
133  dsmr_versions={"2.2", "4", "5", "5B", "5L"},
134  device_class=SensorDeviceClass.ENERGY,
135  state_class=SensorStateClass.TOTAL_INCREASING,
136  ),
138  key="electricity_delivered_tariff_1",
139  translation_key="electricity_delivered_tariff_1",
140  obis_reference="ELECTRICITY_DELIVERED_TARIFF_1",
141  dsmr_versions={"2.2", "4", "5", "5B", "5L"},
142  device_class=SensorDeviceClass.ENERGY,
143  state_class=SensorStateClass.TOTAL_INCREASING,
144  ),
146  key="electricity_delivered_tariff_2",
147  translation_key="electricity_delivered_tariff_2",
148  obis_reference="ELECTRICITY_DELIVERED_TARIFF_2",
149  dsmr_versions={"2.2", "4", "5", "5B", "5L"},
150  device_class=SensorDeviceClass.ENERGY,
151  state_class=SensorStateClass.TOTAL_INCREASING,
152  ),
154  key="instantaneous_active_power_l1_positive",
155  translation_key="instantaneous_active_power_l1_positive",
156  obis_reference="INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE",
157  device_class=SensorDeviceClass.POWER,
158  entity_registry_enabled_default=False,
159  state_class=SensorStateClass.MEASUREMENT,
160  ),
162  key="instantaneous_active_power_l2_positive",
163  translation_key="instantaneous_active_power_l2_positive",
164  obis_reference="INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE",
165  device_class=SensorDeviceClass.POWER,
166  entity_registry_enabled_default=False,
167  state_class=SensorStateClass.MEASUREMENT,
168  ),
170  key="instantaneous_active_power_l3_positive",
171  translation_key="instantaneous_active_power_l3_positive",
172  obis_reference="INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE",
173  device_class=SensorDeviceClass.POWER,
174  entity_registry_enabled_default=False,
175  state_class=SensorStateClass.MEASUREMENT,
176  ),
178  key="instantaneous_active_power_l1_negative",
179  translation_key="instantaneous_active_power_l1_negative",
180  obis_reference="INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE",
181  device_class=SensorDeviceClass.POWER,
182  entity_registry_enabled_default=False,
183  state_class=SensorStateClass.MEASUREMENT,
184  ),
186  key="instantaneous_active_power_l2_negative",
187  translation_key="instantaneous_active_power_l2_negative",
188  obis_reference="INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE",
189  device_class=SensorDeviceClass.POWER,
190  entity_registry_enabled_default=False,
191  state_class=SensorStateClass.MEASUREMENT,
192  ),
194  key="instantaneous_active_power_l3_negative",
195  translation_key="instantaneous_active_power_l3_negative",
196  obis_reference="INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE",
197  device_class=SensorDeviceClass.POWER,
198  entity_registry_enabled_default=False,
199  state_class=SensorStateClass.MEASUREMENT,
200  ),
202  key="short_power_failure_count",
203  translation_key="short_power_failure_count",
204  obis_reference="SHORT_POWER_FAILURE_COUNT",
205  dsmr_versions={"2.2", "4", "5", "5L"},
206  entity_registry_enabled_default=False,
207  entity_category=EntityCategory.DIAGNOSTIC,
208  ),
210  key="long_power_failure_count",
211  translation_key="long_power_failure_count",
212  obis_reference="LONG_POWER_FAILURE_COUNT",
213  dsmr_versions={"2.2", "4", "5", "5L"},
214  entity_registry_enabled_default=False,
215  entity_category=EntityCategory.DIAGNOSTIC,
216  ),
218  key="voltage_sag_l1_count",
219  translation_key="voltage_sag_l1_count",
220  obis_reference="VOLTAGE_SAG_L1_COUNT",
221  dsmr_versions={"2.2", "4", "5", "5L"},
222  entity_registry_enabled_default=False,
223  entity_category=EntityCategory.DIAGNOSTIC,
224  ),
226  key="voltage_sag_l2_count",
227  translation_key="voltage_sag_l2_count",
228  obis_reference="VOLTAGE_SAG_L2_COUNT",
229  dsmr_versions={"2.2", "4", "5", "5L"},
230  entity_registry_enabled_default=False,
231  entity_category=EntityCategory.DIAGNOSTIC,
232  ),
234  key="voltage_sag_l3_count",
235  translation_key="voltage_sag_l3_count",
236  obis_reference="VOLTAGE_SAG_L3_COUNT",
237  dsmr_versions={"2.2", "4", "5", "5L"},
238  entity_registry_enabled_default=False,
239  entity_category=EntityCategory.DIAGNOSTIC,
240  ),
242  key="voltage_swell_l1_count",
243  translation_key="voltage_swell_l1_count",
244  obis_reference="VOLTAGE_SWELL_L1_COUNT",
245  dsmr_versions={"2.2", "4", "5", "5L"},
246  entity_registry_enabled_default=False,
247  entity_category=EntityCategory.DIAGNOSTIC,
248  ),
250  key="voltage_swell_l2_count",
251  translation_key="voltage_swell_l2_count",
252  obis_reference="VOLTAGE_SWELL_L2_COUNT",
253  dsmr_versions={"2.2", "4", "5", "5L"},
254  entity_registry_enabled_default=False,
255  entity_category=EntityCategory.DIAGNOSTIC,
256  ),
258  key="voltage_swell_l3_count",
259  translation_key="voltage_swell_l3_count",
260  obis_reference="VOLTAGE_SWELL_L3_COUNT",
261  dsmr_versions={"2.2", "4", "5", "5L"},
262  entity_registry_enabled_default=False,
263  entity_category=EntityCategory.DIAGNOSTIC,
264  ),
266  key="instantaneous_voltage_l1",
267  translation_key="instantaneous_voltage_l1",
268  obis_reference="INSTANTANEOUS_VOLTAGE_L1",
269  device_class=SensorDeviceClass.VOLTAGE,
270  entity_registry_enabled_default=False,
271  state_class=SensorStateClass.MEASUREMENT,
272  entity_category=EntityCategory.DIAGNOSTIC,
273  ),
275  key="instantaneous_voltage_l2",
276  translation_key="instantaneous_voltage_l2",
277  obis_reference="INSTANTANEOUS_VOLTAGE_L2",
278  device_class=SensorDeviceClass.VOLTAGE,
279  entity_registry_enabled_default=False,
280  state_class=SensorStateClass.MEASUREMENT,
281  entity_category=EntityCategory.DIAGNOSTIC,
282  ),
284  key="instantaneous_voltage_l3",
285  translation_key="instantaneous_voltage_l3",
286  obis_reference="INSTANTANEOUS_VOLTAGE_L3",
287  device_class=SensorDeviceClass.VOLTAGE,
288  entity_registry_enabled_default=False,
289  state_class=SensorStateClass.MEASUREMENT,
290  entity_category=EntityCategory.DIAGNOSTIC,
291  ),
293  key="instantaneous_current_l1",
294  translation_key="instantaneous_current_l1",
295  obis_reference="INSTANTANEOUS_CURRENT_L1",
296  device_class=SensorDeviceClass.CURRENT,
297  entity_registry_enabled_default=False,
298  state_class=SensorStateClass.MEASUREMENT,
299  entity_category=EntityCategory.DIAGNOSTIC,
300  ),
302  key="instantaneous_current_l2",
303  translation_key="instantaneous_current_l2",
304  obis_reference="INSTANTANEOUS_CURRENT_L2",
305  device_class=SensorDeviceClass.CURRENT,
306  entity_registry_enabled_default=False,
307  state_class=SensorStateClass.MEASUREMENT,
308  entity_category=EntityCategory.DIAGNOSTIC,
309  ),
311  key="instantaneous_current_l3",
312  translation_key="instantaneous_current_l3",
313  obis_reference="INSTANTANEOUS_CURRENT_L3",
314  device_class=SensorDeviceClass.CURRENT,
315  entity_registry_enabled_default=False,
316  state_class=SensorStateClass.MEASUREMENT,
317  entity_category=EntityCategory.DIAGNOSTIC,
318  ),
320  key="belgium_max_power_per_phase",
321  translation_key="max_power_per_phase",
322  obis_reference="ACTUAL_TRESHOLD_ELECTRICITY",
323  dsmr_versions={"5B"},
324  device_class=SensorDeviceClass.POWER,
325  entity_registry_enabled_default=False,
326  state_class=SensorStateClass.MEASUREMENT,
327  entity_category=EntityCategory.DIAGNOSTIC,
328  ),
330  key="belgium_max_current_per_phase",
331  translation_key="max_current_per_phase",
332  obis_reference="FUSE_THRESHOLD_L1",
333  dsmr_versions={"5B"},
334  device_class=SensorDeviceClass.CURRENT,
335  entity_registry_enabled_default=False,
336  state_class=SensorStateClass.MEASUREMENT,
337  entity_category=EntityCategory.DIAGNOSTIC,
338  ),
340  key="electricity_imported_total",
341  translation_key="electricity_imported_total",
342  obis_reference="ELECTRICITY_IMPORTED_TOTAL",
343  dsmr_versions={"5L", "5S", "Q3D"},
344  device_class=SensorDeviceClass.ENERGY,
345  state_class=SensorStateClass.TOTAL_INCREASING,
346  ),
348  key="electricity_exported_total",
349  translation_key="electricity_exported_total",
350  obis_reference="ELECTRICITY_EXPORTED_TOTAL",
351  dsmr_versions={"5L", "5S", "Q3D"},
352  device_class=SensorDeviceClass.ENERGY,
353  state_class=SensorStateClass.TOTAL_INCREASING,
354  ),
356  key="belgium_current_average_demand",
357  translation_key="current_average_demand",
358  obis_reference="BELGIUM_CURRENT_AVERAGE_DEMAND",
359  dsmr_versions={"5B"},
360  device_class=SensorDeviceClass.POWER,
361  state_class=SensorStateClass.MEASUREMENT,
362  ),
364  key="belgium_maximum_demand_current_month",
365  translation_key="maximum_demand_current_month",
366  obis_reference="BELGIUM_MAXIMUM_DEMAND_MONTH",
367  dsmr_versions={"5B"},
368  device_class=SensorDeviceClass.POWER,
369  state_class=SensorStateClass.MEASUREMENT,
370  ),
372  key="hourly_gas_meter_reading",
373  translation_key="gas_meter_reading",
374  obis_reference="HOURLY_GAS_METER_READING",
375  dsmr_versions={"4", "5", "5L"},
376  is_gas=True,
377  device_class=SensorDeviceClass.GAS,
378  state_class=SensorStateClass.TOTAL_INCREASING,
379  ),
381  key="gas_meter_reading",
382  translation_key="gas_meter_reading",
383  obis_reference="GAS_METER_READING",
384  dsmr_versions={"2.2"},
385  is_gas=True,
386  device_class=SensorDeviceClass.GAS,
387  state_class=SensorStateClass.TOTAL_INCREASING,
388  ),
389 )
390 
391 SENSORS_MBUS_DEVICE_TYPE: dict[int, tuple[DSMRSensorEntityDescription, ...]] = {
392  MbusDeviceType.GAS: (
394  key="gas_reading",
395  translation_key="gas_meter_reading",
396  obis_reference="MBUS_METER_READING",
397  is_gas=True,
398  device_class=SensorDeviceClass.GAS,
399  state_class=SensorStateClass.TOTAL_INCREASING,
400  ),
401  ),
402  MbusDeviceType.HEAT: (
404  key="heat_reading",
405  translation_key="heat_meter_reading",
406  obis_reference="MBUS_METER_READING",
407  is_heat=True,
408  device_class=SensorDeviceClass.ENERGY,
409  state_class=SensorStateClass.TOTAL_INCREASING,
410  ),
411  ),
412  MbusDeviceType.WATER: (
414  key="water_reading",
415  translation_key="water_meter_reading",
416  obis_reference="MBUS_METER_READING",
417  is_water=True,
418  device_class=SensorDeviceClass.WATER,
419  state_class=SensorStateClass.TOTAL_INCREASING,
420  ),
421  ),
422 }
423 
424 
426  data: Telegram | MbusDevice,
427  entity_description: DSMRSensorEntityDescription,
428 ) -> tuple[SensorDeviceClass | None, str | None]:
429  """Get native unit of measurement from telegram,."""
430  dsmr_object = getattr(data, entity_description.obis_reference)
431  uom: str | None = getattr(dsmr_object, "unit") or None
432  with suppress(ValueError):
433  if entity_description.device_class == SensorDeviceClass.GAS and (
434  enery_uom := UnitOfEnergy(str(uom))
435  ):
436  return (SensorDeviceClass.ENERGY, enery_uom)
437  if uom in UNIT_CONVERSION:
438  return (entity_description.device_class, UNIT_CONVERSION[uom])
439  return (entity_description.device_class, uom)
440 
441 
443  hass: HomeAssistant, entry: ConfigEntry, mbus_device_id: str
444 ) -> None:
445  """Rename old gas sensor to mbus variant."""
446  dev_reg = dr.async_get(hass)
447  for dev_id in (mbus_device_id, entry.entry_id):
448  device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, dev_id)})
449  if device_entry_v1 is not None:
450  device_id = device_entry_v1.id
451 
452  ent_reg = er.async_get(hass)
453  entries = er.async_entries_for_device(ent_reg, device_id)
454 
455  for entity in entries:
456  if entity.unique_id.endswith(
457  "belgium_5min_gas_meter_reading"
458  ) or entity.unique_id.endswith("hourly_gas_meter_reading"):
459  try:
460  ent_reg.async_update_entity(
461  entity.entity_id,
462  new_unique_id=mbus_device_id,
463  device_id=mbus_device_id,
464  )
465  except ValueError:
466  LOGGER.debug(
467  "Skip migration of %s because it already exists",
468  entity.entity_id,
469  )
470  else:
471  LOGGER.debug(
472  "Migrated entity %s from unique id %s to %s",
473  entity.entity_id,
474  entity.unique_id,
475  mbus_device_id,
476  )
477  # Cleanup old device
478  dev_entities = er.async_entries_for_device(
479  ent_reg, device_id, include_disabled_entities=True
480  )
481  if not dev_entities:
482  dev_reg.async_remove_device(device_id)
483 
484 
486  data: Telegram | MbusDevice,
487  description: DSMRSensorEntityDescription,
488  dsmr_version: str,
489 ) -> bool:
490  """Check if this is a supported description for this telegram."""
491  return hasattr(data, description.obis_reference) and (
492  description.dsmr_versions is None or dsmr_version in description.dsmr_versions
493  )
494 
495 
497  hass: HomeAssistant, telegram: Telegram, entry: ConfigEntry, dsmr_version: str
498 ) -> Generator[DSMREntity]:
499  """Create MBUS Entities."""
500  mbus_devices: list[MbusDevice] = getattr(telegram, "MBUS_DEVICES", [])
501  for device in mbus_devices:
502  if (device_type := getattr(device, "MBUS_DEVICE_TYPE", None)) is None:
503  continue
504  type_ = int(device_type.value)
505 
506  if type_ not in SENSORS_MBUS_DEVICE_TYPE:
507  LOGGER.warning("Unsupported MBUS_DEVICE_TYPE (%d)", type_)
508  continue
509 
510  if identifier := getattr(device, "MBUS_EQUIPMENT_IDENTIFIER", None):
511  serial_ = identifier.value
512  rename_old_gas_to_mbus(hass, entry, serial_)
513  else:
514  serial_ = ""
515 
516  for description in SENSORS_MBUS_DEVICE_TYPE.get(type_, ()):
517  if not is_supported_description(device, description, dsmr_version):
518  continue
519  yield DSMREntity(
520  description,
521  entry,
522  telegram,
523  *device_class_and_uom(device, description), # type: ignore[arg-type]
524  serial_,
525  device.channel_id,
526  )
527 
528 
530  telegram: Telegram | None, mbus_id: int, obis_reference: str
531 ) -> DSMRObject | None:
532  """Extract DSMR object from telegram."""
533  if not telegram:
534  return None
535 
536  telegram_or_device: Telegram | MbusDevice | None = telegram
537  if mbus_id:
538  telegram_or_device = telegram.get_mbus_device_by_channel(mbus_id)
539  if telegram_or_device is None:
540  return None
541 
542  return getattr(telegram_or_device, obis_reference, None)
543 
544 
546  hass: HomeAssistant, entry: DsmrConfigEntry, async_add_entities: AddEntitiesCallback
547 ) -> None:
548  """Set up the DSMR sensor."""
549  dsmr_version = entry.data[CONF_DSMR_VERSION]
550  entities: list[DSMREntity] = []
551  initialized: bool = False
552  add_entities_handler: Callable[..., None] | None
553 
554  @callback
555  def init_async_add_entities(telegram: Telegram) -> None:
556  """Add the sensor entities after the first telegram was received."""
557  nonlocal add_entities_handler
558  assert add_entities_handler is not None
559  add_entities_handler()
560  add_entities_handler = None
561 
562  entities.extend(create_mbus_entities(hass, telegram, entry, dsmr_version))
563 
564  entities.extend(
565  [
566  DSMREntity(
567  description,
568  entry,
569  telegram,
570  *device_class_and_uom(telegram, description), # type: ignore[arg-type]
571  )
572  for description in SENSORS
573  if is_supported_description(telegram, description, dsmr_version)
574  and (
575  (not description.is_gas and not description.is_heat)
576  or CONF_SERIAL_ID_GAS in entry.data
577  )
578  ]
579  )
580  async_add_entities(entities)
581 
582  add_entities_handler = async_dispatcher_connect(
583  hass, EVENT_FIRST_TELEGRAM.format(entry.entry_id), init_async_add_entities
584  )
585  min_time_between_updates = timedelta(
586  seconds=entry.options.get(CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE)
587  )
588 
589  @Throttle(min_time_between_updates)
590  def update_entities_telegram(telegram: Telegram | None) -> None:
591  """Update entities with latest telegram and trigger state update."""
592  nonlocal initialized
593  # Make all device entities aware of new telegram
594  for entity in entities:
595  entity.update_data(telegram)
596 
597  entry.runtime_data.telegram = telegram
598 
599  if not initialized and telegram:
600  initialized = True
602  hass, EVENT_FIRST_TELEGRAM.format(entry.entry_id), telegram
603  )
604 
605  # Creates an asyncio.Protocol factory for reading DSMR telegrams from
606  # serial and calls update_entities_telegram to update entities on arrival
607  protocol = entry.data.get(CONF_PROTOCOL, DSMR_PROTOCOL)
608  if CONF_HOST in entry.data:
609  if protocol == DSMR_PROTOCOL:
610  create_reader = create_tcp_dsmr_reader
611  else:
612  create_reader = create_rfxtrx_tcp_dsmr_reader
613  reader_factory = partial(
614  create_reader,
615  entry.data[CONF_HOST],
616  entry.data[CONF_PORT],
617  dsmr_version,
618  update_entities_telegram,
619  loop=hass.loop,
620  keep_alive_interval=60,
621  )
622  else:
623  if protocol == DSMR_PROTOCOL:
624  create_reader = create_dsmr_reader
625  else:
626  create_reader = create_rfxtrx_dsmr_reader
627  reader_factory = partial(
628  create_reader,
629  entry.data[CONF_PORT],
630  dsmr_version,
631  update_entities_telegram,
632  loop=hass.loop,
633  )
634 
635  async def connect_and_reconnect() -> None:
636  """Connect to DSMR and keep reconnecting until Home Assistant stops."""
637  stop_listener = None
638  transport = None
639  protocol = None
640 
641  while hass.state is CoreState.not_running or hass.is_running:
642  # Start DSMR asyncio.Protocol reader
643 
644  # Reflect connected state in devices state by setting an
645  # empty telegram resulting in `unknown` states
646  update_entities_telegram({})
647 
648  try:
649  transport, protocol = await hass.loop.create_task(reader_factory())
650 
651  if transport:
652  # Register listener to close transport on HA shutdown
653  @callback
654  def close_transport(_event: Event) -> None:
655  """Close the transport on HA shutdown."""
656  if not transport: # noqa: B023
657  return
658  transport.close() # noqa: B023
659 
660  stop_listener = hass.bus.async_listen_once(
661  EVENT_HOMEASSISTANT_STOP, close_transport
662  )
663 
664  # Wait for reader to close
665  await protocol.wait_closed()
666 
667  # Unexpected disconnect
668  if hass.state is CoreState.not_running or hass.is_running:
669  stop_listener()
670 
671  transport = None
672  protocol = None
673 
674  # Reflect disconnect state in devices state by setting an
675  # None telegram resulting in `unavailable` states
676  update_entities_telegram(None)
677 
678  # throttle reconnect attempts
679  await asyncio.sleep(DEFAULT_RECONNECT_INTERVAL)
680 
681  except (serial.SerialException, OSError):
682  # Log any error while establishing connection and drop to retry
683  # connection wait
684  LOGGER.exception("Error connecting to DSMR")
685  transport = None
686  protocol = None
687 
688  # Reflect disconnect state in devices state by setting an
689  # None telegram resulting in `unavailable` states
690  update_entities_telegram(None)
691 
692  # throttle reconnect attempts
693  await asyncio.sleep(DEFAULT_RECONNECT_INTERVAL)
694  except CancelledError:
695  # Reflect disconnect state in devices state by setting an
696  # None telegram resulting in `unavailable` states
697  update_entities_telegram(None)
698 
699  if stop_listener and (
700  hass.state is CoreState.not_running or hass.is_running
701  ):
702  stop_listener()
703 
704  if transport:
705  transport.close()
706 
707  if protocol:
708  await protocol.wait_closed()
709 
710  return
711 
712  # Can't be hass.async_add_job because job runs forever
713  task = asyncio.create_task(connect_and_reconnect())
714 
715  @callback
716  def _async_stop(_: Event) -> None:
717  if add_entities_handler is not None:
718  add_entities_handler()
719  task.cancel()
720 
721  # Make sure task is cancelled on shutdown (or tests complete)
722  entry.async_on_unload(
723  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop)
724  )
725 
726  # Save the task to be able to cancel it when unloading
727  entry.runtime_data.task = task
728 
729 
731  """Entity reading values from DSMR telegram."""
732 
733  entity_description: DSMRSensorEntityDescription
734  _attr_has_entity_name = True
735  _attr_should_poll = False
736 
737  def __init__(
738  self,
739  entity_description: DSMRSensorEntityDescription,
740  entry: ConfigEntry,
741  telegram: Telegram,
742  device_class: SensorDeviceClass,
743  native_unit_of_measurement: str | None,
744  serial_id: str = "",
745  mbus_id: int = 0,
746  ) -> None:
747  """Initialize entity."""
748  self.entity_descriptionentity_description = entity_description
749  self._attr_device_class_attr_device_class = device_class
750  self._attr_native_unit_of_measurement_attr_native_unit_of_measurement = native_unit_of_measurement
751  self._entry_entry = entry
752  self.telegramtelegram: Telegram | None = telegram
753 
754  device_serial = entry.data[CONF_SERIAL_ID]
755  device_name = DEVICE_NAME_ELECTRICITY
756  if entity_description.is_gas:
757  if serial_id:
758  device_serial = serial_id
759  else:
760  device_serial = entry.data[CONF_SERIAL_ID_GAS]
761  device_name = DEVICE_NAME_GAS
762  if entity_description.is_water:
763  if serial_id:
764  device_serial = serial_id
765  device_name = DEVICE_NAME_WATER
766  if entity_description.is_heat:
767  if serial_id:
768  device_serial = serial_id
769  device_name = DEVICE_NAME_HEAT
770  if device_serial is None:
771  device_serial = entry.entry_id
772 
773  self._attr_device_info_attr_device_info = DeviceInfo(
774  identifiers={(DOMAIN, device_serial)},
775  name=device_name,
776  )
777  self._mbus_id_mbus_id = mbus_id
778  if mbus_id != 0:
779  if serial_id:
780  self._attr_unique_id_attr_unique_id = f"{device_serial}"
781  else:
782  self._attr_unique_id_attr_unique_id = f"{device_serial}_{mbus_id}"
783  else:
784  self._attr_unique_id_attr_unique_id = f"{device_serial}_{entity_description.key}"
785 
786  @callback
787  def update_data(self, telegram: Telegram | None) -> None:
788  """Update data."""
789  self.telegramtelegram = telegram
790  if self.hasshass and (
791  telegram is None
792  or get_dsmr_object(
793  telegram, self._mbus_id_mbus_id, self.entity_descriptionentity_description.obis_reference
794  )
795  ):
796  self.async_write_ha_stateasync_write_ha_state()
797 
798  def get_dsmr_object_attr(self, attribute: str) -> str | None:
799  """Read attribute from last received telegram for this DSMR object."""
800  # Get the object
801  dsmr_object = get_dsmr_object(
802  self.telegramtelegram, self._mbus_id_mbus_id, self.entity_descriptionentity_description.obis_reference
803  )
804  if dsmr_object is None:
805  return None
806 
807  # Get the attribute value if the object has it
808  attr: str | None = getattr(dsmr_object, attribute)
809  return attr
810 
811  @property
812  def available(self) -> bool:
813  """Entity is only available if there is a telegram."""
814  return self.telegramtelegram is not None
815 
816  @property
817  def native_value(self) -> StateType:
818  """Return the state of sensor, if available, translate if needed."""
819  value: StateType
820  if (value := self.get_dsmr_object_attrget_dsmr_object_attr("value")) is None:
821  return None
822 
823  if self.entity_descriptionentity_description.obis_reference == "ELECTRICITY_ACTIVE_TARIFF":
824  return self.translate_tarifftranslate_tariff(value, self._entry_entry.data[CONF_DSMR_VERSION])
825 
826  with suppress(TypeError):
827  value = round(float(value), DEFAULT_PRECISION)
828 
829  # Make sure we do not return a zero value for an energy sensor
830  if not value and self.state_classstate_classstate_class == SensorStateClass.TOTAL_INCREASING:
831  return None
832 
833  return value
834 
835  @staticmethod
836  def translate_tariff(value: str, dsmr_version: str) -> str | None:
837  """Convert 2/1 to normal/low depending on DSMR version."""
838  # DSMR V5B: Note: In Belgium values are swapped:
839  # Rate code 2 is used for low rate and rate code 1 is used for normal rate.
840  if dsmr_version == "5B":
841  if value == "0001":
842  value = "0002"
843  elif value == "0002":
844  value = "0001"
845  # DSMR V2.2: Note: Rate code 1 is used for low rate and rate code 2 is
846  # used for normal rate.
847  if value == "0002":
848  return "normal"
849  if value == "0001":
850  return "low"
851 
852  return None
None update_data(self, Telegram|None telegram)
Definition: sensor.py:787
str|None translate_tariff(str value, str dsmr_version)
Definition: sensor.py:836
str|None get_dsmr_object_attr(self, str attribute)
Definition: sensor.py:798
None __init__(self, DSMRSensorEntityDescription entity_description, ConfigEntry entry, Telegram telegram, SensorDeviceClass device_class, str|None native_unit_of_measurement, str serial_id="", int mbus_id=0)
Definition: sensor.py:746
SensorStateClass|str|None state_class(self)
Definition: __init__.py:342
Generator[DSMREntity] create_mbus_entities(HomeAssistant hass, Telegram telegram, ConfigEntry entry, str dsmr_version)
Definition: sensor.py:498
tuple[SensorDeviceClass|None, str|None] device_class_and_uom(Telegram|MbusDevice data, DSMRSensorEntityDescription entity_description)
Definition: sensor.py:428
None rename_old_gas_to_mbus(HomeAssistant hass, ConfigEntry entry, str mbus_device_id)
Definition: sensor.py:444
None async_setup_entry(HomeAssistant hass, DsmrConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:547
DSMRObject|None get_dsmr_object(Telegram|None telegram, int mbus_id, str obis_reference)
Definition: sensor.py:531
bool is_supported_description(Telegram|MbusDevice data, DSMRSensorEntityDescription description, str dsmr_version)
Definition: sensor.py:489
None _async_stop(HomeAssistant hass, bool restart)
Definition: __init__.py:392
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193