diff --git a/homeassistant/components/solaredge_modbus/diagnostics.py b/homeassistant/components/solaredge_modbus/diagnostics.py new file mode 100644 index 000000000000..925f12234efd --- /dev/null +++ b/homeassistant/components/solaredge_modbus/diagnostics.py @@ -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) diff --git a/homeassistant/components/solaredge_modbus/quality_scale.yaml b/homeassistant/components/solaredge_modbus/quality_scale.yaml index 315e54139564..75a8d222258d 100644 --- a/homeassistant/components/solaredge_modbus/quality_scale.yaml +++ b/homeassistant/components/solaredge_modbus/quality_scale.yaml @@ -51,7 +51,7 @@ rules: # Gold devices: done - diagnostics: todo + diagnostics: done discovery: done discovery-update-info: done docs-data-update: todo diff --git a/tests/components/solaredge_modbus/snapshots/test_diagnostics.ambr b/tests/components/solaredge_modbus/snapshots/test_diagnostics.ambr new file mode 100644 index 000000000000..b1c0d683353c --- /dev/null +++ b/tests/components/solaredge_modbus/snapshots/test_diagnostics.ambr @@ -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([ + ]), + }) +# --- diff --git a/tests/components/solaredge_modbus/test_diagnostics.py b/tests/components/solaredge_modbus/test_diagnostics.py new file mode 100644 index 000000000000..980bdfd91550 --- /dev/null +++ b/tests/components/solaredge_modbus/test_diagnostics.py @@ -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 + )