mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 00:37:13 +00:00
Add home battery storage entities for enphase_envoy (#114015)
This commit is contained in:
parent
817d931df0
commit
205c457a77
@ -89,8 +89,10 @@ async def async_get_config_entry_diagnostics(
|
|||||||
"system_production_phases": envoy_data.system_production_phases,
|
"system_production_phases": envoy_data.system_production_phases,
|
||||||
"ctmeter_production": envoy_data.ctmeter_production,
|
"ctmeter_production": envoy_data.ctmeter_production,
|
||||||
"ctmeter_consumption": envoy_data.ctmeter_consumption,
|
"ctmeter_consumption": envoy_data.ctmeter_consumption,
|
||||||
|
"ctmeter_storage": envoy_data.ctmeter_storage,
|
||||||
"ctmeter_production_phases": envoy_data.ctmeter_production_phases,
|
"ctmeter_production_phases": envoy_data.ctmeter_production_phases,
|
||||||
"ctmeter_consumption_phases": envoy_data.ctmeter_consumption_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_status": envoy_data.dry_contact_status,
|
||||||
"dry_contact_settings": envoy_data.dry_contact_settings,
|
"dry_contact_settings": envoy_data.dry_contact_settings,
|
||||||
"inverters": envoy_data.inverters,
|
"inverters": envoy_data.inverters,
|
||||||
@ -108,6 +110,7 @@ async def async_get_config_entry_diagnostics(
|
|||||||
"ct_count": envoy.ct_meter_count,
|
"ct_count": envoy.ct_meter_count,
|
||||||
"ct_consumption_meter": envoy.consumption_meter_type,
|
"ct_consumption_meter": envoy.consumption_meter_type,
|
||||||
"ct_production_meter": envoy.production_meter_type,
|
"ct_production_meter": envoy.production_meter_type,
|
||||||
|
"ct_storage_meter": envoy.storage_meter_type,
|
||||||
}
|
}
|
||||||
|
|
||||||
diagnostic_data: dict[str, Any] = {
|
diagnostic_data: dict[str, Any] = {
|
||||||
|
@ -364,6 +364,87 @@ CT_PRODUCTION_PHASE_SENSORS = {
|
|||||||
for phase in range(3)
|
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)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class EnvoyEnchargeSensorEntityDescription(SensorEntityDescription):
|
class EnvoyEnchargeSensorEntityDescription(SensorEntityDescription):
|
||||||
@ -560,6 +641,21 @@ async def async_setup_entry(
|
|||||||
for description in CT_PRODUCTION_PHASE_SENSORS[use_phase]
|
for description in CT_PRODUCTION_PHASE_SENSORS[use_phase]
|
||||||
if phase.measurement_type == CtType.PRODUCTION
|
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:
|
if envoy_data.inverters:
|
||||||
entities.extend(
|
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):
|
class EnvoyInverterEntity(EnvoySensorBaseEntity):
|
||||||
"""Envoy inverter entity."""
|
"""Envoy inverter entity."""
|
||||||
|
|
||||||
|
@ -170,6 +170,24 @@
|
|||||||
"production_ct_status_flags": {
|
"production_ct_status_flags": {
|
||||||
"name": "Meter status flags active production CT"
|
"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": {
|
"lifetime_net_consumption_phase": {
|
||||||
"name": "Lifetime net energy consumption {phase_name}"
|
"name": "Lifetime net energy consumption {phase_name}"
|
||||||
},
|
},
|
||||||
@ -197,6 +215,24 @@
|
|||||||
"production_ct_status_flags_phase": {
|
"production_ct_status_flags_phase": {
|
||||||
"name": "Meter status flags active production CT {phase_name}"
|
"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": {
|
"reserve_soc": {
|
||||||
"name": "Reserve battery level"
|
"name": "Reserve battery level"
|
||||||
},
|
},
|
||||||
|
@ -55,15 +55,18 @@ def config_fixture():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="mock_envoy")
|
@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."""
|
"""Define a mocked Envoy fixture."""
|
||||||
mock_envoy = Mock(spec=Envoy)
|
mock_envoy = Mock(spec=Envoy)
|
||||||
mock_envoy.serial_number = serial_number
|
mock_envoy.serial_number = serial_number
|
||||||
mock_envoy.firmware = "7.1.2"
|
mock_envoy.firmware = "7.1.2"
|
||||||
mock_envoy.part_number = "123456789"
|
mock_envoy.part_number = "123456789"
|
||||||
mock_envoy.envoy_model = (
|
mock_envoy.envoy_model = "Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT"
|
||||||
"Envoy, phases: 3, phase mode: three, net-consumption CT, production CT"
|
|
||||||
)
|
|
||||||
mock_envoy.authenticate = mock_authenticate
|
mock_envoy.authenticate = mock_authenticate
|
||||||
mock_envoy.setup = mock_setup
|
mock_envoy.setup = mock_setup
|
||||||
mock_envoy.auth = mock_auth
|
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_mode = EnvoyPhaseMode.THREE
|
||||||
mock_envoy.phase_count = 3
|
mock_envoy.phase_count = 3
|
||||||
mock_envoy.active_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.consumption_meter_type = CtType.NET_CONSUMPTION
|
||||||
mock_envoy.production_meter_type = CtType.PRODUCTION
|
mock_envoy.production_meter_type = CtType.PRODUCTION
|
||||||
|
mock_envoy.storage_meter_type = CtType.STORAGE
|
||||||
mock_envoy.data = EnvoyData(
|
mock_envoy.data = EnvoyData(
|
||||||
system_consumption=EnvoySystemConsumption(
|
system_consumption=EnvoySystemConsumption(
|
||||||
watt_hours_last_7_days=1234,
|
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,
|
metering_status=CtMeterStatus.NORMAL,
|
||||||
status_flags=[],
|
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={
|
ctmeter_production_phases={
|
||||||
PhaseNames.PHASE_1: EnvoyMeterData(
|
PhaseNames.PHASE_1: EnvoyMeterData(
|
||||||
eid="100000011",
|
eid="100000011",
|
||||||
@ -261,6 +280,53 @@ def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth):
|
|||||||
status_flags=[],
|
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={
|
inverters={
|
||||||
"1": EnvoyInverter(
|
"1": EnvoyInverter(
|
||||||
serial_number="1",
|
serial_number="1",
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user