Sofar split devices using via devices (#180321)

This commit is contained in:
darkrain-nl
2026-08-26 16:07:56 -04:00
committed by GitHub
parent 161d0b0dca
commit 077f5d883b
11 changed files with 1506 additions and 4463 deletions
+6 -1
View File
@@ -9,6 +9,7 @@ from homeassistant.components.modbus import async_get_unit
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers import device_registry as dr
from .const import CONF_UNIT_ID, DOMAIN, SCAN_INTERVAL, SETTINGS_SCAN_INTERVAL
from .coordinator import SofarConfigEntry, SofarDataUpdateCoordinator, SofarRuntimeData
@@ -59,7 +60,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: SofarConfigEntry) -> boo
await readings.async_config_entry_first_refresh()
await settings.async_refresh()
entry.runtime_data = SofarRuntimeData(readings, settings)
# Up front: a part's device must name an inverter that has an id.
inverter = dr.async_get(hass).async_get_or_create(
config_entry_id=entry.entry_id, **readings.device_info
)
entry.runtime_data = SofarRuntimeData(readings, settings, inverter.id)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -150,6 +150,7 @@ class SofarRuntimeData:
readings: SofarDataUpdateCoordinator
settings: SofarDataUpdateCoordinator
inverter_device_id: str
@property
def served_components(self) -> frozenset[str]:
+21 -1
View File
@@ -3,17 +3,23 @@
from dataclasses import dataclass
from typing import override
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import SofarDataUpdateCoordinator, SofarRuntimeData
# A part of the inverter, as (device translation key, number).
type SofarPart = tuple[str, int]
@dataclass(frozen=True, kw_only=True)
class SofarEntityDescription(EntityDescription):
"""Describe a Sofar entity."""
component: str # attribute name on SofarInverter, e.g. 'grid', 'pv_1_2'
part: SofarPart | None = None # None keeps the entity on the inverter
class SofarEntity(CoordinatorEntity[SofarDataUpdateCoordinator]):
@@ -33,7 +39,21 @@ class SofarEntity(CoordinatorEntity[SofarDataUpdateCoordinator]):
serial = self.coordinator.device.serial_number
assert serial is not None
self._attr_unique_id = f"{serial}_{entity_description.key}"
self._attr_device_info = self.coordinator.device_info
self._attr_device_info = self._device_info(runtime_data, serial)
def _device_info(
self, runtime_data: SofarRuntimeData, serial: str
) -> dr.DeviceInfo:
"""Place the entity on the inverter, or on the part it measures."""
if (part := self.entity_description.part) is None:
return self.coordinator.device_info
kind, number = part
return dr.DeviceInfo(
identifiers={(DOMAIN, f"{serial}_{kind}_{number}")},
via_device_id=runtime_data.inverter_device_id,
translation_key=kind,
translation_placeholders={"number": str(number)},
)
@property
@override
+3 -24
View File
@@ -16,32 +16,11 @@
"bat_config_tempco": {
"default": "mdi:battery-check-outline"
},
"battery_state_of_health_1": {
"default": "mdi:battery-heart"
},
"battery_state_of_health_2": {
"default": "mdi:battery-heart"
},
"battery_state_of_health_3": {
"default": "mdi:battery-heart"
},
"battery_state_of_health_4": {
"default": "mdi:battery-heart"
},
"battery_state_of_health_5": {
"default": "mdi:battery-heart"
},
"battery_state_of_health_6": {
"default": "mdi:battery-heart"
},
"battery_state_of_health_7": {
"default": "mdi:battery-heart"
},
"battery_state_of_health_8": {
"default": "mdi:battery-heart"
},
"battery_state_of_health_total": {
"default": "mdi:battery-heart"
},
"state_of_health": {
"default": "mdi:battery-heart"
}
}
}
@@ -63,9 +63,7 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: One fixed device per config entry; nothing is discovered dynamically at runtime.
dynamic-devices: done
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
@@ -74,9 +72,7 @@ rules:
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices:
status: exempt
comment: One fixed device per config entry; nothing to detect as stale.
stale-devices: todo
# Platinum
async-dependency: done
+187 -755
View File
@@ -1,11 +1,13 @@
"""Support for Sofar sensors."""
from collections.abc import Callable
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from enum import IntEnum
from typing import cast, override
from sofar_modbus.modern.device import SofarInverter
from homeassistant.components.sensor import (
RestoreSensor,
SensorDeviceClass,
@@ -26,7 +28,7 @@ from homeassistant.const import (
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from homeassistant.util.variance import ignore_variance
@@ -58,13 +60,53 @@ async def async_setup_entry(
"""Set up the Sofar Inverter Modbus sensor platform."""
runtime_data = entry.runtime_data
served = runtime_data.served_components
device = runtime_data.readings.device
entities: list[SensorEntity] = [
async_add_entities(
_sensor_class(description)(runtime_data, description)
for description in SENSOR_DESCRIPTIONS
if description.component in served
]
async_add_entities(entities)
if description.component in served and not _is_battery_pack(description)
)
wired: set[int] = set()
@callback
def _async_add_wired_packs() -> None:
"""Add a pack's sensors the first time it reports a voltage."""
new = {
number
for number in _BATTERY_COMPONENTS
if number not in wired and _pack_is_wired(device, served, number)
}
if not new:
return
wired.update(new)
async_add_entities(
_sensor_class(description)(runtime_data, description)
for description in SENSOR_DESCRIPTIONS
if (part := description.part) is not None
and part[0] == "battery"
and part[1] in new
)
_async_add_wired_packs()
entry.async_on_unload(
runtime_data.readings.async_add_listener(_async_add_wired_packs)
)
def _is_battery_pack(description: SofarSensorDescription) -> bool:
"""Whether a description belongs to one numbered battery pack."""
return description.part is not None and description.part[0] == "battery"
def _pack_is_wired(device: SofarInverter, served: frozenset[str], number: int) -> bool:
"""Whether a pack has answered, so it physically exists."""
component_name = _BATTERY_COMPONENTS[number]
if component_name not in served:
return False
component = getattr(device, component_name)
return bool(getattr(component, f"battery_voltage_{number}", None))
def _sensor_class(
@@ -162,25 +204,142 @@ class SofarSensorDescription(SensorEntityDescription, SofarEntityDescription):
"""Describe a Sofar sensor."""
@dataclass(frozen=True, kw_only=True)
class _PartMeasurement:
"""One measurement every string or pack repeats, before it gets a number."""
key: str
translation_key: str
device_class: SensorDeviceClass | None = None
native_unit_of_measurement: str | None = None
state_class: SensorStateClass | None = None
suggested_display_precision: int | None = None
entity_category: EntityCategory | None = None
entity_registry_enabled_default: bool = True
# Which register block each string or pack is read from.
_PV_STRING_COMPONENTS = {
1: "pv_1_2",
2: "pv_1_2",
3: "pv_3",
4: "pv_4",
5: "pv_5_6",
6: "pv_5_6",
7: "pv_7_8",
8: "pv_7_8",
9: "pv_9_10",
10: "pv_9_10",
}
_BATTERY_COMPONENTS = {
n: "battery_1_2" if n <= 2 else "battery_3_8" for n in range(1, 9)
}
_PV_STRING_MEASUREMENTS = (
_PartMeasurement(
key="pv_voltage",
translation_key="voltage",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
_PartMeasurement(
key="pv_current",
translation_key="current",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
_PartMeasurement(
key="pv_power",
translation_key="power",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
)
_BATTERY_MEASUREMENTS = (
_PartMeasurement(
key="battery_voltage",
translation_key="voltage",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
),
_PartMeasurement(
key="battery_current",
translation_key="current",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
),
_PartMeasurement(
key="battery_power",
translation_key="power",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
_PartMeasurement(
key="battery_temperature",
translation_key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
_PartMeasurement(
key="battery_capacity",
translation_key="state_of_charge",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
_PartMeasurement(
key="battery_state_of_health",
translation_key="state_of_health",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
_PartMeasurement(
key="battery_charge_cycle",
translation_key="charge_cycle",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
def _part_sensors(
kind: str,
components: Mapping[int, str],
measurements: tuple[_PartMeasurement, ...],
) -> tuple[SofarSensorDescription, ...]:
"""Repeat a part's measurements across every string or pack it has."""
return tuple(
SofarSensorDescription(
key=f"{measurement.key}_{number}",
component=component,
translation_key=measurement.translation_key,
part=(kind, number),
device_class=measurement.device_class,
native_unit_of_measurement=measurement.native_unit_of_measurement,
state_class=measurement.state_class,
suggested_display_precision=measurement.suggested_display_precision,
entity_category=measurement.entity_category,
entity_registry_enabled_default=(
measurement.entity_registry_enabled_default
),
)
for number, component in components.items()
for measurement in measurements
)
SENSOR_DESCRIPTIONS: tuple[SofarSensorDescription, ...] = (
SofarSensorDescription(
key="pv_power_1",
component="pv_1_2",
translation_key="pv_power_1",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SofarSensorDescription(
key="pv_power_2",
component="pv_1_2",
translation_key="pv_power_2",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SofarSensorDescription(
key="pv_power_total",
component="pv_1_2",
@@ -1026,737 +1185,6 @@ SENSOR_DESCRIPTIONS: tuple[SofarSensorDescription, ...] = (
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_voltage_1",
component="pv_1_2",
translation_key="pv_voltage_1",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_current_1",
component="pv_1_2",
translation_key="pv_current_1",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_voltage_2",
component="pv_1_2",
translation_key="pv_voltage_2",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_current_2",
component="pv_1_2",
translation_key="pv_current_2",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_voltage_3",
component="pv_3",
translation_key="pv_voltage_3",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_current_3",
component="pv_3",
translation_key="pv_current_3",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_power_3",
component="pv_3",
translation_key="pv_power_3",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SofarSensorDescription(
key="pv_voltage_4",
component="pv_4",
translation_key="pv_voltage_4",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_current_4",
component="pv_4",
translation_key="pv_current_4",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_power_4",
component="pv_4",
translation_key="pv_power_4",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SofarSensorDescription(
key="pv_voltage_5",
component="pv_5_6",
translation_key="pv_voltage_5",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_current_5",
component="pv_5_6",
translation_key="pv_current_5",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_power_5",
component="pv_5_6",
translation_key="pv_power_5",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SofarSensorDescription(
key="pv_voltage_6",
component="pv_5_6",
translation_key="pv_voltage_6",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_current_6",
component="pv_5_6",
translation_key="pv_current_6",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_power_6",
component="pv_5_6",
translation_key="pv_power_6",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SofarSensorDescription(
key="pv_voltage_7",
component="pv_7_8",
translation_key="pv_voltage_7",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_current_7",
component="pv_7_8",
translation_key="pv_current_7",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_power_7",
component="pv_7_8",
translation_key="pv_power_7",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SofarSensorDescription(
key="pv_voltage_8",
component="pv_7_8",
translation_key="pv_voltage_8",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_current_8",
component="pv_7_8",
translation_key="pv_current_8",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_power_8",
component="pv_7_8",
translation_key="pv_power_8",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SofarSensorDescription(
key="pv_voltage_9",
component="pv_9_10",
translation_key="pv_voltage_9",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_current_9",
component="pv_9_10",
translation_key="pv_current_9",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_power_9",
component="pv_9_10",
translation_key="pv_power_9",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SofarSensorDescription(
key="pv_voltage_10",
component="pv_9_10",
translation_key="pv_voltage_10",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_current_10",
component="pv_9_10",
translation_key="pv_current_10",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="pv_power_10",
component="pv_9_10",
translation_key="pv_power_10",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_voltage_1",
component="battery_1_2",
translation_key="battery_voltage_1",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
),
SofarSensorDescription(
key="battery_current_1",
component="battery_1_2",
translation_key="battery_current_1",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_power_1",
component="battery_1_2",
translation_key="battery_power_1",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_temperature_1",
component="battery_1_2",
translation_key="battery_temperature_1",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
SofarSensorDescription(
key="battery_capacity_1",
component="battery_1_2",
translation_key="battery_state_of_charge_1",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SofarSensorDescription(
key="battery_state_of_health_1",
component="battery_1_2",
translation_key="battery_state_of_health_1",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
SofarSensorDescription(
key="battery_charge_cycle_1",
component="battery_1_2",
translation_key="battery_charge_cycle_1",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
SofarSensorDescription(
key="battery_voltage_2",
component="battery_1_2",
translation_key="battery_voltage_2",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_current_2",
component="battery_1_2",
translation_key="battery_current_2",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_power_2",
component="battery_1_2",
translation_key="battery_power_2",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_temperature_2",
component="battery_1_2",
translation_key="battery_temperature_2",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_capacity_2",
component="battery_1_2",
translation_key="battery_state_of_charge_2",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_state_of_health_2",
component="battery_1_2",
translation_key="battery_state_of_health_2",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_charge_cycle_2",
component="battery_1_2",
translation_key="battery_charge_cycle_2",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_voltage_3",
component="battery_3_8",
translation_key="battery_voltage_3",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_current_3",
component="battery_3_8",
translation_key="battery_current_3",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_power_3",
component="battery_3_8",
translation_key="battery_power_3",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_temperature_3",
component="battery_3_8",
translation_key="battery_temperature_3",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_capacity_3",
component="battery_3_8",
translation_key="battery_state_of_charge_3",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_state_of_health_3",
component="battery_3_8",
translation_key="battery_state_of_health_3",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_charge_cycle_3",
component="battery_3_8",
translation_key="battery_charge_cycle_3",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_voltage_4",
component="battery_3_8",
translation_key="battery_voltage_4",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_current_4",
component="battery_3_8",
translation_key="battery_current_4",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_power_4",
component="battery_3_8",
translation_key="battery_power_4",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_temperature_4",
component="battery_3_8",
translation_key="battery_temperature_4",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_capacity_4",
component="battery_3_8",
translation_key="battery_state_of_charge_4",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_state_of_health_4",
component="battery_3_8",
translation_key="battery_state_of_health_4",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_charge_cycle_4",
component="battery_3_8",
translation_key="battery_charge_cycle_4",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_voltage_5",
component="battery_3_8",
translation_key="battery_voltage_5",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_current_5",
component="battery_3_8",
translation_key="battery_current_5",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_power_5",
component="battery_3_8",
translation_key="battery_power_5",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_temperature_5",
component="battery_3_8",
translation_key="battery_temperature_5",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_capacity_5",
component="battery_3_8",
translation_key="battery_state_of_charge_5",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_state_of_health_5",
component="battery_3_8",
translation_key="battery_state_of_health_5",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_charge_cycle_5",
component="battery_3_8",
translation_key="battery_charge_cycle_5",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_voltage_6",
component="battery_3_8",
translation_key="battery_voltage_6",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_current_6",
component="battery_3_8",
translation_key="battery_current_6",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_power_6",
component="battery_3_8",
translation_key="battery_power_6",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_temperature_6",
component="battery_3_8",
translation_key="battery_temperature_6",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_capacity_6",
component="battery_3_8",
translation_key="battery_state_of_charge_6",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_state_of_health_6",
component="battery_3_8",
translation_key="battery_state_of_health_6",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_charge_cycle_6",
component="battery_3_8",
translation_key="battery_charge_cycle_6",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_voltage_7",
component="battery_3_8",
translation_key="battery_voltage_7",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_current_7",
component="battery_3_8",
translation_key="battery_current_7",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_power_7",
component="battery_3_8",
translation_key="battery_power_7",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_temperature_7",
component="battery_3_8",
translation_key="battery_temperature_7",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_capacity_7",
component="battery_3_8",
translation_key="battery_state_of_charge_7",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_state_of_health_7",
component="battery_3_8",
translation_key="battery_state_of_health_7",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_charge_cycle_7",
component="battery_3_8",
translation_key="battery_charge_cycle_7",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_voltage_8",
component="battery_3_8",
translation_key="battery_voltage_8",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_current_8",
component="battery_3_8",
translation_key="battery_current_8",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_power_8",
component="battery_3_8",
translation_key="battery_power_8",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
suggested_display_precision=2,
),
SofarSensorDescription(
key="battery_temperature_8",
component="battery_3_8",
translation_key="battery_temperature_8",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_capacity_8",
component="battery_3_8",
translation_key="battery_state_of_charge_8",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_state_of_health_8",
component="battery_3_8",
translation_key="battery_state_of_health_8",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_charge_cycle_8",
component="battery_3_8",
translation_key="battery_charge_cycle_8",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SofarSensorDescription(
key="battery_power_total",
component="battery_totals",
@@ -2067,3 +1495,7 @@ SENSOR_DESCRIPTIONS: tuple[SofarSensorDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
),
)
SENSOR_DESCRIPTIONS += _part_sensors(
"pv_string", _PV_STRING_COMPONENTS, _PV_STRING_MEASUREMENTS
) + _part_sensors("battery", _BATTERY_COMPONENTS, _BATTERY_MEASUREMENTS)
+27 -256
View File
@@ -23,6 +23,14 @@
}
}
},
"device": {
"battery": {
"name": "Battery {number}"
},
"pv_string": {
"name": "PV string {number}"
}
},
"entity": {
"sensor": {
"active_power_load_sys": {
@@ -148,54 +156,6 @@
"bat_config_voltage_float": {
"name": "Battery config lead acid float voltage"
},
"battery_charge_cycle_1": {
"name": "Battery charge cycle 1"
},
"battery_charge_cycle_2": {
"name": "Battery charge cycle 2"
},
"battery_charge_cycle_3": {
"name": "Battery charge cycle 3"
},
"battery_charge_cycle_4": {
"name": "Battery charge cycle 4"
},
"battery_charge_cycle_5": {
"name": "Battery charge cycle 5"
},
"battery_charge_cycle_6": {
"name": "Battery charge cycle 6"
},
"battery_charge_cycle_7": {
"name": "Battery charge cycle 7"
},
"battery_charge_cycle_8": {
"name": "Battery charge cycle 8"
},
"battery_current_1": {
"name": "Battery current 1"
},
"battery_current_2": {
"name": "Battery current 2"
},
"battery_current_3": {
"name": "Battery current 3"
},
"battery_current_4": {
"name": "Battery current 4"
},
"battery_current_5": {
"name": "Battery current 5"
},
"battery_current_6": {
"name": "Battery current 6"
},
"battery_current_7": {
"name": "Battery current 7"
},
"battery_current_8": {
"name": "Battery current 8"
},
"battery_input_energy_today": {
"name": "Battery input energy today"
},
@@ -208,134 +168,20 @@
"battery_output_energy_total": {
"name": "Battery output energy total"
},
"battery_power_1": {
"name": "Battery power 1"
},
"battery_power_2": {
"name": "Battery power 2"
},
"battery_power_3": {
"name": "Battery power 3"
},
"battery_power_4": {
"name": "Battery power 4"
},
"battery_power_5": {
"name": "Battery power 5"
},
"battery_power_6": {
"name": "Battery power 6"
},
"battery_power_7": {
"name": "Battery power 7"
},
"battery_power_8": {
"name": "Battery power 8"
},
"battery_power_total": {
"name": "Battery power total"
},
"battery_state_of_charge_1": {
"name": "Battery state of charge 1"
},
"battery_state_of_charge_2": {
"name": "Battery state of charge 2"
},
"battery_state_of_charge_3": {
"name": "Battery state of charge 3"
},
"battery_state_of_charge_4": {
"name": "Battery state of charge 4"
},
"battery_state_of_charge_5": {
"name": "Battery state of charge 5"
},
"battery_state_of_charge_6": {
"name": "Battery state of charge 6"
},
"battery_state_of_charge_7": {
"name": "Battery state of charge 7"
},
"battery_state_of_charge_8": {
"name": "Battery state of charge 8"
},
"battery_state_of_charge_total": {
"name": "Battery state of charge total"
},
"battery_state_of_health_1": {
"name": "Battery state of health 1"
},
"battery_state_of_health_2": {
"name": "Battery state of health 2"
},
"battery_state_of_health_3": {
"name": "Battery state of health 3"
},
"battery_state_of_health_4": {
"name": "Battery state of health 4"
},
"battery_state_of_health_5": {
"name": "Battery state of health 5"
},
"battery_state_of_health_6": {
"name": "Battery state of health 6"
},
"battery_state_of_health_7": {
"name": "Battery state of health 7"
},
"battery_state_of_health_8": {
"name": "Battery state of health 8"
},
"battery_state_of_health_total": {
"name": "Battery state of health total"
},
"battery_temperature_1": {
"name": "Battery temperature 1"
"charge_cycle": {
"name": "Charge cycle"
},
"battery_temperature_2": {
"name": "Battery temperature 2"
},
"battery_temperature_3": {
"name": "Battery temperature 3"
},
"battery_temperature_4": {
"name": "Battery temperature 4"
},
"battery_temperature_5": {
"name": "Battery temperature 5"
},
"battery_temperature_6": {
"name": "Battery temperature 6"
},
"battery_temperature_7": {
"name": "Battery temperature 7"
},
"battery_temperature_8": {
"name": "Battery temperature 8"
},
"battery_voltage_1": {
"name": "Battery voltage 1"
},
"battery_voltage_2": {
"name": "Battery voltage 2"
},
"battery_voltage_3": {
"name": "Battery voltage 3"
},
"battery_voltage_4": {
"name": "Battery voltage 4"
},
"battery_voltage_5": {
"name": "Battery voltage 5"
},
"battery_voltage_6": {
"name": "Battery voltage 6"
},
"battery_voltage_7": {
"name": "Battery voltage 7"
},
"battery_voltage_8": {
"name": "Battery voltage 8"
"current": {
"name": "Current"
},
"current_output_l1": {
"name": "Current output L1"
@@ -502,6 +348,9 @@
"passive_eps_wait_time": {
"name": "EPS wait time"
},
"power": {
"name": "Power"
},
"power_factor_output_l1": {
"name": "Power factor output L1"
},
@@ -520,99 +369,9 @@
"power_factor_pcc_l3": {
"name": "Power factor PCC L3"
},
"pv_current_1": {
"name": "PV current 1"
},
"pv_current_10": {
"name": "PV current 10"
},
"pv_current_2": {
"name": "PV current 2"
},
"pv_current_3": {
"name": "PV current 3"
},
"pv_current_4": {
"name": "PV current 4"
},
"pv_current_5": {
"name": "PV current 5"
},
"pv_current_6": {
"name": "PV current 6"
},
"pv_current_7": {
"name": "PV current 7"
},
"pv_current_8": {
"name": "PV current 8"
},
"pv_current_9": {
"name": "PV current 9"
},
"pv_power_1": {
"name": "PV power 1"
},
"pv_power_10": {
"name": "PV power 10"
},
"pv_power_2": {
"name": "PV power 2"
},
"pv_power_3": {
"name": "PV power 3"
},
"pv_power_4": {
"name": "PV power 4"
},
"pv_power_5": {
"name": "PV power 5"
},
"pv_power_6": {
"name": "PV power 6"
},
"pv_power_7": {
"name": "PV power 7"
},
"pv_power_8": {
"name": "PV power 8"
},
"pv_power_9": {
"name": "PV power 9"
},
"pv_power_total": {
"name": "PV power total"
},
"pv_voltage_1": {
"name": "PV voltage 1"
},
"pv_voltage_10": {
"name": "PV voltage 10"
},
"pv_voltage_2": {
"name": "PV voltage 2"
},
"pv_voltage_3": {
"name": "PV voltage 3"
},
"pv_voltage_4": {
"name": "PV voltage 4"
},
"pv_voltage_5": {
"name": "PV voltage 5"
},
"pv_voltage_6": {
"name": "PV voltage 6"
},
"pv_voltage_7": {
"name": "PV voltage 7"
},
"pv_voltage_8": {
"name": "PV voltage 8"
},
"pv_voltage_9": {
"name": "PV voltage 9"
},
"reactive_power_offgrid_total": {
"name": "Reactive power off-grid total"
},
@@ -649,6 +408,12 @@
"solar_generation_total": {
"name": "Solar generation total"
},
"state_of_charge": {
"name": "State of charge"
},
"state_of_health": {
"name": "State of health"
},
"sync_rtc_result": {
"name": "Update system time operation result",
"state": {
@@ -676,6 +441,12 @@
"waiting": "Waiting"
}
},
"temperature": {
"name": "Temperature"
},
"voltage": {
"name": "Voltage"
},
"voltage_l1": {
"name": "Voltage L1"
},
+5 -1
View File
@@ -53,5 +53,9 @@ def seed_pv_inverter(unit: MockModbusUnit, serial: str = MOCK_SERIAL) -> None:
def seed_hybrid_inverter(
unit: MockModbusUnit, serial: str = MOCK_HYBRID_SERIAL
) -> None:
"""Seed identity registers for a hybrid inverter; the rest read as zero."""
"""Seed a hybrid inverter with battery packs 1 and 3 wired, 2 absent."""
_seed_common(unit, serial)
unit.holding[0x0604] = 520 # battery_voltage_1 -> 52.0 V
unit.holding[0x0608] = 87 # battery_capacity_1 -> 87%
unit.holding[0x0612] = 515 # battery_voltage_3 -> 51.5 V
unit.holding[0x0616] = 85 # battery_capacity_3 -> 85%
File diff suppressed because it is too large Load Diff
+146 -1
View File
@@ -20,11 +20,20 @@ from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import MOCK_HW_VERSION, MOCK_SERIAL, MOCK_SW_VERSION, MOCK_USER_INPUT
from . import (
MOCK_HW_VERSION,
MOCK_HYBRID_MODEL,
MOCK_HYBRID_SERIAL,
MOCK_SERIAL,
MOCK_SW_VERSION,
MOCK_USER_INPUT,
seed_hybrid_inverter,
)
from tests.common import MockConfigEntry, async_fire_time_changed
PV_POWER_REGISTER = 0x0586
BATTERY_3_VOLTAGE_REGISTER = 0x0612
SOLAR_GENERATION_REGISTER = 0x0684
@@ -336,3 +345,139 @@ async def test_link_dying_during_the_retry_marks_sensors_unavailable(
)
assert entity_id is not None
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
async def test_pv_strings_become_their_own_devices(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
init_integration: MockConfigEntry,
) -> None:
"""Test every PV string the inverter serves gets a device of its own."""
inverter = device_registry.async_get_device_by_identifier(
(DOMAIN, MOCK_SERIAL), init_integration.entry_id
)
assert inverter is not None
strings = [
device
for device in dr.async_entries_for_config_entry(
device_registry, init_integration.entry_id
)
if device.via_device_id == inverter.id
]
# Two MPPTs on this model, so strings 3 to 10 are not served at all.
assert {device.name for device in strings} == {"PV string 1", "PV string 2"}
async def test_pv_aggregate_stays_on_the_inverter(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
init_integration: MockConfigEntry,
) -> None:
"""Test a total living in a per-string component is not moved off."""
inverter = device_registry.async_get_device_by_identifier(
(DOMAIN, MOCK_SERIAL), init_integration.entry_id
)
assert inverter is not None
# pv_power_total sits in the pv_1_2 component but measures all strings.
entity_id = entity_registry.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_pv_power_total"
)
assert entity_id is not None
assert entity_registry.async_get(entity_id).device_id == inverter.id
async def test_only_wired_battery_packs_become_devices(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test packs are counted by what answers, not by the register map."""
connection = MockModbusConnection()
seed_hybrid_inverter(connection.for_unit(1))
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=MOCK_HYBRID_SERIAL,
data=MOCK_USER_INPUT,
title=MOCK_HYBRID_MODEL,
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.sofar.async_get_unit",
side_effect=lambda hass, entry, params, unit_id: connection.for_unit(unit_id),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
inverter = device_registry.async_get_device_by_identifier(
(DOMAIN, MOCK_HYBRID_SERIAL), entry.entry_id
)
assert inverter is not None
battery_names = {
device.name
for device in dr.async_entries_for_config_entry(device_registry, entry.entry_id)
if device.via_device_id == inverter.id and device.name.startswith("Battery")
}
# The seed wires packs 1 and 3; the map allows 8, so the rest must not.
assert battery_names == {"Battery 1", "Battery 3"}
assert (
entity_registry.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, f"{MOCK_HYBRID_SERIAL}_battery_voltage_2"
)
is None
)
inverter = device_registry.async_get_device_by_identifier(
(DOMAIN, MOCK_HYBRID_SERIAL), entry.entry_id
)
assert inverter is not None
# A combined total is the inverter's, not any one pack's.
total_id = entity_registry.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, f"{MOCK_HYBRID_SERIAL}_battery_capacity_total"
)
assert total_id is not None
assert entity_registry.async_get(total_id).device_id == inverter.id
async def test_battery_pack_appears_once_its_block_answers(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
entity_registry: er.EntityRegistry,
) -> None:
"""Test a pack whose block failed at setup is added when it recovers."""
connection = MockModbusConnection()
unit = connection.for_unit(1)
seed_hybrid_inverter(unit)
# Inside the battery_3_8 block, so pack 3 cannot be seen at setup.
unit.fail_read(BATTERY_3_VOLTAGE_REGISTER, ModbusError("block busy"))
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=MOCK_HYBRID_SERIAL,
data=MOCK_USER_INPUT,
title=MOCK_HYBRID_MODEL,
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.sofar.async_get_unit",
side_effect=lambda hass, entry, params, unit_id: connection.for_unit(unit_id),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
unique_id = f"{MOCK_HYBRID_SERIAL}_battery_voltage_3"
assert entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, unique_id) is None
unit.fail_read(BATTERY_3_VOLTAGE_REGISTER, None)
freezer.tick(timedelta(seconds=SCAN_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# No reload: the coordinator's own listener notices the pack answering.
entity_id = entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, unique_id)
assert entity_id is not None
assert hass.states.get(entity_id).state == "51.5"
+2 -2
View File
@@ -103,8 +103,8 @@ async def test_sensor_entities_created_and_state(
MOCK_HYBRID_SERIAL,
MOCK_HYBRID_MODEL,
seed_hybrid_inverter,
181,
39,
139,
46,
id="hybrid",
),
],