Add home battery storage entities for enphase_envoy (#114015)

This commit is contained in:
Arie Catsman 2024-03-22 19:46:39 +01:00 committed by GitHub
parent 817d931df0
commit 205c457a77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 2192 additions and 8 deletions

View File

@ -89,8 +89,10 @@ async def async_get_config_entry_diagnostics(
"system_production_phases": envoy_data.system_production_phases,
"ctmeter_production": envoy_data.ctmeter_production,
"ctmeter_consumption": envoy_data.ctmeter_consumption,
"ctmeter_storage": envoy_data.ctmeter_storage,
"ctmeter_production_phases": envoy_data.ctmeter_production_phases,
"ctmeter_consumption_phases": envoy_data.ctmeter_consumption_phases,
"ctmeter_storage_phases": envoy_data.ctmeter_storage_phases,
"dry_contact_status": envoy_data.dry_contact_status,
"dry_contact_settings": envoy_data.dry_contact_settings,
"inverters": envoy_data.inverters,
@ -108,6 +110,7 @@ async def async_get_config_entry_diagnostics(
"ct_count": envoy.ct_meter_count,
"ct_consumption_meter": envoy.consumption_meter_type,
"ct_production_meter": envoy.production_meter_type,
"ct_storage_meter": envoy.storage_meter_type,
}
diagnostic_data: dict[str, Any] = {

View File

@ -364,6 +364,87 @@ CT_PRODUCTION_PHASE_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=lambda ct: ct.energy_delivered,
on_phase=None,
),
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=lambda ct: ct.energy_received,
on_phase=None,
),
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=lambda ct: ct.active_power,
on_phase=None,
),
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=lambda ct: ct.voltage,
on_phase=None,
),
EnvoyCTSensorEntityDescription(
key="storage_ct_metering_status",
translation_key="storage_ct_metering_status",
device_class=SensorDeviceClass.ENUM,
options=list(CtMeterStatus),
entity_registry_enabled_default=False,
value_fn=lambda ct: ct.metering_status,
on_phase=None,
),
EnvoyCTSensorEntityDescription(
key="storage_ct_status_flags",
translation_key="storage_ct_status_flags",
state_class=None,
entity_registry_enabled_default=False,
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
on_phase=None,
),
)
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):
@ -560,6 +641,21 @@ async def async_setup_entry(
for description in CT_PRODUCTION_PHASE_SENSORS[use_phase]
if phase.measurement_type == CtType.PRODUCTION
)
# Add storage CT entities
if ctmeter := envoy_data.ctmeter_storage:
entities.extend(
EnvoyStorageCTEntity(coordinator, description)
for description in CT_STORAGE_SENSORS
if ctmeter.measurement_type == CtType.STORAGE
)
# For each storage ct phase reported add storage ct entities
if phase_data := envoy_data.ctmeter_storage_phases:
entities.extend(
EnvoyStorageCTPhaseEntity(coordinator, description)
for use_phase, phase in phase_data.items()
for description in CT_STORAGE_PHASE_SENSORS[use_phase]
if phase.measurement_type == CtType.STORAGE
)
if envoy_data.inverters:
entities.extend(
@ -758,6 +854,40 @@ class EnvoyProductionCTPhaseEntity(EnvoySystemSensorEntity):
)
class EnvoyStorageCTEntity(EnvoySystemSensorEntity):
"""Envoy net storage 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 (ctmeter := self.data.ctmeter_storage) is None:
return None
return self.entity_description.value_fn(ctmeter)
class EnvoyStorageCTPhaseEntity(EnvoySystemSensorEntity):
"""Envoy net storage 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 (ctmeter := self.data.ctmeter_storage_phases) is None:
return None
return self.entity_description.value_fn(
ctmeter[self.entity_description.on_phase]
)
class EnvoyInverterEntity(EnvoySensorBaseEntity):
"""Envoy inverter entity."""

View File

