"""Support for Enphase Envoy solar energy monitor.""" from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass, replace import datetime import logging from operator import attrgetter from typing import TYPE_CHECKING from pyenphase import ( EnvoyACBPower, EnvoyBatteryAggregate, EnvoyC6CC, EnvoyCollar, EnvoyEncharge, EnvoyEnchargeAggregate, EnvoyEnchargePower, EnvoyEnpower, EnvoyInverter, EnvoySystemConsumption, EnvoySystemProduction, ) from pyenphase.const import PHASENAMES from pyenphase.models.meters import ( CtMeterStatus, CtState, CtStatusFlags, CtType, EnvoyMeterData, ) from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.const import ( PERCENTAGE, EntityCategory, UnitOfApparentPower, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, UnitOfFrequency, UnitOfPower, UnitOfTemperature, UnitOfTime, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util from .const import DOMAIN from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator from .entity import EnvoyBaseEntity _LOGGER = logging.getLogger(__name__) INVERTERS_KEY = "inverters" LAST_REPORTED_KEY = "last_reported" PARALLEL_UPDATES = 0 @dataclass(frozen=True, kw_only=True) class EnvoyInverterSensorEntityDescription(SensorEntityDescription): """Describes an Envoy inverter sensor entity.""" value_fn: Callable[[EnvoyInverter], datetime.datetime | float] INVERTER_SENSORS = ( EnvoyInverterSensorEntityDescription( key=INVERTERS_KEY, name=None, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, value_fn=attrgetter("last_report_watts"), ), EnvoyInverterSensorEntityDescription( key="dc_voltage", translation_key="dc_voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.VOLTAGE, suggested_display_precision=3, entity_registry_enabled_default=False, value_fn=attrgetter("dc_voltage"), ), EnvoyInverterSensorEntityDescription( key="dc_current", translation_key="dc_current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.CURRENT, suggested_display_precision=3, entity_registry_enabled_default=False, value_fn=attrgetter("dc_current"), ), EnvoyInverterSensorEntityDescription( key="ac_voltage", translation_key="ac_voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.VOLTAGE, suggested_display_precision=3, entity_registry_enabled_default=False, value_fn=attrgetter("ac_voltage"), ), EnvoyInverterSensorEntityDescription( key="ac_current", translation_key="ac_current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.CURRENT, suggested_display_precision=3, entity_registry_enabled_default=False, value_fn=attrgetter("ac_current"), ), EnvoyInverterSensorEntityDescription( key="ac_frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.FREQUENCY, suggested_display_precision=3, entity_registry_enabled_default=False, value_fn=attrgetter("ac_frequency"), ), EnvoyInverterSensorEntityDescription( key="temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, suggested_display_precision=3, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, value_fn=attrgetter("temperature"), ), EnvoyInverterSensorEntityDescription( key="lifetime_energy", translation_key="lifetime_energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, entity_registry_enabled_default=False, value_fn=attrgetter("lifetime_energy"), ), EnvoyInverterSensorEntityDescription( key="energy_today", translation_key="energy_today", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, entity_registry_enabled_default=False, value_fn=attrgetter("energy_today"), ), EnvoyInverterSensorEntityDescription( key="last_report_duration", translation_key="last_report_duration", native_unit_of_measurement=UnitOfTime.SECONDS, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DURATION, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, value_fn=attrgetter("last_report_duration"), ), EnvoyInverterSensorEntityDescription( key="energy_produced", translation_key="energy_produced", native_unit_of_measurement=UnitOfEnergy.MILLIWATT_HOUR, state_class=SensorStateClass.TOTAL, device_class=SensorDeviceClass.ENERGY, suggested_display_precision=3, entity_registry_enabled_default=False, value_fn=attrgetter("energy_produced"), ), EnvoyInverterSensorEntityDescription( key="max_reported", translation_key="max_reported", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, value_fn=attrgetter("max_report_watts"), ), EnvoyInverterSensorEntityDescription( key=LAST_REPORTED_KEY, translation_key=LAST_REPORTED_KEY, device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, value_fn=lambda inverter: dt_util.utc_from_timestamp(inverter.last_report_date), ), ) @dataclass(frozen=True, kw_only=True) class EnvoyProductionSensorEntityDescription(SensorEntityDescription): """Describes an Envoy production sensor entity.""" value_fn: Callable[[EnvoySystemProduction], int] on_phase: str | None PRODUCTION_SENSORS = ( EnvoyProductionSensorEntityDescription( key="production", translation_key="current_power_production", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=3, value_fn=attrgetter("watts_now"), on_phase=None, ), EnvoyProductionSensorEntityDescription( key="daily_production", translation_key="daily_production", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, value_fn=attrgetter("watt_hours_today"), on_phase=None, ), EnvoyProductionSensorEntityDescription( key="seven_days_production", translation_key="seven_days_production", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=1, value_fn=attrgetter("watt_hours_last_7_days"), on_phase=None, ), EnvoyProductionSensorEntityDescription( key="lifetime_production", translation_key="lifetime_production", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, value_fn=attrgetter("watt_hours_lifetime"), on_phase=None, ), ) PRODUCTION_PHASE_SENSORS = { (on_phase := PHASENAMES[phase]): [ replace( sensor, key=f"{sensor.key}_l{phase + 1}", translation_key=f"{sensor.translation_key}_phase", entity_registry_enabled_default=False, on_phase=on_phase, translation_placeholders={"phase_name": f"l{phase + 1}"}, ) for sensor in list(PRODUCTION_SENSORS) ] for phase in range(3) } @dataclass(frozen=True, kw_only=True) class EnvoyConsumptionSensorEntityDescription(SensorEntityDescription): """Describes an Envoy consumption sensor entity.""" value_fn: Callable[[EnvoySystemConsumption], int] on_phase: str | None CONSUMPTION_SENSORS = ( EnvoyConsumptionSensorEntityDescription( key="consumption", translation_key="current_power_consumption", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=3, value_fn=attrgetter("watts_now"), on_phase=None, ), EnvoyConsumptionSensorEntityDescription( key="daily_consumption", translation_key="daily_consumption", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, value_fn=attrgetter("watt_hours_today"), on_phase=None, ), EnvoyConsumptionSensorEntityDescription( key="seven_days_consumption", translation_key="seven_days_consumption", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=1, value_fn=attrgetter("watt_hours_last_7_days"), on_phase=None, ), EnvoyConsumptionSensorEntityDescription( key="lifetime_consumption", translation_key="lifetime_consumption", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, value_fn=attrgetter("watt_hours_lifetime"), on_phase=None, ), ) CONSUMPTION_PHASE_SENSORS = { (on_phase := PHASENAMES[phase]): [ replace( sensor, key=f"{sensor.key}_l{phase + 1}", translation_key=f"{sensor.translation_key}_phase", entity_registry_enabled_default=False, on_phase=on_phase, translation_placeholders={"phase_name": f"l{phase + 1}"}, ) for sensor in list(CONSUMPTION_SENSORS) ] for phase in range(3) } NET_CONSUMPTION_SENSORS = ( EnvoyConsumptionSensorEntityDescription( key="balanced_net_consumption", translation_key="balanced_net_consumption", entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=3, value_fn=attrgetter("watts_now"), on_phase=None, ), EnvoyConsumptionSensorEntityDescription( key="lifetime_balanced_net_consumption", translation_key="lifetime_balanced_net_consumption", entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL, device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=3, value_fn=attrgetter("watt_hours_lifetime"), on_phase=None, ), ) NET_CONSUMPTION_PHASE_SENSORS = { (on_phase := PHASENAMES[phase]): [ replace( sensor, key=f"{sensor.key}_l{phase + 1}", translation_key=f"{sensor.translation_key}_phase", entity_registry_enabled_default=False, on_phase=on_phase, translation_placeholders={"phase_name": f"l{phase + 1}"}, ) for sensor in list(NET_CONSUMPTION_SENSORS) ] for phase in range(3) } @dataclass(frozen=True, kw_only=True) class EnvoyCTSensorEntityDescription(SensorEntityDescription): """Describes an Envoy CT sensor entity.""" value_fn: Callable[ [EnvoyMeterData], int | float | str | CtType | CtMeterStatus | CtStatusFlags | CtState | None, ] on_phase: str | None cttype: str | None = None CT_NET_CONSUMPTION_SENSORS = ( EnvoyCTSensorEntityDescription( key="lifetime_net_consumption", translation_key="lifetime_net_consumption", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, value_fn=attrgetter("energy_delivered"), on_phase=None, cttype=CtType.NET_CONSUMPTION, ), EnvoyCTSensorEntityDescription( key="lifetime_net_production", translation_key="lifetime_net_production", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, value_fn=attrgetter("energy_received"), on_phase=None, cttype=CtType.NET_CONSUMPTION, ), EnvoyCTSensorEntityDescription( key="net_consumption", translation_key="net_consumption", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=3, value_fn=attrgetter("active_power"), on_phase=None, cttype=CtType.NET_CONSUMPTION, ), EnvoyCTSensorEntityDescription( key="frequency", translation_key="net_ct_frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.FREQUENCY, suggested_display_precision=1, entity_registry_enabled_default=False, value_fn=attrgetter("frequency"), on_phase=None, cttype=CtType.NET_CONSUMPTION, ), EnvoyCTSensorEntityDescription( key="voltage", translation_key="net_ct_voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.VOLTAGE, suggested_unit_of_measurement=UnitOfElectricPotential.VOLT, suggested_display_precision=1, entity_registry_enabled_default=False, value_fn=attrgetter("voltage"), on_phase=None, cttype=CtType.NET_CONSUMPTION, ), EnvoyCTSensorEntityDescription( key="net_ct_current", translation_key="net_ct_current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.CURRENT, suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE, suggested_display_precision=3, entity_registry_enabled_default=False, value_fn=attrgetter("current"), on_phase=None, cttype=CtType.NET_CONSUMPTION, ), EnvoyCTSensorEntityDescription( key="net_ct_powerfactor", translation_key="net_ct_powerfactor", device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=2, entity_registry_enabled_default=False, value_fn=attrgetter("power_factor"), on_phase=None, cttype=CtType.NET_CONSUMPTION, ), EnvoyCTSensorEntityDescription( key="net_consumption_ct_metering_status", translation_key="net_ct_metering_status", device_class=SensorDeviceClass.ENUM, entity_category=EntityCategory.DIAGNOSTIC, options=list(CtMeterStatus), entity_registry_enabled_default=False, value_fn=attrgetter("metering_status"), on_phase=None, cttype=CtType.NET_CONSUMPTION, ), EnvoyCTSensorEntityDescription( key="net_consumption_ct_status_flags", translation_key="net_ct_status_flags", state_class=None, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags), on_phase=None, cttype=CtType.NET_CONSUMPTION, ), ) CT_NET_CONSUMPTION_PHASE_SENSORS = { (on_phase := PHASENAMES[phase]): [ replace( sensor, key=f"{sensor.key}_l{phase + 1}", translation_key=f"{sensor.translation_key}_phase", entity_registry_enabled_default=False, on_phase=on_phase, translation_placeholders={"phase_name": f"l{phase + 1}"}, ) for sensor in list(CT_NET_CONSUMPTION_SENSORS) ] for phase in range(3) } CT_PRODUCTION_SENSORS = ( EnvoyCTSensorEntityDescription( key="production_ct_frequency", translation_key="production_ct_frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.FREQUENCY, suggested_display_precision=1, entity_registry_enabled_default=False, value_fn=attrgetter("frequency"), on_phase=None, cttype=CtType.PRODUCTION, ), EnvoyCTSensorEntityDescription( key="production_ct_voltage", translation_key="production_ct_voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.VOLTAGE, suggested_unit_of_measurement=UnitOfElectricPotential.VOLT, suggested_display_precision=1, entity_registry_enabled_default=False, value_fn=attrgetter("voltage"), on_phase=None, cttype=CtType.PRODUCTION, ), EnvoyCTSensorEntityDescription( key="production_ct_current", translation_key="production_ct_current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.CURRENT, suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE, suggested_display_precision=3, entity_registry_enabled_default=False, value_fn=attrgetter("current"), on_phase=None, cttype=CtType.PRODUCTION, ), EnvoyCTSensorEntityDescription( key="production_ct_powerfactor", translation_key="production_ct_powerfactor", device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=2, entity_registry_enabled_default=False, value_fn=attrgetter("power_factor"), on_phase=None, cttype=CtType.PRODUCTION, ), EnvoyCTSensorEntityDescription( key="production_ct_metering_status", translation_key="production_ct_metering_status", device_class=SensorDeviceClass.ENUM, options=list(CtMeterStatus), entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=attrgetter("metering_status"), on_phase=None, cttype=CtType.PRODUCTION, ), EnvoyCTSensorEntityDescription( key="production_ct_status_flags", translation_key="production_ct_status_flags", state_class=None, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags), on_phase=None, cttype=CtType.PRODUCTION, ), ) CT_PRODUCTION_PHASE_SENSORS = { (on_phase := PHASENAMES[phase]): [ replace( sensor, key=f"{sensor.key}_l{phase + 1}", translation_key=f"{sensor.translation_key}_phase", entity_registry_enabled_default=False, on_phase=on_phase, translation_placeholders={"phase_name": f"l{phase + 1}"}, ) for sensor in list(CT_PRODUCTION_SENSORS) ] for phase in range(3) } CT_STORAGE_SENSORS = ( EnvoyCTSensorEntityDescription( key="lifetime_battery_discharged", translation_key="lifetime_battery_discharged", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, value_fn=attrgetter("energy_delivered"), on_phase=None, cttype=CtType.STORAGE, ), EnvoyCTSensorEntityDescription( key="lifetime_battery_charged", translation_key="lifetime_battery_charged", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, value_fn=attrgetter("energy_received"), on_phase=None, cttype=CtType.STORAGE, ), EnvoyCTSensorEntityDescription( key="battery_discharge", translation_key="battery_discharge", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=3, value_fn=attrgetter("active_power"), on_phase=None, cttype=CtType.STORAGE, ), EnvoyCTSensorEntityDescription( key="storage_ct_frequency", translation_key="storage_ct_frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.FREQUENCY, suggested_display_precision=1, entity_registry_enabled_default=False, value_fn=attrgetter("frequency"), on_phase=None, cttype=CtType.STORAGE, ), EnvoyCTSensorEntityDescription( key="storage_voltage", translation_key="storage_ct_voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.VOLTAGE, suggested_unit_of_measurement=UnitOfElectricPotential.VOLT, suggested_display_precision=1, entity_registry_enabled_default=False, value_fn=attrgetter("voltage"), on_phase=None, cttype=CtType.STORAGE, ), EnvoyCTSensorEntityDescription( key="storage_ct_current", translation_key="storage_ct_current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.CURRENT, suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE, suggested_display_precision=3, entity_registry_enabled_default=False, value_fn=attrgetter("current"), on_phase=None, cttype=CtType.STORAGE, ), EnvoyCTSensorEntityDescription( key="storage_ct_powerfactor", translation_key="storage_ct_powerfactor", device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=2, entity_registry_enabled_default=False, value_fn=attrgetter("power_factor"), on_phase=None, cttype=CtType.STORAGE, ), EnvoyCTSensorEntityDescription( key="storage_ct_metering_status", translation_key="storage_ct_metering_status", device_class=SensorDeviceClass.ENUM, options=list(CtMeterStatus), entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=attrgetter("metering_status"), on_phase=None, cttype=CtType.STORAGE, ), EnvoyCTSensorEntityDescription( key="storage_ct_status_flags", translation_key="storage_ct_status_flags", state_class=None, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags), on_phase=None, cttype=CtType.STORAGE, ), ) CT_STORAGE_PHASE_SENSORS = { (on_phase := PHASENAMES[phase]): [ replace( sensor, key=f"{sensor.key}_l{phase + 1}", translation_key=f"{sensor.translation_key}_phase", entity_registry_enabled_default=False, on_phase=on_phase, translation_placeholders={"phase_name": f"l{phase + 1}"}, ) for sensor in list(CT_STORAGE_SENSORS) ] for phase in range(3) } @dataclass(frozen=True, kw_only=True) class EnvoyEnchargeSensorEntityDescription(SensorEntityDescription): """Describes an Envoy Encharge sensor entity.""" value_fn: Callable[[EnvoyEncharge], datetime.datetime | int | float] @dataclass(frozen=True) class EnvoyEnchargePowerRequiredKeysMixin: """Mixin for required keys.""" @dataclass(frozen=True, kw_only=True) class EnvoyEnchargePowerSensorEntityDescription(SensorEntityDescription): """Describes an Envoy Encharge sensor entity.""" value_fn: Callable[[EnvoyEnchargePower], int | float] ENCHARGE_INVENTORY_SENSORS = ( EnvoyEnchargeSensorEntityDescription( key="temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, value_fn=attrgetter("temperature"), ), EnvoyEnchargeSensorEntityDescription( key=LAST_REPORTED_KEY, translation_key=LAST_REPORTED_KEY, native_unit_of_measurement=None, device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda encharge: dt_util.utc_from_timestamp(encharge.last_report_date), ), ) ENCHARGE_POWER_SENSORS = ( EnvoyEnchargePowerSensorEntityDescription( key="soc", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, value_fn=attrgetter("soc"), ), EnvoyEnchargePowerSensorEntityDescription( key="apparent_power_mva", native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, device_class=SensorDeviceClass.APPARENT_POWER, value_fn=lambda encharge: encharge.apparent_power_mva * 0.001, ), EnvoyEnchargePowerSensorEntityDescription( key="real_power_mw", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, value_fn=lambda encharge: encharge.real_power_mw * 0.001, ), ) @dataclass(frozen=True, kw_only=True) class EnvoyEnpowerSensorEntityDescription(SensorEntityDescription): """Describes an Envoy Encharge sensor entity.""" value_fn: Callable[[EnvoyEnpower], datetime.datetime | int | float] ENPOWER_SENSORS = ( EnvoyEnpowerSensorEntityDescription( key="temperature", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, value_fn=attrgetter("temperature"), ), EnvoyEnpowerSensorEntityDescription( key=LAST_REPORTED_KEY, translation_key=LAST_REPORTED_KEY, device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda enpower: dt_util.utc_from_timestamp(enpower.last_report_date), ), ) @dataclass(frozen=True, kw_only=True) class EnvoyCollarSensorEntityDescription(SensorEntityDescription): """Describes an Envoy Collar sensor entity.""" value_fn: Callable[[EnvoyCollar], datetime.datetime | int | float | str] COLLAR_SENSORS = ( EnvoyCollarSensorEntityDescription( key="temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, value_fn=attrgetter("temperature"), ), EnvoyCollarSensorEntityDescription( key=LAST_REPORTED_KEY, translation_key=LAST_REPORTED_KEY, native_unit_of_measurement=None, device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda collar: dt_util.utc_from_timestamp(collar.last_report_date), ), EnvoyCollarSensorEntityDescription( key="grid_state", translation_key="grid_status", value_fn=lambda collar: collar.grid_state, ), EnvoyCollarSensorEntityDescription( key="mid_state", translation_key="mid_state", value_fn=lambda collar: collar.mid_state, ), ) @dataclass(frozen=True, kw_only=True) class EnvoyC6CCSensorEntityDescription(SensorEntityDescription): """Describes an Envoy C6 Combiner controller sensor entity.""" value_fn: Callable[[EnvoyC6CC], datetime.datetime] C6CC_SENSORS = ( EnvoyC6CCSensorEntityDescription( key=LAST_REPORTED_KEY, translation_key=LAST_REPORTED_KEY, native_unit_of_measurement=None, device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda c6cc: dt_util.utc_from_timestamp(c6cc.last_report_date), ), ) @dataclass(frozen=True) class EnvoyEnchargeAggregateRequiredKeysMixin: """Mixin for required keys.""" @dataclass(frozen=True, kw_only=True) class EnvoyEnchargeAggregateSensorEntityDescription(SensorEntityDescription): """Describes an Envoy Encharge sensor entity.""" value_fn: Callable[[EnvoyEnchargeAggregate], int] ENCHARGE_AGGREGATE_SENSORS = ( EnvoyEnchargeAggregateSensorEntityDescription( key="battery_level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, value_fn=attrgetter("state_of_charge"), ), EnvoyEnchargeAggregateSensorEntityDescription( key="reserve_soc", translation_key="reserve_soc", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, value_fn=attrgetter("reserve_state_of_charge"), ), EnvoyEnchargeAggregateSensorEntityDescription( key="available_energy", translation_key="available_energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, value_fn=attrgetter("available_energy"), ), EnvoyEnchargeAggregateSensorEntityDescription( key="reserve_energy", translation_key="reserve_energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, value_fn=attrgetter("backup_reserve"), ), EnvoyEnchargeAggregateSensorEntityDescription( key="max_capacity", translation_key="max_capacity", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, value_fn=attrgetter("max_available_capacity"), ), ) @dataclass(frozen=True, kw_only=True) class EnvoyAcbBatterySensorEntityDescription(SensorEntityDescription): """Describes an Envoy ACB Battery sensor entity.""" value_fn: Callable[[EnvoyACBPower], int | str] ACB_BATTERY_POWER_SENSORS = ( EnvoyAcbBatterySensorEntityDescription( key="acb_power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, value_fn=attrgetter("power"), ), EnvoyAcbBatterySensorEntityDescription( key="acb_soc", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, value_fn=attrgetter("state_of_charge"), ), EnvoyAcbBatterySensorEntityDescription( key="acb_battery_state", translation_key="acb_battery_state", device_class=SensorDeviceClass.ENUM, options=["discharging", "idle", "charging", "full"], value_fn=attrgetter("state"), ), ) ACB_BATTERY_ENERGY_SENSORS = ( EnvoyAcbBatterySensorEntityDescription( key="acb_available_energy", translation_key="acb_available_energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY_STORAGE, value_fn=attrgetter("charge_wh"), ), ) @dataclass(frozen=True, kw_only=True) class EnvoyAggregateBatterySensorEntityDescription(SensorEntityDescription): """Describes an Envoy aggregate Ensemble and ACB Battery sensor entity.""" value_fn: Callable[[EnvoyBatteryAggregate], int] AGGREGATE_BATTERY_SENSORS = ( EnvoyAggregateBatterySensorEntityDescription( key="aggregated_soc", translation_key="aggregated_soc", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, value_fn=attrgetter("state_of_charge"), ), EnvoyAggregateBatterySensorEntityDescription( key="aggregated_available_energy", translation_key="aggregated_available_energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY_STORAGE, value_fn=attrgetter("available_energy"), ), EnvoyAggregateBatterySensorEntityDescription( key="aggregated_max_battery_capacity", translation_key="aggregated_max_capacity", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY_STORAGE, value_fn=attrgetter("max_available_capacity"), ), ) async def async_setup_entry( hass: HomeAssistant, config_entry: EnphaseConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up envoy sensor platform.""" coordinator = config_entry.runtime_data envoy_data = coordinator.envoy.data assert envoy_data is not None _LOGGER.debug("Envoy data: %s", envoy_data) entities: list[Entity] = [ EnvoyProductionEntity(coordinator, description) for description in PRODUCTION_SENSORS ] if envoy_data.system_consumption: entities.extend( EnvoyConsumptionEntity(coordinator, description) for description in CONSUMPTION_SENSORS ) if envoy_data.system_net_consumption: entities.extend( EnvoyNetConsumptionEntity(coordinator, description) for description in NET_CONSUMPTION_SENSORS ) # For each production phase reported add production entities if envoy_data.system_production_phases: entities.extend( EnvoyProductionPhaseEntity(coordinator, description) for use_phase, phase in envoy_data.system_production_phases.items() for description in PRODUCTION_PHASE_SENSORS[use_phase] if phase is not None ) # For each consumption phase reported add consumption entities if envoy_data.system_consumption_phases: entities.extend( EnvoyConsumptionPhaseEntity(coordinator, description) for use_phase, phase in envoy_data.system_consumption_phases.items() for description in CONSUMPTION_PHASE_SENSORS[use_phase] if phase is not None ) # For each net_consumption phase reported add consumption entities if envoy_data.system_net_consumption_phases: entities.extend( EnvoyNetConsumptionPhaseEntity(coordinator, description) for use_phase, phase in envoy_data.system_net_consumption_phases.items() for description in NET_CONSUMPTION_PHASE_SENSORS[use_phase] if phase is not None ) # Add Current Transformer entities if envoy_data.ctmeters: entities.extend( EnvoyCTEntity(coordinator, description) for sensors in ( CT_NET_CONSUMPTION_SENSORS, CT_PRODUCTION_SENSORS, CT_STORAGE_SENSORS, ) for description in sensors if description.cttype in envoy_data.ctmeters ) # Add Current Transformer phase entities if ctmeters_phases := envoy_data.ctmeters_phases: entities.extend( EnvoyCTPhaseEntity(coordinator, description) for sensors in ( CT_NET_CONSUMPTION_PHASE_SENSORS, CT_PRODUCTION_PHASE_SENSORS, CT_STORAGE_PHASE_SENSORS, ) for phase, descriptions in sensors.items() for description in descriptions if (cttype := description.cttype) in ctmeters_phases and phase in ctmeters_phases[cttype] ) if envoy_data.inverters: entities.extend( EnvoyInverterEntity(coordinator, description, inverter) for description in INVERTER_SENSORS for inverter in envoy_data.inverters ) if envoy_data.encharge_inventory: entities.extend( EnvoyEnchargeInventoryEntity(coordinator, description, encharge) for description in ENCHARGE_INVENTORY_SENSORS for encharge in envoy_data.encharge_inventory ) if envoy_data.encharge_power: entities.extend( EnvoyEnchargePowerEntity(coordinator, description, encharge) for description in ENCHARGE_POWER_SENSORS for encharge in envoy_data.encharge_power ) if envoy_data.encharge_aggregate: entities.extend( EnvoyEnchargeAggregateEntity(coordinator, description) for description in ENCHARGE_AGGREGATE_SENSORS ) if envoy_data.enpower: entities.extend( EnvoyEnpowerEntity(coordinator, description) for description in ENPOWER_SENSORS ) if envoy_data.acb_power: entities.extend( EnvoyAcbBatteryPowerEntity(coordinator, description) for description in ACB_BATTERY_POWER_SENSORS ) entities.extend( EnvoyAcbBatteryEnergyEntity(coordinator, description) for description in ACB_BATTERY_ENERGY_SENSORS ) if envoy_data.battery_aggregate: entities.extend( AggregateBatteryEntity(coordinator, description) for description in AGGREGATE_BATTERY_SENSORS ) if envoy_data.collar: entities.extend( EnvoyCollarEntity(coordinator, description) for description in COLLAR_SENSORS ) if envoy_data.c6cc: entities.extend( EnvoyC6CCEntity(coordinator, description) for description in C6CC_SENSORS ) async_add_entities(entities) class EnvoySensorBaseEntity(EnvoyBaseEntity, SensorEntity): """Defines a base envoy entity.""" class EnvoySystemSensorEntity(EnvoySensorBaseEntity): """Envoy system base entity.""" def __init__( self, coordinator: EnphaseUpdateCoordinator, description: SensorEntityDescription, ) -> None: """Initialize Envoy entity.""" super().__init__(coordinator, description) self._attr_unique_id = f"{self.envoy_serial_num}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.envoy_serial_num)}, manufacturer="Enphase", model=coordinator.envoy.envoy_model, name=coordinator.name, sw_version=str(coordinator.envoy.firmware), hw_version=coordinator.envoy.part_number, serial_number=self.envoy_serial_num, ) class EnvoyProductionEntity(EnvoySystemSensorEntity): """Envoy production entity.""" entity_description: EnvoyProductionSensorEntityDescription @property def native_value(self) -> int | None: """Return the state of the sensor.""" system_production = self.data.system_production assert system_production is not None return self.entity_description.value_fn(system_production) class EnvoyConsumptionEntity(EnvoySystemSensorEntity): """Envoy consumption entity.""" entity_description: EnvoyConsumptionSensorEntityDescription @property def native_value(self) -> int | None: """Return the state of the sensor.""" system_consumption = self.data.system_consumption assert system_consumption is not None return self.entity_description.value_fn(system_consumption) class EnvoyNetConsumptionEntity(EnvoySystemSensorEntity): """Envoy consumption entity.""" entity_description: EnvoyConsumptionSensorEntityDescription @property def native_value(self) -> int | None: """Return the state of the sensor.""" system_net_consumption = self.data.system_net_consumption assert system_net_consumption is not None return self.entity_description.value_fn(system_net_consumption) class EnvoyProductionPhaseEntity(EnvoySystemSensorEntity): """Envoy phase production entity.""" entity_description: EnvoyProductionSensorEntityDescription @property def native_value(self) -> int | None: """Return the state of the sensor.""" if TYPE_CHECKING: assert self.entity_description.on_phase assert self.data.system_production_phases if ( system_production := self.data.system_production_phases[ self.entity_description.on_phase ] ) is None: return None return self.entity_description.value_fn(system_production) class EnvoyConsumptionPhaseEntity(EnvoySystemSensorEntity): """Envoy phase consumption entity.""" entity_description: EnvoyConsumptionSensorEntityDescription @property def native_value(self) -> int | None: """Return the state of the sensor.""" if TYPE_CHECKING: assert self.entity_description.on_phase assert self.data.system_consumption_phases if ( system_consumption := self.data.system_consumption_phases[ self.entity_description.on_phase ] ) is None: return None return self.entity_description.value_fn(system_consumption) class EnvoyNetConsumptionPhaseEntity(EnvoySystemSensorEntity): """Envoy phase consumption entity.""" entity_description: EnvoyConsumptionSensorEntityDescription @property def native_value(self) -> int | None: """Return the state of the sensor.""" if TYPE_CHECKING: assert self.entity_description.on_phase assert self.data.system_net_consumption_phases if ( system_net_consumption := self.data.system_net_consumption_phases[ self.entity_description.on_phase ] ) is None: return None return self.entity_description.value_fn(system_net_consumption) class EnvoyCTEntity(EnvoySystemSensorEntity): """Envoy CT entity.""" entity_description: EnvoyCTSensorEntityDescription @property def native_value( self, ) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None: """Return the state of the CT sensor.""" if (cttype := self.entity_description.cttype) not in self.data.ctmeters: return None return self.entity_description.value_fn(self.data.ctmeters[cttype]) class EnvoyCTPhaseEntity(EnvoySystemSensorEntity): """Envoy CT phase entity.""" entity_description: EnvoyCTSensorEntityDescription @property def native_value( self, ) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None: """Return the state of the CT phase sensor.""" if TYPE_CHECKING: assert self.entity_description.on_phase if (cttype := self.entity_description.cttype) not in self.data.ctmeters_phases: return None if (phase := self.entity_description.on_phase) not in self.data.ctmeters_phases[ cttype ]: return None return self.entity_description.value_fn( self.data.ctmeters_phases[cttype][phase] ) class EnvoyInverterEntity(EnvoySensorBaseEntity): """Envoy inverter entity.""" entity_description: EnvoyInverterSensorEntityDescription def __init__( self, coordinator: EnphaseUpdateCoordinator, description: EnvoyInverterSensorEntityDescription, serial_number: str, ) -> None: """Initialize Envoy inverter entity.""" super().__init__(coordinator, description) self._serial_number = serial_number key = description.key if key == INVERTERS_KEY: # Originally there was only one inverter sensor, so we don't want to # break existing installations by changing the unique_id. self._attr_unique_id = serial_number else: # Additional sensors have a unique_id that includes the # sensor key. self._attr_unique_id = f"{serial_number}_{key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, serial_number)}, name=f"Inverter {serial_number}", manufacturer="Enphase", model="Inverter", via_device=(DOMAIN, self.envoy_serial_num), serial_number=serial_number, ) @property def native_value(self) -> datetime.datetime | float | None: """Return the state of the sensor.""" inverters = self.data.inverters assert inverters is not None # Some envoy fw versions return an empty inverter array every 4 hours when # no production is taking place. Prevent collection failure due to this # as other data seems fine. Inverters will show unknown during this cycle. if self._serial_number not in inverters: _LOGGER.debug( "Inverter %s not in returned inverters array (size: %s)", self._serial_number, len(inverters), ) return None return self.entity_description.value_fn(inverters[self._serial_number]) class EnvoyEnchargeEntity(EnvoySensorBaseEntity): """Envoy Encharge sensor entity.""" def __init__( self, coordinator: EnphaseUpdateCoordinator, description: EnvoyEnchargeSensorEntityDescription | EnvoyEnchargePowerSensorEntityDescription, serial_number: str, ) -> None: """Initialize Encharge entity.""" super().__init__(coordinator, description) self._serial_number = serial_number self._attr_unique_id = f"{serial_number}_{description.key}" encharge_inventory = self.data.encharge_inventory assert encharge_inventory is not None self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, serial_number)}, manufacturer="Enphase", model="Encharge", name=f"Encharge {serial_number}", sw_version=str(encharge_inventory[self._serial_number].firmware_version), via_device=(DOMAIN, self.envoy_serial_num), serial_number=serial_number, ) class EnvoyEnchargeInventoryEntity(EnvoyEnchargeEntity): """Envoy Encharge inventory entity.""" entity_description: EnvoyEnchargeSensorEntityDescription @property def native_value(self) -> int | float | datetime.datetime | None: """Return the state of the inventory sensors.""" encharge_inventory = self.data.encharge_inventory assert encharge_inventory is not None return self.entity_description.value_fn(encharge_inventory[self._serial_number]) class EnvoyEnchargePowerEntity(EnvoyEnchargeEntity): """Envoy Encharge power entity.""" entity_description: EnvoyEnchargePowerSensorEntityDescription @property def native_value(self) -> int | float | None: """Return the state of the power sensors.""" encharge_power = self.data.encharge_power assert encharge_power is not None return self.entity_description.value_fn(encharge_power[self._serial_number]) class EnvoyEnchargeAggregateEntity(EnvoySystemSensorEntity): """Envoy Encharge Aggregate sensor entity.""" entity_description: EnvoyEnchargeAggregateSensorEntityDescription @property def native_value(self) -> int: """Return the state of the aggregate sensors.""" encharge_aggregate = self.data.encharge_aggregate assert encharge_aggregate is not None return self.entity_description.value_fn(encharge_aggregate) class EnvoyEnpowerEntity(EnvoySensorBaseEntity): """Envoy Enpower sensor entity.""" entity_description: EnvoyEnpowerSensorEntityDescription def __init__( self, coordinator: EnphaseUpdateCoordinator, description: EnvoyEnpowerSensorEntityDescription, ) -> None: """Initialize Enpower entity.""" super().__init__(coordinator, description) enpower_data = self.data.enpower assert enpower_data is not None self._attr_unique_id = f"{enpower_data.serial_number}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, enpower_data.serial_number)}, manufacturer="Enphase", model="Enpower", name=f"Enpower {enpower_data.serial_number}", sw_version=str(enpower_data.firmware_version), via_device=(DOMAIN, self.envoy_serial_num), serial_number=enpower_data.serial_number, ) @property def native_value(self) -> datetime.datetime | int | float | None: """Return the state of the power sensors.""" enpower = self.data.enpower assert enpower is not None return self.entity_description.value_fn(enpower) class EnvoyAcbBatteryPowerEntity(EnvoySensorBaseEntity): """Envoy ACB Battery power sensor entity.""" entity_description: EnvoyAcbBatterySensorEntityDescription def __init__( self, coordinator: EnphaseUpdateCoordinator, description: EnvoyAcbBatterySensorEntityDescription, ) -> None: """Initialize ACB Battery entity.""" super().__init__(coordinator, description) acb_data = self.data.acb_power assert acb_data is not None self._attr_unique_id = f"{self.envoy_serial_num}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{self.envoy_serial_num}_acb")}, manufacturer="Enphase", model="ACB", name=f"ACB {self.envoy_serial_num}", via_device=(DOMAIN, self.envoy_serial_num), ) @property def native_value(self) -> int | str | None: """Return the state of the ACB Battery power sensors.""" acb = self.data.acb_power assert acb is not None return self.entity_description.value_fn(acb) class EnvoyAcbBatteryEnergyEntity(EnvoySystemSensorEntity): """Envoy combined ACB and Ensemble Battery Aggregate energy sensor entity.""" entity_description: EnvoyAcbBatterySensorEntityDescription @property def native_value(self) -> int | str: """Return the state of the aggregate energy sensors.""" acb = self.data.acb_power assert acb is not None return self.entity_description.value_fn(acb) class AggregateBatteryEntity(EnvoySystemSensorEntity): """Envoy combined ACB and Ensemble Battery Aggregate sensor entity.""" entity_description: EnvoyAggregateBatterySensorEntityDescription @property def native_value(self) -> int: """Return the state of the aggregate sensors.""" battery_aggregate = self.data.battery_aggregate assert battery_aggregate is not None return self.entity_description.value_fn(battery_aggregate) class EnvoyCollarEntity(EnvoySensorBaseEntity): """Envoy Collar sensor entity.""" entity_description: EnvoyCollarSensorEntityDescription def __init__( self, coordinator: EnphaseUpdateCoordinator, description: EnvoyCollarSensorEntityDescription, ) -> None: """Initialize Collar entity.""" super().__init__(coordinator, description) collar_data = self.data.collar assert collar_data is not None self._serial_number = collar_data.serial_number self._attr_unique_id = f"{collar_data.serial_number}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, collar_data.serial_number)}, manufacturer="Enphase", model="IQ Meter Collar", name=f"Collar {collar_data.serial_number}", sw_version=str(collar_data.firmware_version), via_device=(DOMAIN, self.envoy_serial_num), serial_number=collar_data.serial_number, ) @property def native_value(self) -> datetime.datetime | int | float | str: """Return the state of the collar sensors.""" collar_data = self.data.collar assert collar_data is not None return self.entity_description.value_fn(collar_data) class EnvoyC6CCEntity(EnvoySensorBaseEntity): """Envoy C6CC sensor entity.""" entity_description: EnvoyC6CCSensorEntityDescription def __init__( self, coordinator: EnphaseUpdateCoordinator, description: EnvoyC6CCSensorEntityDescription, ) -> None: """Initialize Encharge entity.""" super().__init__(coordinator, description) c6cc_data = self.data.c6cc assert c6cc_data is not None self._attr_unique_id = f"{c6cc_data.serial_number}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, c6cc_data.serial_number)}, manufacturer="Enphase", model="C6 COMBINER CONTROLLER", name=f"C6 Combiner {c6cc_data.serial_number}", sw_version=str(c6cc_data.firmware_version), via_device=(DOMAIN, self.envoy_serial_num), serial_number=c6cc_data.serial_number, ) @property def native_value(self) -> datetime.datetime: """Return the state of the c6cc inventory sensors.""" c6cc_data = self.data.c6cc assert c6cc_data is not None return self.entity_description.value_fn(c6cc_data)