diff --git a/homeassistant/components/kaco_modbus/diagnostics.py b/homeassistant/components/kaco_modbus/diagnostics.py new file mode 100644 index 000000000000..f8f1ebc1dd28 --- /dev/null +++ b/homeassistant/components/kaco_modbus/diagnostics.py @@ -0,0 +1,46 @@ +"""Diagnostics for KACO Modbus.""" + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.core import HomeAssistant + +from .coordinator import KacoConfigEntry + +TO_REDACT = {"serial_number"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: KacoConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry. + + The raw register map is included because it loads straight into + ``modbus_connection.mock``, which reproduces a reported problem without + the hardware. It covers the polled components only, so the serial number + model 1 holds in the clear is not in it. + """ + coordinator = entry.runtime_data + device = coordinator.device + info = device.info + assert info is not None + + return async_redact_data( + { + "manufacturer": info.manufacturer, + "model": info.model, + "firmware": info.firmware, + "options": info.options, + "serial_number": info.serial_number, + "base_address": device.base_address, + "models": sorted(device.models or ()), + "strings": len(device.strings), + "updated": sorted(coordinator.data.updated), + "failed": { + component: str(error) + for component, error in coordinator.data.failed.items() + }, + "raw": await device.async_read_raw(), + }, + TO_REDACT, + ) diff --git a/homeassistant/components/kaco_modbus/quality_scale.yaml b/homeassistant/components/kaco_modbus/quality_scale.yaml index 5e4ff32ee2aa..a2e69a830d66 100644 --- a/homeassistant/components/kaco_modbus/quality_scale.yaml +++ b/homeassistant/components/kaco_modbus/quality_scale.yaml @@ -52,7 +52,7 @@ rules: test-coverage: done # Gold devices: done - diagnostics: todo + diagnostics: done discovery-update-info: status: exempt comment: There is no discovery to carry updated network information. diff --git a/tests/components/kaco_modbus/snapshots/test_diagnostics.ambr b/tests/components/kaco_modbus/snapshots/test_diagnostics.ambr new file mode 100644 index 000000000000..c73fb850d3c3 --- /dev/null +++ b/tests/components/kaco_modbus/snapshots/test_diagnostics.ambr @@ -0,0 +1,282 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'base_address': 40000, + 'failed': dict({ + }), + 'firmware': 'V5.53', + 'manufacturer': 'KACO new energy', + 'model': 'blueplanet 8.6 TL3 INT', + 'models': list([ + 1, + 103, + 113, + 120, + 121, + 122, + 123, + 126, + 129, + 130, + 132, + 135, + 136, + 160, + 64204, + ]), + 'options': '086L32', + 'raw': dict({ + 'holding': dict({ + '40070': 103, + '40071': 50, + '40072': 484, + '40073': 164, + '40074': 162, + '40075': 157, + '40076': 65534, + '40077': 65535, + '40078': 65535, + '40079': 65535, + '40080': 2265, + '40081': 2282, + '40082': 2278, + '40083': 65535, + '40084': 100, + '40085': 1, + '40086': 49944, + '40087': 65533, + '40088': 99, + '40089': 1, + '40090': 0, + '40091': 1, + '40092': 1000, + '40093': 65533, + '40094': 185, + '40095': 63009, + '40096': 0, + '40097': 230, + '40098': 65534, + '40099': 4437, + '40100': 65535, + '40101': 102, + '40102': 1, + '40103': 469, + '40104': 32768, + '40105': 32768, + '40106': 32768, + '40107': 65535, + '40108': 4, + '40109': 4, + '40110': 0, + '40111': 0, + '40112': 0, + '40113': 0, + '40114': 65535, + '40115': 65535, + '40116': 65535, + '40117': 65535, + '40118': 65535, + '40119': 65535, + '40120': 65535, + '40121': 65535, + '40184': 120, + '40185': 26, + '40186': 4, + '40187': 860, + '40188': 1, + '40189': 860, + '40190': 1, + '40191': 860, + '40192': 32768, + '40193': 32768, + '40194': 860, + '40195': 1, + '40196': 65535, + '40197': 32768, + '40198': 50880, + '40199': 32768, + '40200': 32768, + '40201': 50880, + '40202': 65533, + '40203': 65535, + '40204': 32768, + '40205': 65535, + '40206': 32768, + '40207': 65535, + '40208': 32768, + '40209': 65535, + '40210': 32768, + '40212': 121, + '40213': 30, + '40214': 860, + '40215': 2300, + '40216': 32768, + '40217': 65535, + '40218': 65535, + '40219': 860, + '40220': 32768, + '40221': 32768, + '40222': 32768, + '40223': 32768, + '40224': 65535, + '40225': 32768, + '40226': 32768, + '40227': 32768, + '40228': 32768, + '40229': 65535, + '40230': 65535, + '40231': 65535, + '40232': 65535, + '40233': 65535, + '40234': 1, + '40235': 65535, + '40236': 32768, + '40237': 32768, + '40238': 1, + '40239': 32768, + '40240': 32768, + '40241': 32768, + '40242': 32768, + '40243': 32768, + '40244': 122, + '40245': 44, + '40246': 7, + '40247': 65535, + '40248': 1, + '40249': 0, + '40250': 0, + '40251': 0, + '40252': 0, + '40253': 0, + '40254': 0, + '40255': 0, + '40256': 0, + '40257': 0, + '40258': 0, + '40259': 0, + '40260': 0, + '40261': 0, + '40262': 0, + '40263': 0, + '40264': 0, + '40265': 0, + '40266': 0, + '40267': 0, + '40268': 0, + '40269': 0, + '40270': 0, + '40271': 0, + '40272': 0, + '40273': 32768, + '40274': 32768, + '40275': 65535, + '40276': 32768, + '40277': 65535, + '40278': 65535, + '40279': 65535, + '40280': 65535, + '40281': 19265, + '40282': 17231, + '40283': 8302, + '40284': 101, + '40285': 12830, + '40286': 1609, + '40287': 65535, + '40288': 65535, + '40289': 32768, + '40290': 123, + '40291': 24, + '40292': 65535, + '40293': 65535, + '40294': 1, + '40295': 1000, + '40296': 65535, + '40297': 300, + '40298': 65535, + '40299': 0, + '40300': 1000, + '40301': 65535, + '40302': 300, + '40303': 65535, + '40304': 0, + '40305': 0, + '40306': 0, + '40307': 32768, + '40308': 65535, + '40309': 300, + '40310': 65535, + '40311': 1, + '40312': 0, + '40313': 65535, + '40314': 65533, + '40315': 65535, + '40316': 126, + '40317': 226, + '40318': 1, + '40319': 0, + '40320': 65535, + '40321': 65535, + '40322': 65535, + '40323': 4, + '40324': 10, + '40858': 160, + '40859': 48, + '40860': 65534, + '40861': 65535, + '40862': 1, + '40863': 32768, + '40864': 0, + '40865': 0, + '40866': 2, + '40867': 65535, + '40868': 0, + '40869': 19792, + '40870': 20564, + '40871': 8240, + '40872': 0, + '40873': 0, + '40874': 0, + '40875': 0, + '40876': 0, + '40877': 102, + '40878': 3551, + '40879': 36, + '40880': 0, + '40881': 0, + '40882': 27275, + '40883': 15290, + '40884': 45, + '40885': 65535, + '40886': 65535, + '40887': 65535, + '40888': 1, + '40889': 19792, + '40890': 20564, + '40891': 8241, + '40892': 0, + '40893': 0, + '40894': 0, + '40895': 0, + '40896': 0, + '40897': 122, + '40898': 5330, + '40899': 65, + '40900': 0, + '40901': 0, + '40902': 27275, + '40903': 15290, + '40904': 0, + '40905': 65535, + '40906': 65535, + '40907': 65535, + }), + }), + 'serial_number': '**REDACTED**', + 'strings': 2, + 'updated': list([ + 'inverter', + 'mppt', + 'status', + ]), + }) +# --- diff --git a/tests/components/kaco_modbus/test_diagnostics.py b/tests/components/kaco_modbus/test_diagnostics.py new file mode 100644 index 000000000000..b43a90b2b9f6 --- /dev/null +++ b/tests/components/kaco_modbus/test_diagnostics.py @@ -0,0 +1,41 @@ +"""Test the KACO Modbus diagnostics.""" + +import json + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from . import MOCK_SERIAL + +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, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test generating diagnostics for a config entry.""" + diag = await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + + assert diag == snapshot + + +async def test_diagnostics_does_not_leak_the_serial_number( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + init_integration: MockConfigEntry, +) -> None: + """Test the serial is redacted, and absent from the raw registers. + + Model 1 carries it in the clear, so a raw map that reached back over that + block would hand it to anyone who decoded the dump. + """ + diag = await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + + assert diag["serial_number"] == "**REDACTED**" + assert MOCK_SERIAL not in json.dumps(diag)