@ -170,6 +170,24 @@
"production_ct_status_flags": {
"name": "Meter status flags active production CT"
},
"lifetime_battery_discharged": {
"name": "Lifetime battery energy discharged"
},
"lifetime_battery_charged": {
"name": "Lifetime battery energy charged"
},
"battery_discharge": {
"name": "Current battery discharge"
},
"storage_ct_voltage": {
"name": "Voltage storage CT"
},
"storage_ct_metering_status": {
"name": "Metering status storage CT"
},
"storage_ct_status_flags": {
"name": "Meter status flags active storage CT"
},
"lifetime_net_consumption_phase": {
"name": "Lifetime net energy consumption {phase_name}"
},
@ -197,6 +215,24 @@
"production_ct_status_flags_phase": {
"name": "Meter status flags active production CT {phase_name}"
},
"lifetime_battery_discharged_phase": {
"name": "Lifetime battery energy discharged {phase_name}"
},
"lifetime_battery_charged_phase": {
"name": "Lifetime battery energy charged {phase_name}"
},
"battery_discharge_phase": {
"name": "Current battery discharge {phase_name}"
},
"storage_ct_voltage_phase": {
"name": "Voltage storage CT {phase_name}"
},
"storage_ct_metering_status_phase": {
"name": "Metering status storage CT {phase_name}"
},
"storage_ct_status_flags_phase": {
"name": "Meter status flags active storage CT {phase_name}"
},
"reserve_soc": {
"name": "Reserve battery level"
},

View File

@ -55,15 +55,18 @@ def config_fixture():
@pytest.fixture(name="mock_envoy")
def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth):
def mock_envoy_fixture(
serial_number,
mock_authenticate,
mock_setup,
mock_auth,
):
"""Define a mocked Envoy fixture."""
mock_envoy = Mock(spec=Envoy)
mock_envoy.serial_number = serial_number
mock_envoy.firmware = "7.1.2"
mock_envoy.part_number = "123456789"
mock_envoy.envoy_model = (
"Envoy, phases: 3, phase mode: three, net-consumption CT, production CT"
)
mock_envoy.envoy_model = "Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT"
mock_envoy.authenticate = mock_authenticate
mock_envoy.setup = mock_setup
mock_envoy.auth = mock_auth
@ -78,9 +81,10 @@ def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth):
mock_envoy.phase_mode = EnvoyPhaseMode.THREE
mock_envoy.phase_count = 3
mock_envoy.active_phase_count = 3
mock_envoy.ct_meter_count = 2
mock_envoy.ct_meter_count = 3
mock_envoy.consumption_meter_type = CtType.NET_CONSUMPTION
mock_envoy.production_meter_type = CtType.PRODUCTION
mock_envoy.storage_meter_type = CtType.STORAGE
mock_envoy.data = EnvoyData(
system_consumption=EnvoySystemConsumption(
watt_hours_last_7_days=1234,
@ -167,6 +171,21 @@ def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth):
metering_status=CtMeterStatus.NORMAL,
status_flags=[],
),
ctmeter_storage=EnvoyMeterData(
eid="100000030",
timestamp=1708006120,
energy_delivered=31234,
energy_received=32345,
active_power=103,
power_factor=0.23,
voltage=113,
current=0.4,
frequency=50.3,
state=CtState.ENABLED,
measurement_type=CtType.STORAGE,
metering_status=CtMeterStatus.NORMAL,
status_flags=[],
),
ctmeter_production_phases={
PhaseNames.PHASE_1: EnvoyMeterData(
eid="100000011",
@ -261,6 +280,53 @@ def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth):
status_flags=[],
),
},
ctmeter_storage_phases={
PhaseNames.PHASE_1: EnvoyMeterData(
eid="100000031",
timestamp=1708006121,
energy_delivered=312341,
energy_received=323451,
active_power=22,
power_factor=0.32,
voltage=113,
current=0.4,
frequency=50.3,
state=CtState.ENABLED,
measurement_type=CtType.STORAGE,
metering_status=CtMeterStatus.NORMAL,
status_flags=[],
),
PhaseNames.PHASE_2: EnvoyMeterData(
eid="100000032",
timestamp=1708006122,
energy_delivered=312342,
energy_received=323452,
active_power=33,
power_factor=0.23,
voltage=112,
current=0.3,
frequency=50.2,
state=CtState.ENABLED,
measurement_type=CtType.STORAGE,
metering_status=CtMeterStatus.NORMAL,
status_flags=[],
),
PhaseNames.PHASE_3: EnvoyMeterData(
eid="100000033",
timestamp=1708006123,
energy_delivered=312343,
energy_received=323453,
active_power=53,
power_factor=0.24,
voltage=112,
current=0.3,
frequency=50.2,
state=CtState.ENABLED,
measurement_type=CtType.STORAGE,
metering_status=CtMeterStatus.NORMAL,
status_flags=[],
),
},
inverters={
"1": EnvoyInverter(
serial_number="1",

File diff suppressed because it is too large Load Diff