Add diagnostic information to DSMR (#122041)

* Add diagnostic information to DSMR

Switches to runtime_data to get access
to the last telegram received.

* Correct import of domain

* Apply suggestions from code review

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

---------

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
This commit is contained in:
Joakim Plate 2024-07-18 11:57:11 +02:00 committed by GitHub
parent 41d75e159b
commit 42610f4e09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 171 additions and 23 deletions

View File

@ -2,18 +2,32 @@
from __future__ import annotations
from asyncio import CancelledError
from asyncio import CancelledError, Task
from contextlib import suppress
from dataclasses import dataclass
from typing import Any
from dsmr_parser.objects import Telegram
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from .const import CONF_DSMR_VERSION, DATA_TASK, DOMAIN, PLATFORMS
from .const import CONF_DSMR_VERSION, PLATFORMS
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@dataclass
class DsmrState:
"""State of integration."""
task: Task | None = None
telegram: Telegram | None = None
type DsmrConfigEntry = ConfigEntry[DsmrState]
async def async_setup_entry(hass: HomeAssistant, entry: DsmrConfigEntry) -> bool:
"""Set up DSMR from a config entry."""
@callback
@ -25,32 +39,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await er.async_migrate_entries(hass, entry.entry_id, _async_migrate_entity_entry)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {}
entry.runtime_data = DsmrState()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_update_options))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: DsmrConfigEntry) -> bool:
"""Unload a config entry."""
task = hass.data[DOMAIN][entry.entry_id][DATA_TASK]
# Cancel the reconnect task
task.cancel()
with suppress(CancelledError):
await task
if task := entry.runtime_data.task:
task.cancel()
with suppress(CancelledError):
await task
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_update_options(hass: HomeAssistant, entry: DsmrConfigEntry) -> None:
"""Update options."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -23,8 +23,6 @@ DEFAULT_PRECISION = 3
DEFAULT_RECONNECT_INTERVAL = 30
DEFAULT_TIME_BETWEEN_UPDATE = 30
DATA_TASK = "task"
DEVICE_NAME_ELECTRICITY = "Electricity Meter"
DEVICE_NAME_GAS = "Gas Meter"
DEVICE_NAME_WATER = "Water Meter"

View File

@ -0,0 +1,28 @@
"""Diagnostics support for DSMR."""
from __future__ import annotations
from typing import Any
from homeassistant.core import HomeAssistant
from homeassistant.util.json import json_loads
from . import DsmrConfigEntry
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: DsmrConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"entry": {
"data": {
**config_entry.data,
},
"unique_id": config_entry.unique_id,
},
"data": json_loads(config_entry.runtime_data.telegram.to_json())
if config_entry.runtime_data.telegram
else None,
}

View File

@ -46,12 +46,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util import Throttle
from . import DsmrConfigEntry
from .const import (
CONF_DSMR_VERSION,
CONF_SERIAL_ID,
CONF_SERIAL_ID_GAS,
CONF_TIME_BETWEEN_UPDATE,
DATA_TASK,
DEFAULT_PRECISION,
DEFAULT_RECONNECT_INTERVAL,
DEFAULT_TIME_BETWEEN_UPDATE,
@ -514,7 +514,7 @@ def create_mbus_entities(
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant, entry: DsmrConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the DSMR sensor."""
dsmr_version = entry.data[CONF_DSMR_VERSION]
@ -567,6 +567,8 @@ async def async_setup_entry(
for entity in entities:
entity.update_data(telegram)
entry.runtime_data.telegram = telegram
if not initialized and telegram:
initialized = True
async_dispatcher_send(
@ -695,7 +697,7 @@ async def async_setup_entry(
)
# Save the task to be able to cancel it when unloading
hass.data[DOMAIN][entry.entry_id][DATA_TASK] = task
entry.runtime_data.task = task
class DSMREntity(SensorEntity):

View File

@ -0,0 +1,29 @@
# serializer version: 1
# name: test_diagnostics
dict({
'data': dict({
'CURRENT_ELECTRICITY_USAGE': dict({
'unit': 'W',
'value': 0.0,
}),
'ELECTRICITY_ACTIVE_TARIFF': dict({
'unit': '',
'value': '0001',
}),
'GAS_METER_READING': dict({
'datetime': '2019-03-03T19:43:33+00:00',
'unit': 'm³',
'value': 745.695,
}),
}),
'entry': dict({
'data': dict({
'dsmr_version': '2.2',
'port': '/dev/ttyUSB0',
'serial_id': '1234',
'serial_id_gas': '5678',
}),
'unique_id': '/dev/ttyUSB0',
}),
})
# ---

View File

@ -10,7 +10,8 @@ import serial
import serial.tools.list_ports
from homeassistant import config_entries
from homeassistant.components.dsmr import DOMAIN, config_flow
from homeassistant.components.dsmr import config_flow
from homeassistant.components.dsmr.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType

View File

@ -0,0 +1,82 @@
"""Test DSMR diagnostics."""
import datetime
from decimal import Decimal
from unittest.mock import MagicMock
from dsmr_parser.obis_references import (
CURRENT_ELECTRICITY_USAGE,
ELECTRICITY_ACTIVE_TARIFF,
GAS_METER_READING,
)
from dsmr_parser.objects import CosemObject, MBusObject, Telegram
from syrupy.assertion import SnapshotAssertion
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
async def test_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock],
snapshot: SnapshotAssertion,
) -> None:
"""Test diagnostics."""
(connection_factory, transport, protocol) = dsmr_connection_fixture
entry_data = {
"port": "/dev/ttyUSB0",
"dsmr_version": "2.2",
"serial_id": "1234",
"serial_id_gas": "5678",
}
entry_options = {
"time_between_update": 0,
}
telegram = Telegram()
telegram.add(
CURRENT_ELECTRICITY_USAGE,
CosemObject(
(0, 0),
[{"value": Decimal("0.0"), "unit": "W"}],
),
"CURRENT_ELECTRICITY_USAGE",
)
telegram.add(
ELECTRICITY_ACTIVE_TARIFF,
CosemObject((0, 0), [{"value": "0001", "unit": ""}]),
"ELECTRICITY_ACTIVE_TARIFF",
)
telegram.add(
GAS_METER_READING,
MBusObject(
(0, 0),
[
{"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": ""},
],
),
"GAS_METER_READING",
)
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)
result = await get_diagnostics_for_config_entry(hass, hass_client, mock_entry)
assert result == snapshot