from datetime import timedelta from typing import Any, Optional, Union import reactivex.operators as ops from homeassistant.components.sensor import (SensorDeviceClass, SensorEntity, SensorStateClass) from homeassistant.config_entries import ConfigEntry from homeassistant.const import (ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, FREQUENCY_HERTZ, PERCENTAGE, POWER_WATT, TEMP_CELSIUS) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow from reactivex import Observable from . import DOMAIN, EcoFlowEntity, HassioEcoFlowClient, select_bms from .ecoflow import (is_delta, is_delta_mini, is_delta_pro, is_power_station, is_river) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback): client: HassioEcoFlowClient = hass.data[DOMAIN][entry.entry_id] entities = [] if is_power_station(client.product): entities.extend([ CurrentEntity(client, client.inverter, "ac_in_current", "AC input current"), CurrentEntity(client, client.inverter, "ac_out_current", "AC output current"), EnergyEntity(client, client.pd, "mppt_in_energy", "MPPT input energy"), EnergySumEntity(client, "in_energy", [ "ac", "car", "mppt"], "Total input energy"), EnergySumEntity(client, "out_energy", [ "ac", "car"], "Total output energy"), FanEntity(client, client.inverter, "fan_state", "Fan"), FrequencyEntity(client, client.inverter, "ac_in_freq", "AC input frequency"), FrequencyEntity(client, client.inverter, "ac_out_freq", "AC output frequency"), RemainEntity(client, client.pd, "remain_display", "Remain"), LevelEntity(client, client.pd, "battery_level", "Battery"), VoltageEntity(client, client.inverter, "ac_in_voltage", "AC input voltage"), VoltageEntity(client, client.inverter, "ac_out_voltage", "AC output voltage"), WattsEntity(client, client.pd, "in_power", "Total input"), WattsEntity(client, client.pd, "out_power", "Total output"), WattsEntity(client, client.inverter, "ac_consumption", "AC output + loss", real=True), WattsEntity(client, client.inverter, "ac_out_power", "AC output", real=False), WattsEntity(client, client.pd, "usb_out1_power", "USB-A left output"), WattsEntity(client, client.pd, "usb_out2_power", "USB-A right output"), ]) if is_delta(client.product): bms = ( client.bms.pipe(select_bms(0), ops.share()), client.bms.pipe(select_bms(1), ops.share()), client.bms.pipe(select_bms(2), ops.share()), ) entities.extend([ CurrentEntity(client, client.mppt, "dc_in_current", "DC input current"), CyclesEntity( client, bms[0], "battery_cycles", "Main battery cycles", 0), RemainEntity(client, client.ems, "battery_remain_charge", "Remain charge"), RemainEntity(client, client.ems, "battery_remain_discharge", "Remain discharge"), SingleLevelEntity( client, bms[0], "battery_level_f32", "Main battery", 0), TempEntity(client, client.inverter, "ac_out_temp", "AC temperature"), TempEntity(client, bms[0], "battery_temp", "Main battery temperature", 0), TempEntity(client, client.mppt, "dc_in_temp", "DC input temperature"), TempEntity(client, client.mppt, "dc24_temp", "DC output temperature"), TempEntity(client, client.pd, "typec_out1_temp", "USB-C left temperature"), TempEntity(client, client.pd, "typec_out2_temp", "USB-C right temperature"), VoltageEntity(client, client.mppt, "dc_in_voltage", "DC input voltage"), WattsEntity(client, client.inverter, "ac_in_power", "AC input"), WattsEntity(client, client.mppt, "dc_in_power", "DC input", real=True), WattsEntity(client, client.mppt, "car_consumption", "Car output + loss", real=True), WattsEntity(client, client.mppt, "car_out_power", "Car output"), ]) if is_delta_mini(client.product): entities.extend([ WattsEntity(client, client.pd, "usbqc_out1_power", "USB-Fast output"), WattsEntity(client, client.pd, "typec_out1_power", "USB-C output"), ]) else: entities.extend([ CyclesEntity( client, bms[1], "battery_cycles", "Extra1 battery cycles", 1), CyclesEntity( client, bms[2], "battery_cycles", "Extra2 battery cycles", 2), SingleLevelEntity( client, bms[1], "battery_level_f32", "Extra1 battery", 1), SingleLevelEntity( client, bms[2], "battery_level_f32", "Extra2 battery", 2), TempEntity(client, bms[1], "battery_temp", "Extra1 battery temperature", 1), TempEntity(client, bms[2], "battery_temp", "Extra2 battery temperature", 2), WattsEntity(client, client.pd, "usbqc_out1_power", "USB-Fast left output"), WattsEntity(client, client.pd, "usbqc_out2_power", "USB-Fast right output"), WattsEntity(client, client.pd, "typec_out1_power", "USB-C left output"), WattsEntity(client, client.pd, "typec_out2_power", "USB-C right output"), ]) if is_delta_pro(client.product): entities.extend([ WattsEntity(client, client.mppt, "anderson_out_power", "Anderson output"), ]) if is_river(client.product): extra = client.bms.pipe(select_bms(1), ops.share()) entities.extend([ CurrentEntity(client, client.inverter, "dc_in_current", "DC input current"), CyclesEntity(client, client.ems, "battery_cycles", "Main battery cycles"), CyclesEntity(client, extra, "battery_cycles", "Extra battery cycles", 1), SingleLevelEntity(client, client.ems, "battery_main_level", "Main battery"), SingleLevelEntity( client, extra, "battery_level", "Extra battery", 1), TempEntity(client, client.inverter, "ac_in_temp", "AC input temperature"), TempEntity(client, client.inverter, "ac_out_temp", "AC output temperature"), TempEntity(client, client.ems, "battery_main_temp", "Main battery temperature"), TempEntity(client, extra, "battery_temp", "Extra battery temperature", 1), TempEntity(client, client.pd, "car_out_temp", "DC output temperature"), TempEntity(client, client.pd, "typec_out1_temp", "USB-C temperature"), VoltageEntity(client, client.inverter, "dc_in_voltage", "DC input voltage"), WattsEntity(client, client.pd, "car_out_power", "Car output"), WattsEntity(client, client.pd, "light_power", "Light output"), WattsEntity(client, client.pd, "usbqc_out1_power", "USB-Fast output"), WattsEntity(client, client.pd, "typec_out1_power", "USB-C output"), ]) async_add_entities(entities) class BaseEntity(SensorEntity, EcoFlowEntity): def _on_updated(self, data: dict[str, Any]): self._attr_native_value = data[self._key] class CurrentEntity(BaseEntity): _attr_device_class = SensorDeviceClass.CURRENT _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_native_unit_of_measurement = ELECTRIC_CURRENT_AMPERE _attr_state_class = SensorStateClass.MEASUREMENT class CyclesEntity(BaseEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_icon = "mdi:battery-heart-variant" _attr_state_class = SensorStateClass.TOTAL_INCREASING class EnergyEntity(BaseEntity): _attr_device_class = SensorDeviceClass.ENERGY _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_native_unit_of_measurement = ENERGY_WATT_HOUR _attr_state_class = SensorStateClass.TOTAL_INCREASING class EnergySumEntity(EnergyEntity): def __init__(self, client: HassioEcoFlowClient, key: str, keys: list[str], name: str): super().__init__(client, client.pd, key, name) self._suffix_len = len(key) + 1 self._keys = [f"{x}_{key}" for x in keys] def _on_updated(self, data: dict[str, Any]): values = {key[:-self._suffix_len]: data[key] for key in data if key in self._keys} self._attr_extra_state_attributes = values self._attr_native_value = sum(values.values()) class FanEntity(BaseEntity): _attr_state_class = SensorStateClass.MEASUREMENT @property def icon(self): value = self.native_value if value is None or self.native_value <= 0: return "mdi:fan-off" return "mdi:fan" class FrequencyEntity(BaseEntity): _attr_device_class = SensorDeviceClass.FREQUENCY _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_native_unit_of_measurement = FREQUENCY_HERTZ _attr_state_class = SensorStateClass.MEASUREMENT class LevelEntity(BaseEntity): _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, client: HassioEcoFlowClient, src: Observable[dict[str, Any]], key: str, name: str, bms_id: Optional[int] = None): super().__init__(client, src, key, name, bms_id) self._attr_extra_state_attributes = {} class RemainEntity(BaseEntity): _attr_device_class = SensorDeviceClass.TIMESTAMP _attr_entity_registry_enabled_default = False def _on_updated(self, data: dict[str, Any]): value: timedelta = data[self._key] if value.total_seconds() == 8639940: self._attr_native_value = None else: self._attr_native_value = utcnow() + value class SingleLevelEntity(LevelEntity): def _on_updated(self, data: dict[str, Any]): super()._on_updated(data) if "battery_capacity_remain" in data: self._attr_extra_state_attributes["capacity_remain"] = data["battery_capacity_remain"] if "battery_capacity_full" in data: self._attr_extra_state_attributes["capacity_full"] = data["battery_capacity_full"] if "battery_capacity_design" in data: self._attr_extra_state_attributes["capacity_design"] = data["battery_capacity_design"] class TempEntity(BaseEntity): _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_native_unit_of_measurement = TEMP_CELSIUS _attr_state_class = SensorStateClass.MEASUREMENT class VoltageEntity(BaseEntity): _attr_device_class = SensorDeviceClass.VOLTAGE _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT _attr_state_class = SensorStateClass.MEASUREMENT class WattsEntity(BaseEntity): _attr_device_class = SensorDeviceClass.POWER _attr_native_unit_of_measurement = POWER_WATT _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, client: HassioEcoFlowClient, src: Observable[dict[str, Any]], key: str, name: str, real: Union[bool, int] = False): super().__init__(client, src, key, name) if key.endswith("_consumption"): self._key = key[:-11] + "out_power" self._attr_entity_category = EntityCategory.DIAGNOSTIC self._real = real def _on_updated(self, data: dict[str, Any]): key = self._key[:-5] if self._real is not False and f"{key}current" in data and f"{key}voltage" in data: self._attr_native_value = ( data[f"{key}current"] * data[f"{key}voltage"]) if self._real is not True: self._attr_native_value = round( self._attr_native_value, self._real) if self._real == 0: self._attr_native_value = int(self._attr_native_value) else: super()._on_updated(data)