"""Sensors for SolarEdge Optimizers.""" from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from typing import Any from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.const import UnitOfEnergy, UnitOfPower from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import SolarEdgeOptimizerConfigEntry from .api import OptimizerData from .const import DOMAIN from .coordinator import SolarEdgeOptimizerCoordinator type OptimizerValue = float | None @dataclass(frozen=True, kw_only=True) class SolarEdgeOptimizerSensorDescription(SensorEntityDescription): """Describe an optimizer sensor.""" value_fn: Callable[[OptimizerData], OptimizerValue] SENSOR_DESCRIPTIONS = ( SolarEdgeOptimizerSensorDescription( key="daily_energy", translation_key="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="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: Any, entry: SolarEdgeOptimizerConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up optimizer sensors from a config entry.""" coordinator = entry.runtime_data known_serials: set[str] = set() @callback def async_add_new_optimizers() -> None: 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( 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)) class SolarEdgeOptimizerSensor( CoordinatorEntity[SolarEdgeOptimizerCoordinator], SensorEntity ): """Represent one optimizer reading.""" _attr_has_entity_name = True def __init__( self, coordinator: SolarEdgeOptimizerCoordinator, serial: str, description: SolarEdgeOptimizerSensorDescription, ) -> None: """Initialize a sensor.""" super().__init__(coordinator) self.entity_description = description self._serial = serial self._attr_unique_id = f"{coordinator.data.site_id}_{serial}_{description.key}" @property def _optimizer(self) -> OptimizerData | None: return self.coordinator.data.optimizers_by_serial.get(self._serial) @property def native_value(self) -> OptimizerValue: """Return the current sensor value.""" if (optimizer := self._optimizer) is None: return None return self.entity_description.value_fn(optimizer) @property def available(self) -> bool: """Return whether this individual reading is available.""" return super().available and self.native_value is not None @property def device_info(self) -> DeviceInfo: """Return optimizer device information.""" optimizer = self._optimizer optimizer_id = optimizer.optimizer_id if optimizer else None return DeviceInfo( identifiers={(DOMAIN, f"{self.coordinator.data.site_id}_{self._serial}")}, manufacturer="SolarEdge", model="Power Optimizer", name=f"Optimizer {optimizer_id or self._serial}", serial_number=self._serial, ) @property def extra_state_attributes(self) -> dict[str, Any]: """Return stable optimizer metadata.""" if (optimizer := self._optimizer) is None: return {} return { "optimizer_id": optimizer.optimizer_id, "string_id": optimizer.string_id, "inverter_id": optimizer.inverter_id, "last_measurement": optimizer.last_measurement, }