mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
Add phase entities to Enphase Envoy (#108725)
* add phase entities to Enphase Envoy * Implement review feedback for translation strings * Enphase Envoy multiphase review changes Move device name logic to separate function. Refactor native value for phases Use dataclasses.replace for phase entities, add on-phase to base class as well, no need for phase entity descriptions anymore * Enphase Envoy reviewe feedback Move model determination to library. Revert states test for future split to sensor test. * Enphase_Envoy use model description from pyenphase library * Enphase_Envoy refactor Phase Sensors * Enphase_Envoy use walrus in phase sensor --------- Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
e13a34df0f
commit
2b33feb341
@ -2,9 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
import datetime
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pyenphase import (
|
||||
EnvoyEncharge,
|
||||
@ -15,6 +16,7 @@ from pyenphase import (
|
||||
EnvoySystemConsumption,
|
||||
EnvoySystemProduction,
|
||||
)
|
||||
from pyenphase.const import PHASENAMES, PhaseNames
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@ -85,6 +87,7 @@ class EnvoyProductionRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable[[EnvoySystemProduction], int]
|
||||
on_phase: PhaseNames | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -104,6 +107,7 @@ PRODUCTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
suggested_display_precision=3,
|
||||
value_fn=lambda production: production.watts_now,
|
||||
on_phase=None,
|
||||
),
|
||||
EnvoyProductionSensorEntityDescription(
|
||||
key="daily_production",
|
||||
@ -114,6 +118,7 @@ PRODUCTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda production: production.watt_hours_today,
|
||||
on_phase=None,
|
||||
),
|
||||
EnvoyProductionSensorEntityDescription(
|
||||
key="seven_days_production",
|
||||
@ -123,6 +128,7 @@ PRODUCTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=1,
|
||||
value_fn=lambda production: production.watt_hours_last_7_days,
|
||||
on_phase=None,
|
||||
),
|
||||
EnvoyProductionSensorEntityDescription(
|
||||
key="lifetime_production",
|
||||
@ -133,15 +139,32 @@ PRODUCTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
value_fn=lambda production: production.watt_hours_lifetime,
|
||||
on_phase=None,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
PRODUCTION_PHASE_SENSORS = {
|
||||
(on_phase := PhaseNames(PHASENAMES[phase])): [
|
||||
replace(
|
||||
sensor,
|
||||
key=f"{sensor.key}_l{phase + 1}",
|
||||
translation_key=f"{sensor.translation_key}_phase",
|
||||
on_phase=on_phase,
|
||||
translation_placeholders={"phase_name": f"l{phase + 1}"},
|
||||
)
|
||||
for sensor in list(PRODUCTION_SENSORS)
|
||||
]
|
||||
for phase in range(0, 3)
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EnvoyConsumptionRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable[[EnvoySystemConsumption], int]
|
||||
on_phase: PhaseNames | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -161,6 +184,7 @@ CONSUMPTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
suggested_display_precision=3,
|
||||
value_fn=lambda consumption: consumption.watts_now,
|
||||
on_phase=None,
|
||||
),
|
||||
EnvoyConsumptionSensorEntityDescription(
|
||||
key="daily_consumption",
|
||||
@ -171,6 +195,7 @@ CONSUMPTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda consumption: consumption.watt_hours_today,
|
||||
on_phase=None,
|
||||
),
|
||||
EnvoyConsumptionSensorEntityDescription(
|
||||
key="seven_days_consumption",
|
||||
@ -180,6 +205,7 @@ CONSUMPTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=1,
|
||||
value_fn=lambda consumption: consumption.watt_hours_last_7_days,
|
||||
on_phase=None,
|
||||
),
|
||||
EnvoyConsumptionSensorEntityDescription(
|
||||
key="lifetime_consumption",
|
||||
@ -190,10 +216,26 @@ CONSUMPTION_SENSORS = (
|
||||
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
value_fn=lambda consumption: consumption.watt_hours_lifetime,
|
||||
on_phase=None,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
CONSUMPTION_PHASE_SENSORS = {
|
||||
(on_phase := PhaseNames(PHASENAMES[phase])): [
|
||||
replace(
|
||||
sensor,
|
||||
key=f"{sensor.key}_l{phase + 1}",
|
||||
translation_key=f"{sensor.translation_key}_phase",
|
||||
on_phase=on_phase,
|
||||
translation_placeholders={"phase_name": f"l{phase + 1}"},
|
||||
)
|
||||
for sensor in list(CONSUMPTION_SENSORS)
|
||||
]
|
||||
for phase in range(0, 3)
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EnvoyEnchargeRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
@ -361,6 +403,23 @@ async def async_setup_entry(
|
||||
EnvoyConsumptionEntity(coordinator, description)
|
||||
for description in 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[PhaseNames(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[PhaseNames(use_phase)]
|
||||
if phase is not None
|
||||
)
|
||||
|
||||
if envoy_data.inverters:
|
||||
entities.extend(
|
||||
EnvoyInverterEntity(coordinator, description, inverter)
|
||||
@ -414,9 +473,11 @@ class EnvoySystemSensorEntity(EnvoySensorBaseEntity):
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.envoy_serial_num)},
|
||||
manufacturer="Enphase",
|
||||
model=coordinator.envoy.part_number or "Envoy",
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@ -446,6 +507,48 @@ class EnvoyConsumptionEntity(EnvoySystemSensorEntity):
|
||||
return self.entity_description.value_fn(system_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 EnvoyInverterEntity(EnvoySensorBaseEntity):
|
||||
"""Envoy inverter entity."""
|
||||
|
||||
|
@ -119,6 +119,30 @@
|
||||
"lifetime_consumption": {
|
||||
"name": "Lifetime energy consumption"
|
||||
},
|
||||
"current_power_production_phase": {
|
||||
"name": "Current power production {phase_name}"
|
||||
},
|
||||
"daily_production_phase": {
|
||||
"name": "Energy production today {phase_name}"
|
||||
},
|
||||
"seven_days_production_phase": {
|
||||
"name": "Energy production last seven days {phase_name}"
|
||||
},
|
||||
"lifetime_production_phase": {
|
||||
"name": "Lifetime energy production {phase_name}"
|
||||
},
|
||||
"current_power_consumption_phase": {
|
||||
"name": "Current power consumption {phase_name}"
|
||||
},
|
||||
"daily_consumption_phase": {
|
||||
"name": "Energy consumption today {phase_name}"
|
||||
},
|
||||
"seven_days_consumption_phase": {
|
||||
"name": "Energy consumption last seven days {phase_name}"
|
||||
},
|
||||
"lifetime_consumption_phase": {
|
||||
"name": "Lifetime energy consumption {phase_name}"
|
||||
},
|
||||
"reserve_soc": {
|
||||
"name": "Reserve battery level"
|
||||
},
|
||||
|
@ -9,6 +9,8 @@ from pyenphase import (
|
||||
EnvoySystemProduction,
|
||||
EnvoyTokenAuth,
|
||||
)
|
||||
from pyenphase.const import PhaseNames, SupportedFeatures
|
||||
from pyenphase.models.meters import CtType, EnvoyPhaseMode
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.enphase_envoy import DOMAIN
|
||||
@ -53,6 +55,18 @@ def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth):
|
||||
mock_envoy.authenticate = mock_authenticate
|
||||
mock_envoy.setup = mock_setup
|
||||
mock_envoy.auth = mock_auth
|
||||
mock_envoy.supported_features = SupportedFeatures(
|
||||
SupportedFeatures.INVERTERS
|
||||
| SupportedFeatures.PRODUCTION
|
||||
| SupportedFeatures.PRODUCTION
|
||||
| SupportedFeatures.METERING
|
||||
| SupportedFeatures.THREEPHASE
|
||||
)
|
||||
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.consumption_meter_type = CtType.NET_CONSUMPTION
|
||||
mock_envoy.data = EnvoyData(
|
||||
system_consumption=EnvoySystemConsumption(
|
||||
watt_hours_last_7_days=1234,
|
||||
@ -66,6 +80,46 @@ def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth):
|
||||
watt_hours_today=1234,
|
||||
watts_now=1234,
|
||||
),
|
||||
system_consumption_phases={
|
||||
PhaseNames.PHASE_1: EnvoySystemConsumption(
|
||||
watt_hours_last_7_days=1321,
|
||||
watt_hours_lifetime=1322,
|
||||
watt_hours_today=1323,
|
||||
watts_now=1324,
|
||||
),
|
||||
PhaseNames.PHASE_2: EnvoySystemConsumption(
|
||||
watt_hours_last_7_days=2321,
|
||||
watt_hours_lifetime=2322,
|
||||
watt_hours_today=2323,
|
||||
watts_now=2324,
|
||||
),
|
||||
PhaseNames.PHASE_3: EnvoySystemConsumption(
|
||||
watt_hours_last_7_days=3321,
|
||||
watt_hours_lifetime=3322,
|
||||
watt_hours_today=3323,
|
||||
watts_now=3324,
|
||||
),
|
||||
},
|
||||
system_production_phases={
|
||||
PhaseNames.PHASE_1: EnvoySystemProduction(
|
||||
watt_hours_last_7_days=1231,
|
||||
watt_hours_lifetime=1232,
|
||||
watt_hours_today=1233,
|
||||
watts_now=1234,
|
||||
),
|
||||
PhaseNames.PHASE_2: EnvoySystemProduction(
|
||||
watt_hours_last_7_days=2231,
|
||||
watt_hours_lifetime=2232,
|
||||
watt_hours_today=2233,
|
||||
watts_now=2234,
|
||||
),
|
||||
PhaseNames.PHASE_3: EnvoySystemProduction(
|
||||
watt_hours_last_7_days=3231,
|
||||
watt_hours_lifetime=3232,
|
||||
watt_hours_today=3233,
|
||||
watts_now=3234,
|
||||
),
|
||||
},
|
||||
inverters={
|
||||
"1": EnvoyInverter(
|
||||
serial_number="1",
|
||||
|
Loading…
x
Reference in New Issue
Block a user