mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Add diagnostics to WattWächter Plus (#175384)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c13ca88ef0
commit
c00f4e894a
@@ -0,0 +1,53 @@
|
||||
"""Diagnostics support for the WattWächter Plus integration."""
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from aio_wattwaechter import (
|
||||
WattwaechterAuthenticationError,
|
||||
WattwaechterConnectionError,
|
||||
)
|
||||
from aio_wattwaechter.models import SystemInfo
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_MAC, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import WattwaechterConfigEntry
|
||||
|
||||
# The device exposes network identifiers as system info values; redact the
|
||||
# credential and hardware/network identifiers. Local IPs are kept for support.
|
||||
TO_REDACT = {CONF_TOKEN, CONF_MAC, "ssid", "mac_address", "mdns_name"}
|
||||
|
||||
|
||||
def _flatten_system(system: SystemInfo) -> dict[str, dict[str, Any]]:
|
||||
"""Flatten system info sections into {section: {name: value}} mappings."""
|
||||
return {
|
||||
section: {entry["name"]: entry["value"] for entry in entries}
|
||||
for section, entries in asdict(system).items()
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: WattwaechterConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
# System info is only needed on demand here, so it is fetched directly
|
||||
# instead of in the update loop to avoid coupling meter sensor
|
||||
# availability to it. Failure still yields the config and meter data.
|
||||
system: dict[str, dict[str, Any]] | None = None
|
||||
try:
|
||||
system = _flatten_system(await coordinator.client.system_info())
|
||||
except WattwaechterConnectionError, WattwaechterAuthenticationError:
|
||||
system = None
|
||||
|
||||
return async_redact_data(
|
||||
{
|
||||
"config_entry": dict(entry.data),
|
||||
"meter": asdict(coordinator.data),
|
||||
"system": system,
|
||||
},
|
||||
TO_REDACT,
|
||||
)
|
||||
@@ -49,7 +49,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: todo
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# serializer version: 1
|
||||
# name: test_diagnostics
|
||||
dict({
|
||||
'config_entry': dict({
|
||||
'device_id': 'ABC123',
|
||||
'fw_version': '1.2.3',
|
||||
'host': '192.168.1.100',
|
||||
'mac': '**REDACTED**',
|
||||
'model': 'WW-Plus',
|
||||
'token': '**REDACTED**',
|
||||
}),
|
||||
'meter': dict({
|
||||
'datetime_str': '2024-01-01T00:00:00',
|
||||
'timestamp': 1704067200,
|
||||
'values': dict({
|
||||
'1.8.0': dict({
|
||||
'name': 'Total Import',
|
||||
'unit': 'kWh',
|
||||
'value': 12345.678,
|
||||
}),
|
||||
'13.7.0': dict({
|
||||
'name': 'Power Factor',
|
||||
'unit': '',
|
||||
'value': 0.985,
|
||||
}),
|
||||
'14.7.0': dict({
|
||||
'name': 'Frequency',
|
||||
'unit': 'Hz',
|
||||
'value': 50.01,
|
||||
}),
|
||||
'16.7.0': dict({
|
||||
'name': 'Active Power',
|
||||
'unit': 'W',
|
||||
'value': 1500.5,
|
||||
}),
|
||||
'2.8.0': dict({
|
||||
'name': 'Total Export',
|
||||
'unit': 'kWh',
|
||||
'value': 1234.567,
|
||||
}),
|
||||
'31.7.0': dict({
|
||||
'name': 'Current L1',
|
||||
'unit': 'A',
|
||||
'value': 6.52,
|
||||
}),
|
||||
'32.7.0': dict({
|
||||
'name': 'Voltage L1',
|
||||
'unit': 'V',
|
||||
'value': 230.1,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
'system': dict({
|
||||
'ap': dict({
|
||||
}),
|
||||
'esp': dict({
|
||||
'esp_id': 'ABC123',
|
||||
'os_version': '1.2.3',
|
||||
}),
|
||||
'heap': dict({
|
||||
'free_heap': '120000',
|
||||
}),
|
||||
'uptime': dict({
|
||||
'uptime': '2d 5h 30m',
|
||||
}),
|
||||
'wifi': dict({
|
||||
'ip_address': '192.168.1.100',
|
||||
'mac_address': '**REDACTED**',
|
||||
'mdns_name': '**REDACTED**',
|
||||
'signal_strength': '-45',
|
||||
'ssid': '**REDACTED**',
|
||||
}),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Tests for the WattWächter Plus diagnostics."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aio_wattwaechter import WattwaechterConnectionError
|
||||
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,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_client: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test config entry diagnostics."""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)
|
||||
== snapshot
|
||||
)
|
||||
|
||||
|
||||
async def test_diagnostics_system_info_unavailable(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test diagnostics still return config and meter data without system info."""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_client.system_info.side_effect = WattwaechterConnectionError("offline")
|
||||
result = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, mock_config_entry
|
||||
)
|
||||
|
||||
assert result["system"] is None
|
||||
assert result["meter"] is not None
|
||||
@@ -2,17 +2,20 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.wattwaechter.const import DOMAIN
|
||||
from homeassistant.components.wattwaechter.const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import MOCK_DEVICE_ID, MOCK_METER_DATA_MINIMAL
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
async def test_all_entities(
|
||||
@@ -54,3 +57,29 @@ async def test_minimal_meter_data(
|
||||
assert _get_entity_id("2.8.0") is None
|
||||
assert _get_entity_id("32.7.0") is None
|
||||
assert _get_entity_id("31.7.0") is None
|
||||
|
||||
|
||||
async def test_sensor_value_unknown_when_obis_stops_reporting(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_client: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test a sensor reports unknown when its OBIS code is no longer reported."""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
"sensor", DOMAIN, f"{MOCK_DEVICE_ID}_2.8.0"
|
||||
)
|
||||
assert entity_id is not None
|
||||
assert hass.states.get(entity_id).state != STATE_UNKNOWN
|
||||
|
||||
# Device stops reporting the export total OBIS code
|
||||
mock_client.meter_data.return_value = MOCK_METER_DATA_MINIMAL
|
||||
freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_UNKNOWN
|
||||
|
||||
Reference in New Issue
Block a user