Skip to content
Snippets Groups Projects
Unverified Commit 2b33feb3 authored by Arie Catsman's avatar Arie Catsman Committed by GitHub
Browse files

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: default avatarJ. Nick Koston <nick@koston.org>
parent e13a34df
No related branches found
No related tags found
No related merge requests found
......@@ -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",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment