diff --git a/homeassistant/components/sofar/diagnostics.py b/homeassistant/components/sofar/diagnostics.py new file mode 100644 index 000000000000..8a4b2044aacf --- /dev/null +++ b/homeassistant/components/sofar/diagnostics.py @@ -0,0 +1,35 @@ +"""Diagnostics support for Sofar.""" + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.core import HomeAssistant + +from .coordinator import SofarConfigEntry + +TO_REDACT = {"serial_number"} + +_SERIAL_NUMBER_REGISTERS = range(0x0445, 0x044C) + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: SofarConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + device = entry.runtime_data.readings.device + raw = await device.async_read_raw() + if (holding := raw.get("holding")) is not None: + for address in _SERIAL_NUMBER_REGISTERS: + holding.pop(address, None) + + return async_redact_data( + { + "model": device.model, + "inverter_type": device.inverter_type, + "serial_number": device.serial_number, + "readings_components": device.readings_components, + "settings_components": device.settings_components, + "raw": raw, + }, + TO_REDACT, + ) diff --git a/homeassistant/components/sofar/quality_scale.yaml b/homeassistant/components/sofar/quality_scale.yaml index dc2bd37d87bd..004c68be592e 100644 --- a/homeassistant/components/sofar/quality_scale.yaml +++ b/homeassistant/components/sofar/quality_scale.yaml @@ -49,7 +49,7 @@ rules: # Gold devices: done - diagnostics: todo + diagnostics: done discovery-update-info: status: exempt comment: Modbus TCP gateways have no discovery protocol to update network info from. diff --git a/tests/components/sofar/snapshots/test_diagnostics.ambr b/tests/components/sofar/snapshots/test_diagnostics.ambr new file mode 100644 index 000000000000..e8c1ff9856b1 --- /dev/null +++ b/tests/components/sofar/snapshots/test_diagnostics.ambr @@ -0,0 +1,151 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'inverter_type': 1537, + 'model': '4.4 KTLX-G3', + 'raw': dict({ + 'holding': dict({ + '1028': 2, + '1029': 0, + '1030': 0, + '1031': 0, + '1032': 0, + '1033': 0, + '1034': 0, + '1035': 0, + '1036': 0, + '1037': 0, + '1038': 0, + '1039': 0, + '1040': 0, + '1041': 0, + '1042': 0, + '1043': 0, + '1044': 0, + '1045': 0, + '1046': 0, + '1047': 0, + '1048': 0, + '1049': 0, + '1050': 0, + '1051': 0, + '1052': 0, + '1053': 0, + '1054': 0, + '1055': 0, + '1056': 0, + '1057': 0, + '1068': 0, + '1069': 0, + '1070': 0, + '1071': 0, + '1072': 0, + '1073': 0, + '1100': 0, + '1101': 22065, + '1102': 12336, + '1103': 22066, + '1104': 12848, + '1105': 0, + '1106': 0, + '1156': 5000, + '1157': 0, + '1158': 0, + '1159': 0, + '1160': 0, + '1161': 0, + '1162': 0, + '1163': 0, + '1164': 0, + '1165': 0, + '1166': 0, + '1167': 0, + '1168': 0, + '1169': 0, + '1170': 0, + '1171': 0, + '1172': 0, + '1173': 0, + '1174': 0, + '1175': 0, + '1176': 0, + '1177': 0, + '1178': 0, + '1179': 0, + '1180': 0, + '1181': 0, + '1182': 0, + '1183': 0, + '1184': 0, + '1185': 0, + '1186': 0, + '1187': 0, + '1188': 0, + '1189': 0, + '1190': 0, + '1191': 0, + '1192': 0, + '1193': 0, + '1194': 0, + '1195': 0, + '1196': 0, + '1197': 0, + '1198': 0, + '1199': 0, + '1200': 0, + '1201': 0, + '1202': 0, + '1203': 0, + '1204': 0, + '1205': 0, + '1206': 0, + '1207': 0, + '1208': 0, + '1209': 0, + '1210': 0, + '1211': 0, + '1212': 0, + '1412': 0, + '1413': 0, + '1414': 250, + '1415': 0, + '1416': 0, + '1417': 180, + '1476': 43, + '1668': 0, + '1669': 1000, + '1670': 0, + '1671': 150, + '1672': 0, + '1673': 0, + '1674': 0, + '1675': 0, + '1676': 0, + '1677': 0, + '1678': 0, + '1679': 0, + '1680': 0, + '1681': 0, + '1682': 0, + '1683': 0, + '4131': 0, + '4132': 0, + '4356': 0, + '4357': 0, + '4358': 0, + }), + }), + 'readings_components': list([ + 'state', + 'grid', + 'pv_1_2', + 'energy', + ]), + 'serial_number': '**REDACTED**', + 'settings_components': list([ + 'feed_in', + 'remote', + 'active_power_control', + ]), + }) +# --- diff --git a/tests/components/sofar/test_diagnostics.py b/tests/components/sofar/test_diagnostics.py new file mode 100644 index 000000000000..cd2a4f2e71ea --- /dev/null +++ b/tests/components/sofar/test_diagnostics.py @@ -0,0 +1,35 @@ +"""Test the Sofar Inverter Modbus diagnostics.""" + +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, + 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_redacts_serial_number( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + init_integration: MockConfigEntry, +) -> None: + """Test the serial number is redacted, both as a field and as raw ASCII.""" + diag = await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + + assert diag["serial_number"] == "**REDACTED**" + holding = diag["raw"]["holding"] + for address in range(0x0445, 0x044C): + assert str(address) not in holding