Add diagnostics to SolarEdge Modbus (#180599)

This commit is contained in:
Franck Nijhof
2026-08-29 12:26:07 +02:00
committed by GitHub
parent c93f3d3781
commit 6a5d0472eb
4 changed files with 258 additions and 1 deletions
@@ -0,0 +1,70 @@
"""Diagnostics support for the SolarEdge Modbus integration."""
from typing import Any
from modbus_connection.model import Component, RegisterField
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
from .coordinator import (
SolarEdgeModbusConfigEntry,
SolarEdgeModbusDataUpdateCoordinator,
)
TO_REDACT = {"serial_number"}
def _component_data(component: Component) -> dict[str, Any]:
"""Every decoded register field and computed property of a component.
Only the classes below the library's own base count: its plumbing, such as
the Modbus unit a component reads through, is not device data.
"""
names = {
name
for klass in type(component).__mro__
if issubclass(klass, Component) and klass is not Component
for name, attribute in vars(klass).items()
if not name.startswith("_") and isinstance(attribute, (RegisterField, property))
}
return {name: getattr(component, name) for name in sorted(names)}
def _poll_data(coordinator: SolarEdgeModbusDataUpdateCoordinator) -> dict[str, Any]:
"""What a coordinator's most recent poll got out of the device."""
return {
"updated": sorted(coordinator.data.updated),
"failed": {
subsystem: str(error)
for subsystem, error in coordinator.data.failed.items()
},
}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: SolarEdgeModbusConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry.
What the inverter reports about itself and about what is attached to it,
whether or not this integration has entities for it yet.
"""
runtime_data = entry.runtime_data
solaredge = runtime_data.solaredge
data: dict[str, Any] = {
"polls": {"readings": _poll_data(runtime_data.readings)},
"common": _component_data(solaredge.common),
"inverter": _component_data(solaredge.inverter),
"mmppt": (
[_component_data(module) for module in solaredge.mmppt.modules]
if solaredge.mmppt is not None
else None
),
"meters": [_component_data(meter) for meter in solaredge.meters],
"batteries": [_component_data(battery) for battery in solaredge.batteries],
"unresponsive_blocks": sorted(solaredge.unresponsive_blocks),
}
return async_redact_data(data, TO_REDACT)
@@ -51,7 +51,7 @@ rules:
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery: done
discovery-update-info: done
docs-data-update: todo
@@ -0,0 +1,161 @@
# serializer version: 1
# name: test_diagnostics
dict({
'batteries': list([
dict({
'dc_current': -0.0,
'dc_power': 0.0,
'dc_voltage': 403.8467102050781,
'energy_available': 9581.759765625,
'energy_exported': 0,
'energy_imported': 0,
'energy_max': 9700.0,
'manufacturer': '',
'max_charge_peak_power': 6000.0,
'max_charge_power': 5000.0,
'max_discharge_peak_power': 6100.0,
'max_discharge_power': 5100.0,
'model': '',
'rated_energy': 9700.0,
'serial_number': '',
'state_of_energy': 99.94781494140625,
'state_of_health': 100.0,
'status': 6,
'temperature_average': 23.608806610107422,
'temperature_max': 24.5,
'version': '',
}),
dict({
'dc_current': -0.0,
'dc_power': 0.0,
'dc_voltage': 404.8029479980469,
'energy_available': 9706.5595703125,
'energy_exported': 0,
'energy_imported': 0,
'energy_max': 9700.0,
'manufacturer': '',
'max_charge_peak_power': 0.0,
'max_charge_power': 0.0,
'max_discharge_peak_power': 0.0,
'max_discharge_power': 0.0,
'model': '',
'rated_energy': 9700.0,
'serial_number': '',
'state_of_energy': 99.2170181274414,
'state_of_health': 100.0,
'status': 6,
'temperature_average': 23.861509323120117,
'temperature_max': 0.0,
'version': '',
}),
]),
'common': dict({
'device_address': 0,
'manufacturer': 'SolarEdge',
'model': 'SE10000H-AUSNBBX14',
'option': '',
'serial_number': '**REDACTED**',
'version': '',
}),
'inverter': dict({
'ac_current': 39.99,
'ac_current_a': 0.0,
'ac_current_b': 0.0,
'ac_current_c': 0.0,
'ac_energy': 18785652,
'ac_frequency': 50.037,
'ac_power': 9490,
'ac_power_factor': 98,
'ac_va': 9600,
'ac_var': 1449,
'ac_voltage_ab': 250.2,
'ac_voltage_an': 250.2,
'ac_voltage_bc': 0.0,
'ac_voltage_bn': 0.0,
'ac_voltage_ca': 0.0,
'ac_voltage_cn': 0.0,
'dc_current': 0,
'dc_power': 9635,
'dc_voltage': 0,
'did': 101,
'on_grid': True,
'status': 4,
'temperature_cabinet': 0.0,
'temperature_heatsink': 47.67,
'temperature_other': 0.0,
'temperature_transformer': 0.0,
'vendor_status': 4,
'vendor_status_extended': 0,
}),
'meters': list([
dict({
'ac_current': 30.08,
'ac_current_a': 10.0,
'ac_current_b': 10.02,
'ac_current_c': 10.06,
'ac_frequency': 50.02,
'ac_power': 5279,
'ac_power_a': 1750,
'ac_power_b': 1760,
'ac_power_c': 1769,
'ac_power_factor': 98,
'ac_power_factor_a': 0,
'ac_power_factor_b': 0,
'ac_power_factor_c': 0,
'ac_va': 5350,
'ac_va_a': 0,
'ac_va_b': 0,
'ac_va_c': 0,
'ac_var': 869,
'ac_var_a': 0,
'ac_var_b': 0,
'ac_var_c': 0,
'ac_voltage_ab': 400,
'ac_voltage_an': 231,
'ac_voltage_bc': 401,
'ac_voltage_bn': 232,
'ac_voltage_ca': 402,
'ac_voltage_cn': 233,
'ac_voltage_ll': 0,
'ac_voltage_ln': 232,
'apparent_energy_exported': 0,
'apparent_energy_imported': 0,
'did': 203,
'energy_exported': 2324624.0,
'energy_exported_a': 774000.0,
'energy_exported_b': 775000.0,
'energy_exported_c': 775624.0,
'energy_imported': 12320555.0,
'energy_imported_a': 4106000.0,
'energy_imported_b': 4107000.0,
'energy_imported_c': 4107555.0,
'events': 0,
'manufacturer': '',
'model': '',
'option': '',
'reactive_energy_q1': 0,
'reactive_energy_q2': 0,
'reactive_energy_q3': 0,
'reactive_energy_q4': 0,
'serial_number': '',
'version': '',
}),
]),
'mmppt': None,
'polls': dict({
'readings': dict({
'failed': dict({
}),
'updated': list([
'batteries[0]',
'batteries[1]',
'common',
'inverter',
'meters[0]',
]),
}),
}),
'unresponsive_blocks': list([
]),
})
# ---
@@ -0,0 +1,26 @@
"""Tests for the SolarEdge 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,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""The diagnostics dump matches the snapshot."""
mock_config_entry.add_to_hass(hass)
assert 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
)