mirror of
https://github.com/home-assistant/core.git
synced 2026-10-06 22:38:02 -04:00
Put the control blocks in the SolarEdge Modbus diagnostics (#183508)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from typing import Any
|
||||
|
||||
from modbus_connection.model import Component, RegisterField
|
||||
from solaredged import SolarEdge
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -31,6 +32,29 @@ def _component_data(component: Component) -> dict[str, Any]:
|
||||
return {name: getattr(component, name) for name in sorted(names)}
|
||||
|
||||
|
||||
def _optional_component_data(component: Component | None) -> dict[str, Any] | None:
|
||||
"""A component's data, or None for a block this device does not have."""
|
||||
if component is None:
|
||||
return None
|
||||
return _component_data(component)
|
||||
|
||||
|
||||
def _sunspec_models(solaredge: SolarEdge) -> list[dict[str, int]] | None:
|
||||
"""The device's SunSpec model chain, in the order it serves them.
|
||||
|
||||
What the inverter says it carries and where, which is the only account of
|
||||
it that survives a block not being found: everything else here describes
|
||||
what was read, not what was looked for.
|
||||
"""
|
||||
if solaredge.sunspec_models is None:
|
||||
return None
|
||||
|
||||
return [
|
||||
{"model_id": model.model_id, "address": model.address, "length": model.length}
|
||||
for model in solaredge.sunspec_models.chain
|
||||
]
|
||||
|
||||
|
||||
def _poll_data(coordinator: SolarEdgeModbusDataUpdateCoordinator) -> dict[str, Any]:
|
||||
"""What a coordinator's most recent poll got out of the device."""
|
||||
return {
|
||||
@@ -54,7 +78,10 @@ async def async_get_config_entry_diagnostics(
|
||||
solaredge = runtime_data.solaredge
|
||||
|
||||
data: dict[str, Any] = {
|
||||
"polls": {"readings": _poll_data(runtime_data.readings)},
|
||||
"polls": {
|
||||
"readings": _poll_data(runtime_data.readings),
|
||||
"settings": _poll_data(runtime_data.settings),
|
||||
},
|
||||
"common": _component_data(solaredge.common),
|
||||
"inverter": _component_data(solaredge.inverter),
|
||||
"mmppt": (
|
||||
@@ -64,6 +91,13 @@ async def async_get_config_entry_diagnostics(
|
||||
),
|
||||
"meters": [_component_data(meter) for meter in solaredge.meters],
|
||||
"batteries": [_component_data(battery) for battery in solaredge.batteries],
|
||||
"storage_control": _optional_component_data(solaredge.storage_control),
|
||||
"export_control": _optional_component_data(solaredge.export_control),
|
||||
"power_control": _optional_component_data(solaredge.power_control),
|
||||
"advanced_power_control": _optional_component_data(
|
||||
solaredge.advanced_power_control
|
||||
),
|
||||
"sunspec_models": _sunspec_models(solaredge),
|
||||
"unresponsive_blocks": sorted(solaredge.unresponsive_blocks),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# serializer version: 1
|
||||
# name: test_diagnostics
|
||||
dict({
|
||||
'advanced_power_control': dict({
|
||||
'enabled': False,
|
||||
'reactive_power_config': 0,
|
||||
}),
|
||||
'batteries': list([
|
||||
dict({
|
||||
'dc_current': -0.0,
|
||||
@@ -57,6 +61,15 @@
|
||||
'serial_number': '**REDACTED**',
|
||||
'version': '',
|
||||
}),
|
||||
'export_control': dict({
|
||||
'external_production': False,
|
||||
'external_production_max': 0.0,
|
||||
'limit_type': 0,
|
||||
'mode': None,
|
||||
'negative_site_limit': False,
|
||||
'site_limit': 0.0,
|
||||
'site_limit_raw': 0.0,
|
||||
}),
|
||||
'inverter': dict({
|
||||
'ac_current': 39.99,
|
||||
'ac_current_a': 0.0,
|
||||
@@ -154,7 +167,33 @@
|
||||
'meters[0]',
|
||||
]),
|
||||
}),
|
||||
'settings': dict({
|
||||
'failed': dict({
|
||||
}),
|
||||
'updated': list([
|
||||
'advanced_power_control',
|
||||
'power_control',
|
||||
'site_control',
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
'power_control': dict({
|
||||
'active_power_limit': 0,
|
||||
'cos_phi': 0.0,
|
||||
'rrcr_state': 0,
|
||||
}),
|
||||
'storage_control': dict({
|
||||
'ac_charge_limit': 0.0,
|
||||
'ac_charge_policy': 0,
|
||||
'backup_reserve': 2.0,
|
||||
'charge_limit': 11400.0,
|
||||
'command_mode': None,
|
||||
'command_timeout': 3600,
|
||||
'control_mode': 1,
|
||||
'default_mode': 0,
|
||||
'discharge_limit': 11400.0,
|
||||
}),
|
||||
'sunspec_models': None,
|
||||
'unresponsive_blocks': list([
|
||||
]),
|
||||
})
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
"""Tests for the SolarEdge Modbus diagnostics."""
|
||||
|
||||
from modbus_connection import IllegalDataAddressError
|
||||
from modbus_connection.mock import MockModbusUnit
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import add_storage_capacity
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
# The control blocks, each absent on a device that refuses its base address.
|
||||
CONTROL_BASES = {
|
||||
"storage_control": 57348,
|
||||
"export_control": 57344,
|
||||
"power_control": 61440,
|
||||
"advanced_power_control": 61696,
|
||||
}
|
||||
|
||||
|
||||
async def test_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
@@ -24,3 +36,55 @@ async def test_diagnostics(
|
||||
await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)
|
||||
== snapshot
|
||||
)
|
||||
|
||||
|
||||
async def test_diagnostics_without_control_blocks(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_modbus_unit: MockModbusUnit,
|
||||
) -> None:
|
||||
"""A block this device does not have is named in the dump, as null."""
|
||||
for address in CONTROL_BASES.values():
|
||||
mock_modbus_unit.fail_read(address, IllegalDataAddressError())
|
||||
|
||||
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()
|
||||
|
||||
diagnostics = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, mock_config_entry
|
||||
)
|
||||
|
||||
for name in CONTROL_BASES:
|
||||
assert diagnostics[name] is None
|
||||
|
||||
|
||||
async def test_diagnostics_reports_the_sunspec_chain(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_modbus_unit: MockModbusUnit,
|
||||
) -> None:
|
||||
"""A device that serves a model chain has it in the dump.
|
||||
|
||||
It is what explains a block that was looked for and not found, so it is
|
||||
worth having even where nothing was made of the models it names.
|
||||
"""
|
||||
add_storage_capacity(mock_modbus_unit, state_of_charge=5960)
|
||||
|
||||
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()
|
||||
|
||||
diagnostics = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, mock_config_entry
|
||||
)
|
||||
|
||||
assert [model["model_id"] for model in diagnostics["sunspec_models"]] == [
|
||||
1,
|
||||
103,
|
||||
1,
|
||||
203,
|
||||
713,
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user