mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 02:07:09 +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 __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, replace
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from pyenphase import (
|
from pyenphase import (
|
||||||
EnvoyEncharge,
|
EnvoyEncharge,
|
||||||
@ -15,6 +16,7 @@ from pyenphase import (
|
|||||||
EnvoySystemConsumption,
|
EnvoySystemConsumption,
|
||||||
EnvoySystemProduction,
|
EnvoySystemProduction,
|
||||||
)
|
)
|
||||||
|
from pyenphase.const import PHASENAMES, PhaseNames
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
@ -85,6 +87,7 @@ class EnvoyProductionRequiredKeysMixin:
|
|||||||
"""Mixin for required keys."""
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
value_fn: Callable[[EnvoySystemProduction], int]
|
value_fn: Callable[[EnvoySystemProduction], int]
|
||||||
|
on_phase: PhaseNames | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -104,6 +107,7 @@ PRODUCTION_SENSORS = (
|
|||||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
suggested_display_precision=3,
|
suggested_display_precision=3,
|
||||||
value_fn=lambda production: production.watts_now,
|
value_fn=lambda production: production.watts_now,
|
||||||
|
on_phase=None,
|
||||||
),
|
),
|
||||||
EnvoyProductionSensorEntityDescription(
|
EnvoyProductionSensorEntityDescription(
|
||||||
key="daily_production",
|
key="daily_production",
|
||||||
@ -114,6 +118,7 @@ PRODUCTION_SENSORS = (
|
|||||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
value_fn=lambda production: production.watt_hours_today,
|
value_fn=lambda production: production.watt_hours_today,
|
||||||
|
on_phase=None,
|
||||||
),
|
),
|
||||||
EnvoyProductionSensorEntityDescription(
|
EnvoyProductionSensorEntityDescription(
|
||||||
key="seven_days_production",
|
key="seven_days_production",
|
||||||
@ -123,6 +128,7 @@ PRODUCTION_SENSORS = (
|
|||||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
suggested_display_precision=1,
|
suggested_display_precision=1,
|
||||||
value_fn=lambda production: production.watt_hours_last_7_days,
|
value_fn=lambda production: production.watt_hours_last_7_days,
|
||||||
|
on_phase=None,
|
||||||
),
|
),
|
||||||
EnvoyProductionSensorEntityDescription(
|
EnvoyProductionSensorEntityDescription(
|
||||||
key="lifetime_production",
|
key="lifetime_production",
|
||||||
@ -133,15 +139,32 @@ PRODUCTION_SENSORS = (
|
|||||||
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
||||||
suggested_display_precision=3,
|
suggested_display_precision=3,
|
||||||
value_fn=lambda production: production.watt_hours_lifetime,
|
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)
|
@dataclass(frozen=True)
|
||||||
class EnvoyConsumptionRequiredKeysMixin:
|
class EnvoyConsumptionRequiredKeysMixin:
|
||||||
"""Mixin for required keys."""
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
value_fn: Callable[[EnvoySystemConsumption], int]
|
value_fn: Callable[[EnvoySystemConsumption], int]
|
||||||
|
on_phase: PhaseNames | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -161,6 +184,7 @@ CONSUMPTION_SENSORS = (
|
|||||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
suggested_display_precision=3,
|
suggested_display_precision=3,
|
||||||
value_fn=lambda consumption: consumption.watts_now,
|
value_fn=lambda consumption: consumption.watts_now,
|
||||||
|
on_phase=None,
|
||||||
),
|
),
|
||||||
EnvoyConsumptionSensorEntityDescription(
|
EnvoyConsumptionSensorEntityDescription(
|
||||||
key="daily_consumption",
|
key="daily_consumption",
|
||||||
@ -171,6 +195,7 @@ CONSUMPTION_SENSORS = (
|
|||||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
value_fn=lambda consumption: consumption.watt_hours_today,
|
value_fn=lambda consumption: consumption.watt_hours_today,
|
||||||
|
on_phase=None,
|
||||||
),
|
),
|
||||||
EnvoyConsumptionSensorEntityDescription(
|
EnvoyConsumptionSensorEntityDescription(
|
||||||
key="seven_days_consumption",
|
key="seven_days_consumption",
|
||||||
@ -180,6 +205,7 @@ CONSUMPTION_SENSORS = (
|
|||||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
suggested_display_precision=1,
|
suggested_display_precision=1,
|
||||||
value_fn=lambda consumption: consumption.watt_hours_last_7_days,
|
value_fn=lambda consumption: consumption.watt_hours_last_7_days,
|
||||||
|
on_phase=None,
|
||||||
),
|
),
|
||||||
EnvoyConsumptionSensorEntityDescription(
|
EnvoyConsumptionSensorEntityDescription(
|
||||||
key="lifetime_consumption",
|
key="lifetime_consumption",
|
||||||
@ -190,10 +216,26 @@ CONSUMPTION_SENSORS = (
|
|||||||
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
||||||
suggested_display_precision=3,
|
suggested_display_precision=3,
|
||||||
value_fn=lambda consumption: consumption.watt_hours_lifetime,
|
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)
|
@dataclass(frozen=True)
|
||||||
class EnvoyEnchargeRequiredKeysMixin:
|
class EnvoyEnchargeRequiredKeysMixin:
|
||||||
"""Mixin for required keys."""
|
"""Mixin for required keys."""
|
||||||
@ -361,6 +403,23 @@ async def async_setup_entry(
|
|||||||
EnvoyConsumptionEntity(coordinator, description)
|
EnvoyConsumptionEntity(coordinator, description)
|
||||||
for description in CONSUMPTION_SENSORS
|
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:
|
if envoy_data.inverters:
|
||||||
entities.extend(
|
entities.extend(
|
||||||
EnvoyInverterEntity(coordinator, description, inverter)
|
EnvoyInverterEntity(coordinator, description, inverter)
|
||||||
@ -414,9 +473,11 @@ class EnvoySystemSensorEntity(EnvoySensorBaseEntity):
|
|||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, self.envoy_serial_num)},
|
identifiers={(DOMAIN, self.envoy_serial_num)},
|
||||||
manufacturer="Enphase",
|
manufacturer="Enphase",
|
||||||
model=coordinator.envoy.part_number or "Envoy",
|
model=coordinator.envoy.envoy_model,
|
||||||
name=coordinator.name,
|
name=coordinator.name,
|
||||||
sw_version=str(coordinator.envoy.firmware),
|
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)
|
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):
|
class EnvoyInverterEntity(EnvoySensorBaseEntity):
|
||||||
"""Envoy inverter entity."""
|
"""Envoy inverter entity."""
|
||||||
|
|
||||||
|
@ -119,6 +119,30 @@
|
|||||||
"lifetime_consumption": {
|
"lifetime_consumption": {
|
||||||
"name": "Lifetime energy 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": {
|
"reserve_soc": {
|
||||||
"name": "Reserve battery level"
|
"name": "Reserve battery level"
|
||||||
},
|
},
|
||||||
|
@ -9,6 +9,8 @@ from pyenphase import (
|
|||||||
EnvoySystemProduction,
|
EnvoySystemProduction,
|
||||||
EnvoyTokenAuth,
|
EnvoyTokenAuth,
|
||||||
)
|
)
|
||||||
|
from pyenphase.const import PhaseNames, SupportedFeatures
|
||||||
|
from pyenphase.models.meters import CtType, EnvoyPhaseMode
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.enphase_envoy import DOMAIN
|
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.authenticate = mock_authenticate
|
||||||
mock_envoy.setup = mock_setup
|
mock_envoy.setup = mock_setup
|
||||||
mock_envoy.auth = mock_auth
|
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(
|
mock_envoy.data = EnvoyData(
|
||||||
system_consumption=EnvoySystemConsumption(
|
system_consumption=EnvoySystemConsumption(
|
||||||
watt_hours_last_7_days=1234,
|
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,
|
watt_hours_today=1234,
|
||||||
watts_now=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={
|
inverters={
|
||||||
"1": EnvoyInverter(
|
"1": EnvoyInverter(
|
||||||
serial_number="1",
|
serial_number="1",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user