Add automatic optimizer string sensors

This commit is contained in:
2026-08-10 12:31:07 +02:00
parent 02bb4a831e
commit ec6ec30dff
9 changed files with 216 additions and 14 deletions
@@ -78,6 +78,27 @@ AREA_SENSOR_DESCRIPTIONS = (
),
)
STRING_SENSOR_DESCRIPTIONS = (
SolarEdgeOptimizerSensorDescription(
key="daily_energy",
translation_key="string_daily_energy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
value_fn=lambda optimizer: optimizer.daily_energy_wh,
),
SolarEdgeOptimizerSensorDescription(
key="current_power",
translation_key="string_current_power",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda optimizer: optimizer.current_power_w,
),
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -96,25 +117,44 @@ async def async_setup_entry(
async_add_entities(area_entities)
known_serials: set[str] = set()
known_string_ids: set[str] = set()
@callback
def async_add_new_optimizers() -> None:
def async_add_new_entities() -> None:
entities: list[SensorEntity] = []
new_optimizers = [
optimizer
for optimizer in coordinator.data.optimizers
if optimizer.serial not in known_serials
]
if not new_optimizers:
return
known_serials.update(optimizer.serial for optimizer in new_optimizers)
async_add_entities(
entities.extend(
SolarEdgeOptimizerSensor(coordinator, optimizer.serial, description)
for optimizer in new_optimizers
for description in SENSOR_DESCRIPTIONS
)
async_add_new_optimizers()
entry.async_on_unload(coordinator.async_add_listener(async_add_new_optimizers))
new_string_ids = sorted(
{
optimizer.string_id
for optimizer in coordinator.data.optimizers
if optimizer.string_id is not None
}
- known_string_ids
)
known_string_ids.update(new_string_ids)
entities.extend(
SolarEdgeStringSensor(coordinator, string_id, description)
for string_id in new_string_ids
for description in STRING_SENSOR_DESCRIPTIONS
)
if entities:
async_add_entities(entities)
_remove_stale_string_entities(hass, entry)
async_add_new_entities()
entry.async_on_unload(coordinator.async_add_listener(async_add_new_entities))
def _remove_stale_area_entities(
@@ -134,6 +174,31 @@ def _remove_stale_area_entities(
entity_registry.async_remove(entity.entity_id)
def _remove_stale_string_entities(
hass: HomeAssistant,
entry: SolarEdgeOptimizerConfigEntry,
) -> None:
"""Remove registry entries for strings no longer returned by the App."""
entity_registry = er.async_get(hass)
site_id = entry.runtime_data.data.site_id
desired_unique_ids = {
f"{site_id}_string_{slugify(string_id)}_{description.key}"
for string_id in {
optimizer.string_id
for optimizer in entry.runtime_data.data.optimizers
if optimizer.string_id is not None
}
for description in STRING_SENSOR_DESCRIPTIONS
}
string_prefix = f"{site_id}_string_"
for entity in er.async_entries_for_config_entry(entity_registry, entry.entry_id):
if (
entity.unique_id.startswith(string_prefix)
and entity.unique_id not in desired_unique_ids
):
entity_registry.async_remove(entity.entity_id)
def _site_device_info(coordinator: SolarEdgeOptimizerCoordinator) -> DeviceInfo:
"""Return the shared SolarEdge site device information."""
site_id = coordinator.data.site_id
@@ -264,3 +329,75 @@ class SolarEdgeAreaSensor(
"optimizer_count": len(self._optimizer_ids),
"available_optimizer_count": sum(value is not None for value in values),
}
class SolarEdgeStringSensor(
CoordinatorEntity[SolarEdgeOptimizerCoordinator], SensorEntity
):
"""Represent the sum of one optimizer string."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: SolarEdgeOptimizerCoordinator,
string_id: str,
description: SolarEdgeOptimizerSensorDescription,
) -> None:
"""Initialize a string sum sensor."""
super().__init__(coordinator)
self.entity_description = description
self._string_id = string_id
self._attr_unique_id = (
f"{coordinator.data.site_id}_string_{slugify(string_id)}_{description.key}"
)
self._attr_translation_placeholders = {"string_id": string_id}
@property
def _optimizers(self) -> tuple[OptimizerData, ...]:
return tuple(
optimizer
for optimizer in self.coordinator.data.optimizers
if optimizer.string_id == self._string_id
)
@property
def _values(self) -> tuple[OptimizerValue, ...]:
return tuple(
self.entity_description.value_fn(optimizer)
for optimizer in self._optimizers
)
@property
def native_value(self) -> OptimizerValue:
"""Return the sum when every optimizer value is available."""
values = self._values
if not values or any(value is None for value in values):
return None
return sum(value for value in values if value is not None)
@property
def available(self) -> bool:
"""Return whether every optimizer in the string is available."""
return super().available and self.native_value is not None
@property
def device_info(self) -> DeviceInfo:
"""Return the shared SolarEdge site device information."""
return _site_device_info(self.coordinator)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return string membership and completeness information."""
optimizers = self._optimizers
values = self._values
return {
"string_id": self._string_id,
"optimizer_ids": [
optimizer.optimizer_id
for optimizer in optimizers
if optimizer.optimizer_id is not None
],
"optimizer_count": len(optimizers),
"available_optimizer_count": sum(value is not None for value in values),
}