bump pyenphase to 4.0 and add required None handling and tests (#179673)

This commit is contained in:
Arie Catsman
2026-08-21 20:13:20 +00:00
committed by Franck Nijhof
parent 57fb7b7f3f
commit 337fdfaa33
10 changed files with 1225 additions and 37 deletions
@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==3.2.1"],
"requirements": ["pyenphase==4.0.0"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."
@@ -21,7 +21,7 @@ from pyenphase import (
EnvoySystemConsumption,
EnvoySystemProduction,
)
from pyenphase.const import PHASENAMES
from pyenphase.const import PHASENAMES, SupportedFeatures
from pyenphase.models.acb import ACBChargeStatus, ACBSleepState
from pyenphase.models.meters import (
CtMeterStatus,
@@ -383,7 +383,7 @@ class EnvoyCTSensorEntityDescription(SensorEntityDescription):
"""Describes an Envoy CT sensor entity."""
value_fn: Callable[
[EnvoyMeterData],
[EnvoyMeterData | None],
int | float | str | CtType | CtMeterStatus | CtStatusFlags | CtState | None,
]
on_phase: str | None = None
@@ -586,7 +586,9 @@ CT_SENSORS = (
translation_key=(translation_key if translation_key != "" else key),
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
value_fn=lambda ct: (
0 if ct is None or ct.status_flags is None else len(ct.status_flags)
),
cttype=cttype,
)
for cttype, key, translation_key in (
@@ -1020,7 +1022,9 @@ async def async_setup_entry(
) -> None:
"""Set up envoy sensor platform."""
coordinator = config_entry.runtime_data
envoy_data = coordinator.envoy.data
envoy = coordinator.envoy
assert envoy is not None
envoy_data = envoy.data
assert envoy_data is not None
_LOGGER.debug("Envoy data: %s", envoy_data)
@@ -1028,39 +1032,57 @@ async def async_setup_entry(
EnvoyProductionEntity(coordinator, description)
for description in PRODUCTION_SENSORS
]
if envoy_data.system_consumption:
# add unconditionally if TOTAL_CONSUMPTION is available to overcome
# None value at startup caused by envoy fw issues
if envoy.supported_features & SupportedFeatures.TOTAL_CONSUMPTION:
entities.extend(
EnvoyConsumptionEntity(coordinator, description)
for description in CONSUMPTION_SENSORS
)
if envoy_data.system_net_consumption:
# add unconditionally if NET_CONSUMPTION is available to overcome
# None value at startup caused by envoy fw issues
if envoy.supported_features & SupportedFeatures.NET_CONSUMPTION:
entities.extend(
EnvoyNetConsumptionEntity(coordinator, description)
for description in NET_CONSUMPTION_SENSORS
)
# For each production phase reported add production entities
if envoy_data.system_production_phases:
# if PRODUCTION is available and phases detected even if None
# to overcome None value at startup caused by envoy fw issues
if envoy.active_phase_count and (
envoy.supported_features & SupportedFeatures.PRODUCTION
):
entities.extend(
EnvoyProductionPhaseEntity(coordinator, description)
for use_phase, phase in envoy_data.system_production_phases.items()
for index, use_phase in enumerate(PHASENAMES)
for description in PRODUCTION_PHASE_SENSORS[use_phase]
if phase is not None
if index < (envoy.phase_count if envoy.phase_count > 1 else 0)
)
# For each consumption phase reported add consumption entities
if envoy_data.system_consumption_phases:
# if TOTAL_CONSUMPTION is available and phases detected even if None
# to overcome None value at startup caused by envoy fw issues
if (
envoy.active_phase_count
and envoy.phase_count > 1
and (envoy.supported_features & SupportedFeatures.TOTAL_CONSUMPTION)
):
entities.extend(
EnvoyConsumptionPhaseEntity(coordinator, description)
for use_phase, phase in envoy_data.system_consumption_phases.items()
for index, use_phase in enumerate(PHASENAMES)
for description in CONSUMPTION_PHASE_SENSORS[use_phase]
if phase is not None
if index < (envoy.phase_count if envoy.phase_count > 1 else 0)
)
# For each net_consumption phase reported add consumption entities
if envoy_data.system_net_consumption_phases:
# if NET_CONSUMPTION is available and phases detected even if None
# to overcome None value at startup caused by envoy fw issues
if envoy.active_phase_count and (
envoy.supported_features & SupportedFeatures.NET_CONSUMPTION
):
entities.extend(
EnvoyNetConsumptionPhaseEntity(coordinator, description)
for use_phase, phase in envoy_data.system_net_consumption_phases.items()
for index, use_phase in enumerate(PHASENAMES)
for description in NET_CONSUMPTION_PHASE_SENSORS[use_phase]
if phase is not None
if index < (envoy.phase_count if envoy.phase_count > 1 else 0)
)
# Add Current Transformer entities
if envoy_data.ctmeters:
@@ -1181,8 +1203,8 @@ class EnvoyProductionEntity(EnvoySystemSensorEntity):
@override
def native_value(self) -> int | None:
"""Return the state of the sensor."""
system_production = self.data.system_production
assert system_production is not None
if (system_production := self.data.system_production) is None:
return None
return self.entity_description.value_fn(system_production)
@@ -1195,8 +1217,8 @@ class EnvoyConsumptionEntity(EnvoySystemSensorEntity):
@override
def native_value(self) -> int | None:
"""Return the state of the sensor."""
system_consumption = self.data.system_consumption
assert system_consumption is not None
if (system_consumption := self.data.system_consumption) is None:
return None
return self.entity_description.value_fn(system_consumption)
@@ -1209,8 +1231,8 @@ class EnvoyNetConsumptionEntity(EnvoySystemSensorEntity):
@override
def native_value(self) -> int | None:
"""Return the state of the sensor."""
system_net_consumption = self.data.system_net_consumption
assert system_net_consumption is not None
if (system_net_consumption := self.data.system_net_consumption) is None:
return None
return self.entity_description.value_fn(system_net_consumption)
@@ -1225,8 +1247,11 @@ class EnvoyProductionPhaseEntity(EnvoySystemSensorEntity):
"""Return the state of the sensor."""
if TYPE_CHECKING:
assert self.entity_description.on_phase
assert self.data.system_production_phases
if self.data.system_production_phases is None:
return None
if self.entity_description.on_phase not in self.data.system_production_phases:
return None
if (
system_production := self.data.system_production_phases[
self.entity_description.on_phase
@@ -1247,8 +1272,11 @@ class EnvoyConsumptionPhaseEntity(EnvoySystemSensorEntity):
"""Return the state of the sensor."""
if TYPE_CHECKING:
assert self.entity_description.on_phase
assert self.data.system_consumption_phases
if self.data.system_consumption_phases is None:
return None
if self.entity_description.on_phase not in self.data.system_consumption_phases:
return None
if (
system_consumption := self.data.system_consumption_phases[
self.entity_description.on_phase
@@ -1269,8 +1297,14 @@ class EnvoyNetConsumptionPhaseEntity(EnvoySystemSensorEntity):
"""Return the state of the sensor."""
if TYPE_CHECKING:
assert self.entity_description.on_phase
assert self.data.system_net_consumption_phases
if self.data.system_net_consumption_phases is None:
return None
if (
self.entity_description.on_phase
not in self.data.system_net_consumption_phases
):
return None
if (
system_net_consumption := self.data.system_net_consumption_phases[
self.entity_description.on_phase
@@ -1293,6 +1327,8 @@ class EnvoyCTEntity(EnvoySystemSensorEntity):
"""Return the state of the CT sensor."""
if (cttype := self.entity_description.cttype) not in self.data.ctmeters:
return None
if self.data.ctmeters[cttype] is None:
return None
return self.entity_description.value_fn(self.data.ctmeters[cttype])
@@ -1315,6 +1351,8 @@ class EnvoyCTPhaseEntity(EnvoySystemSensorEntity):
cttype
]:
return None
if self.data.ctmeters_phases[cttype][phase] is None:
return None
return self.entity_description.value_fn(
self.data.ctmeters_phases[cttype][phase]
)
+1 -1
View File
@@ -2171,7 +2171,7 @@ pyegps==0.2.5
pyemoncms==0.1.3
# homeassistant.components.enphase_envoy
pyenphase==3.2.1
pyenphase==4.0.0
# homeassistant.components.envertech_evt800
pyenvertechevt800==0.2.4
+11 -7
View File
@@ -206,8 +206,8 @@ def _load_json_2_production_data(
if item := json_fixture["data"].get("system_consumption_phases"):
mocked_data.system_consumption_phases = {}
for sub_item, item_data in item.items():
mocked_data.system_consumption_phases[sub_item] = EnvoySystemConsumption(
**item_data
mocked_data.system_consumption_phases[sub_item] = (
None if not item_data else EnvoySystemConsumption(**item_data)
)
if item := json_fixture["data"].get("system_net_consumption_phases"):
mocked_data.system_net_consumption_phases = {}
@@ -218,8 +218,8 @@ def _load_json_2_production_data(
if item := json_fixture["data"].get("system_production_phases"):
mocked_data.system_production_phases = {}
for sub_item, item_data in item.items():
mocked_data.system_production_phases[sub_item] = EnvoySystemProduction(
**item_data
mocked_data.system_production_phases[sub_item] = (
None if not item_data else EnvoySystemProduction(**item_data)
)
if item := json_fixture["data"].get("acb_power"):
mocked_data.acb_power = EnvoyACBPower(**item)
@@ -232,15 +232,19 @@ def _load_json_2_meter_data(
if meters := json_fixture["data"].get("ctmeters"):
mocked_data.ctmeters = {}
[
mocked_data.ctmeters.update({meter: EnvoyMeterData(**meter_data)})
mocked_data.ctmeters.update(
{meter: None if not meter_data else EnvoyMeterData(**meter_data)}
)
for meter, meter_data in meters.items()
]
if meters := json_fixture["data"].get("ctmeters_phases"):
mocked_data.ctmeters_phases = {}
for meter, meter_data in meters.items():
meter_phase_data: dict[str, EnvoyMeterData] = {}
meter_phase_data: dict[str, EnvoyMeterData | None] = {}
[
meter_phase_data.update({phase: EnvoyMeterData(**phase_data)})
meter_phase_data.update(
{phase: None if not phase_data else EnvoyMeterData(**phase_data)}
)
for phase, phase_data in meter_data.items()
]
mocked_data.ctmeters_phases.update({meter: meter_phase_data})
@@ -3,7 +3,7 @@
"firmware": "7.1.2",
"part_number": "123456789",
"envoy_model": "Envoy, phases: 3, phase mode: split, net-consumption CT, production CT, storage CT",
"supported_features": 1659,
"supported_features": 1663,
"phase_mode": "three",
"phase_count": 3,
"active_phase_count": 3,
@@ -0,0 +1,639 @@
{
"serial_number": "1234",
"firmware": "7.1.2",
"part_number": "123456789",
"envoy_model": "Envoy, phases: 3, phase mode: split, net-consumption CT, production CT, storage CT",
"supported_features": 1663,
"phase_mode": "three",
"phase_count": 3,
"active_phase_count": 3,
"ct_meter_count": 2,
"consumption_meter_type": "net-consumption",
"production_meter_type": "production",
"storage_meter_type": "storage",
"data": {
"encharge_inventory": {
"123456": {
"admin_state": 6,
"admin_state_str": "ENCHG_STATE_READY",
"bmu_firmware_version": "2.1.34",
"comm_level_2_4_ghz": 4,
"comm_level_sub_ghz": 4,
"communicating": true,
"dc_switch_off": false,
"encharge_capacity": 3500,
"encharge_revision": 2,
"firmware_loaded_date": 1695330323,
"firmware_version": "2.6.5973_rel/22.11",
"installed_date": 1695330323,
"last_report_date": 1695769447,
"led_status": 17,
"max_cell_temp": 30,
"operating": true,
"part_number": "830-01760-r37",
"percent_full": 15,
"serial_number": "123456",
"temperature": 29,
"temperature_unit": "C",
"zigbee_dongle_fw_version": "100F"
}
},
"encharge_power": {
"123456": {
"apparent_power_mva": 0,
"real_power_mw": 0,
"soc": 15
}
},
"encharge_aggregate": {
"available_energy": 525,
"backup_reserve": 526,
"state_of_charge": 15,
"reserve_state_of_charge": 15,
"configured_reserve_state_of_charge": 15,
"max_available_capacity": 3500
},
"enpower": {
"grid_mode": "multimode-ongrid",
"admin_state": 24,
"admin_state_str": "ENPWR_STATE_OPER_CLOSED",
"comm_level_2_4_ghz": 5,
"comm_level_sub_ghz": 5,
"communicating": true,
"firmware_loaded_date": 1695330323,
"firmware_version": "1.2.2064_release/20.34",
"installed_date": 1695330323,
"last_report_date": 1695769447,
"mains_admin_state": "closed",
"mains_oper_state": "closed",
"operating": true,
"part_number": "830-01760-r37",
"serial_number": "654321",
"temperature": 79,
"temperature_unit": "F",
"zigbee_dongle_fw_version": "1009"
},
"system_consumption": null,
"system_net_consumption": {
"watt_hours_lifetime": 4321,
"watt_hours_last_7_days": -1,
"watt_hours_today": -1,
"watts_now": 2341
},
"system_production": null,
"system_consumption_phases": {
"L1": null,
"L2": null,
"L3": null
},
"system_net_consumption_phases": {
"L1": {
"watt_hours_lifetime": 1321,
"watt_hours_last_7_days": -1,
"watt_hours_today": -1,
"watts_now": 12341
},
"L2": {
"watt_hours_lifetime": 2321,
"watt_hours_last_7_days": -1,
"watt_hours_today": -1,
"watts_now": 22341
},
"L3": {
"watt_hours_lifetime": 3321,
"watt_hours_last_7_days": -1,
"watt_hours_today": -1,
"watts_now": 32341
}
},
"system_production_phases": {
"L1": null,
"L2": null,
"L3": null
},
"ctmeters": {
"production": {
"eid": "100000010",
"timestamp": 1708006110,
"energy_delivered": 11234,
"energy_received": 12345,
"active_power": 100,
"power_factor": 0.11,
"voltage": 111,
"current": 0.2,
"frequency": 50.1,
"state": "enabled",
"measurement_type": "production",
"metering_status": "normal",
"status_flags": ["production-imbalance", "power-on-unused-phase"]
},
"net-consumption": {
"eid": "100000020",
"timestamp": 1708006120,
"energy_delivered": 21234,
"energy_received": 22345,
"active_power": 101,
"power_factor": 0.21,
"voltage": 112,
"current": 0.3,
"frequency": 50.2,
"state": "enabled",
"measurement_type": "net-consumption",
"metering_status": "normal",
"status_flags": []
},
"storage": null,
"backfeed": null,
"load": {
"eid": "100000050",
"timestamp": 1708006120,
"energy_delivered": 51234,
"energy_received": 52345,
"active_power": 105,
"power_factor": 0.25,
"voltage": 115,
"current": 0.6,
"frequency": 50.6,
"state": "enabled",
"measurement_type": "load",
"metering_status": "normal",
"status_flags": []
},
"evse": {
"eid": "100000060",
"timestamp": 1708006120,
"energy_delivered": 61234,
"energy_received": 62345,
"active_power": 106,
"power_factor": 0.26,
"voltage": 116,
"current": 0.7,
"frequency": 50.7,
"state": "enabled",
"measurement_type": "evse",
"metering_status": "normal",
"status_flags": []
},
"pv3p": {
"eid": "100000070",
"timestamp": 1708006120,
"energy_delivered": 71234,
"energy_received": 72345,
"active_power": 107,
"power_factor": 0.27,
"voltage": 117,
"current": 0.8,
"frequency": 50.8,
"state": "enabled",
"measurement_type": "pv3p",
"metering_status": "normal",
"status_flags": []
}
},
"ctmeters_phases": {
"production": {
"L1": {
"eid": "100000011",
"timestamp": 1708006111,
"energy_delivered": 112341,
"energy_received": 123451,
"active_power": 20,
"power_factor": 0.12,
"voltage": 111,
"current": 0.2,
"frequency": 50.1,
"state": "enabled",
"measurement_type": "production",
"metering_status": "normal",
"status_flags": ["production-imbalance"]
},
"L2": {
"eid": "100000012",
"timestamp": 1708006112,
"energy_delivered": 112342,
"energy_received": 123452,
"active_power": 30,
"power_factor": 0.13,
"voltage": 111,
"current": 0.2,
"frequency": 50.1,
"state": "enabled",
"measurement_type": "production",
"metering_status": "normal",
"status_flags": ["power-on-unused-phase"]
},
"L3": {
"eid": "100000013",
"timestamp": 1708006113,
"energy_delivered": 112343,
"energy_received": 123453,
"active_power": 50,
"power_factor": 0.14,
"voltage": 111,
"current": 0.2,
"frequency": 50.1,
"state": "enabled",
"measurement_type": "production",
"metering_status": "normal",
"status_flags": []
}
},
"net-consumption": {
"L1": {
"eid": "100000021",
"timestamp": 1708006121,
"energy_delivered": 212341,
"energy_received": 223451,
"active_power": 21,
"power_factor": 0.22,
"voltage": 112,
"current": 0.3,
"frequency": 50.2,
"state": "enabled",
"measurement_type": "net-consumption",
"metering_status": "normal",
"status_flags": []
},
"L2": {
"eid": "100000022",
"timestamp": 1708006122,
"energy_delivered": 212342,
"energy_received": 223452,
"active_power": 31,
"power_factor": 0.23,
"voltage": 112,
"current": 0.3,
"frequency": 50.2,
"state": "enabled",
"measurement_type": "net-consumption",
"metering_status": "normal",
"status_flags": []
},
"L3": {
"eid": "100000023",
"timestamp": 1708006123,
"energy_delivered": 212343,
"energy_received": 223453,
"active_power": 51,
"power_factor": 0.24,
"voltage": 112,
"current": 0.3,
"frequency": 50.2,
"state": "enabled",
"measurement_type": "net-consumption",
"metering_status": "normal",
"status_flags": []
}
},
"storage": {
"L1": null,
"L2": {
"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": "enabled",
"measurement_type": "storage",
"metering_status": "normal",
"status_flags": []
},
"L3": {
"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": "enabled",
"measurement_type": "storage",
"metering_status": "normal",
"status_flags": []
}
},
"backfeed": {
"L1": null,
"L2": null,
"L3": null
},
"load": {
"L1": {
"eid": "100000051",
"timestamp": 1708006121,
"energy_delivered": 512341,
"energy_received": 523451,
"active_power": 115,
"power_factor": 0.25,
"voltage": 115,
"current": 5.1,
"frequency": 50.6,
"state": "enabled",
"measurement_type": "load",
"metering_status": "normal",
"status_flags": []
},
"L2": {
"eid": "100000052",
"timestamp": 1708006122,
"energy_delivered": 512342,
"energy_received": 523452,
"active_power": 125,
"power_factor": 0.25,
"voltage": 115,
"current": 5.2,
"frequency": 50.6,
"state": "enabled",
"measurement_type": "load",
"metering_status": "normal",
"status_flags": []
},
"L3": {
"eid": "100000052",
"timestamp": 1708006123,
"energy_delivered": 512343,
"energy_received": 523453,
"active_power": 135,
"power_factor": 0.25,
"voltage": 115,
"current": 5.3,
"frequency": 50.6,
"state": "enabled",
"measurement_type": "load",
"metering_status": "normal",
"status_flags": []
}
},
"evse": {
"L1": {
"eid": "100000061",
"timestamp": 1708006121,
"energy_delivered": 612341,
"energy_received": 623451,
"active_power": 116,
"power_factor": 0.26,
"voltage": 116,
"current": 6.1,
"frequency": 50.6,
"state": "enabled",
"measurement_type": "evse",
"metering_status": "normal",
"status_flags": []
},
"L2": {
"eid": "100000062",
"timestamp": 1708006122,
"energy_delivered": 612342,
"energy_received": 623452,
"active_power": 126,
"power_factor": 0.26,
"voltage": 116,
"current": 6.2,
"frequency": 50.6,
"state": "enabled",
"measurement_type": "evse",
"metering_status": "normal",
"status_flags": []
},
"L3": {
"eid": "100000063",
"timestamp": 1708006123,
"energy_delivered": 612343,
"energy_received": 623453,
"active_power": 136,
"power_factor": 0.26,
"voltage": 116,
"current": 6.3,
"frequency": 50.6,
"state": "enabled",
"measurement_type": "evse",
"metering_status": "normal",
"status_flags": []
}
},
"pv3p": {
"L1": {
"eid": "100000071",
"timestamp": 1708006127,
"energy_delivered": 712341,
"energy_received": 723451,
"active_power": 117,
"power_factor": 0.27,
"voltage": 117,
"current": 7.1,
"frequency": 50.7,
"state": "enabled",
"measurement_type": "pv3p",
"metering_status": "normal",
"status_flags": []
},
"L2": {
"eid": "100000072",
"timestamp": 1708006122,
"energy_delivered": 712342,
"energy_received": 723452,
"active_power": 127,
"power_factor": 0.27,
"voltage": 117,
"current": 7.2,
"frequency": 50.7,
"state": "enabled",
"measurement_type": "pv3p",
"metering_status": "normal",
"status_flags": []
},
"L3": {
"eid": "100000073",
"timestamp": 1708006123,
"energy_delivered": 712343,
"energy_received": 723453,
"active_power": 137,
"power_factor": 0.27,
"voltage": 117,
"current": 7.3,
"frequency": 50.7,
"state": "enabled",
"measurement_type": "pv3p",
"metering_status": "normal",
"status_flags": []
}
}
},
"dry_contact_status": {
"NC1": {
"id": "NC1",
"status": "open"
},
"NC2": {
"id": "NC2",
"status": "closed"
},
"NC3": {
"id": "NC3",
"status": "open"
}
},
"dry_contact_settings": {
"NC1": {
"id": "NC1",
"black_start": 5.0,
"essential_end_time": 32400.0,
"essential_start_time": 57600.0,
"generator_action": "shed",
"grid_action": "shed",
"load_name": "NC1 Fixture",
"manual_override": true,
"micro_grid_action": "shed",
"mode": "manual",
"override": true,
"priority": 1.0,
"pv_serial_nb": [],
"soc_high": 70.0,
"soc_low": 25.0,
"type": "LOAD"
},
"NC2": {
"id": "NC2",
"black_start": 5.0,
"essential_end_time": 57600.0,
"essential_start_time": 32400.0,
"generator_action": "shed",
"grid_action": "apply",
"load_name": "NC2 Fixture",
"manual_override": true,
"micro_grid_action": "shed",
"mode": "manual",
"override": true,
"priority": 2.0,
"pv_serial_nb": [],
"soc_high": 70.0,
"soc_low": 30.0,
"type": "LOAD"
},
"NC3": {
"id": "NC3",
"black_start": 5.0,
"essential_end_time": 57600.0,
"essential_start_time": 32400.0,
"generator_action": "apply",
"grid_action": "shed",
"load_name": "NC3 Fixture",
"manual_override": true,
"micro_grid_action": "apply",
"mode": "manual",
"override": true,
"priority": 3.0,
"pv_serial_nb": [],
"soc_high": 70.0,
"soc_low": 30.0,
"type": "NONE"
}
},
"collar": {
"admin_state": 88,
"admin_state_str": "ENCMN_MDE_ON_GRID",
"firmware_loaded_date": 1752939759,
"firmware_version": "3.0.6-D0",
"installed_date": 1752939759,
"last_report_date": 1752939759,
"communicating": true,
"mid_state": "close",
"grid_state": "on_grid",
"part_number": "865-00400-r22",
"serial_number": "482520020939",
"temperature": 42,
"temperature_unit": "C",
"control_error": 0,
"collar_state": "Installed"
},
"c6cc": {
"admin_state": 82,
"admin_state_str": "ENCMN_C6_CC_READY",
"firmware_loaded_date": 1752945451,
"firmware_version": "0.1.20-D1",
"installed_date": 1752945451,
"last_report_date": 1752945451,
"communicating": true,
"part_number": "800-02403-r08",
"serial_number": "482523040549",
"dmir_version": "0.1.20-D1"
},
"inverters": {
"1": {
"serial_number": "1",
"last_report_date": 1,
"last_report_watts": 1,
"max_report_watts": 1,
"dc_voltage": null,
"dc_current": null,
"ac_voltage": null,
"ac_current": null,
"ac_frequency": null,
"temperature": null,
"energy_produced": null,
"energy_today": null,
"lifetime_energy": null,
"last_report_duration": null
}
},
"tariff": {
"currency": {
"code": "EUR"
},
"logger": "mylogger",
"date": "1695744220",
"storage_settings": {
"mode": "self-consumption",
"operation_mode_sub_type": "",
"reserved_soc": 15.0,
"very_low_soc": 5,
"charge_from_grid": true,
"date": "1695598084",
"opt_schedules": true
},
"single_rate": {
"rate": 0.0,
"sell": 0.0
},
"seasons": [
{
"id": "season_1",
"start": "1/1",
"days": [
{
"id": "all_days",
"days": "Mon,Tue,Wed,Thu,Fri,Sat,Sun",
"must_charge_start": 444,
"must_charge_duration": 35,
"must_charge_mode": "CG",
"enable_discharge_to_grid": true,
"periods": [
{
"id": "period_1",
"start": 480,
"rate": 0.1898
},
{
"id": "filler",
"start": 1320,
"rate": 0.1034
}
]
}
],
"tiers": []
}
],
"seasons_sell": []
},
"raw": {
"varies_by": "firmware_version"
}
}
}
@@ -3,7 +3,7 @@
"firmware": "7.6.175",
"part_number": "123456789",
"envoy_model": "Envoy, phases: 1, phase mode: three, total-consumption CT, production CT",
"supported_features": 1217,
"supported_features": 1231,
"phase_mode": "three",
"phase_count": 1,
"active_phase_count": 0,
@@ -19923,6 +19923,7 @@
'supported_features': list([
'INVERTERS',
'METERING',
'TOTAL_CONSUMPTION',
'NET_CONSUMPTION',
'ENCHARGE',
'ENPOWER',
@@ -43540,6 +43540,67 @@
'state': '2.341',
})
# ---
# name: test_sensor[envoy_tot_cons_metered][sensor.envoy_1234_current_power_consumption-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.envoy_1234_current_power_consumption',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Current power consumption',
'options': dict({
'sensor': dict({
'suggested_display_precision': 3,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Current power consumption',
'platform': 'enphase_envoy',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'current_power_consumption',
'unique_id': '1234_consumption',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
})
# ---
# name: test_sensor[envoy_tot_cons_metered][sensor.envoy_1234_current_power_consumption-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'power',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Envoy 1234 Current power consumption',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfPower.KILO_WATT: 'kW'>,
}),
'context': <ANY>,
'entity_id': 'sensor.envoy_1234_current_power_consumption',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensor[envoy_tot_cons_metered][sensor.envoy_1234_current_power_production-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -43601,6 +43662,125 @@
'state': '1.234',
})
# ---
# name: test_sensor[envoy_tot_cons_metered][sensor.envoy_1234_energy_consumption_last_seven_days-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Energy consumption last seven days',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy consumption last seven days',
'platform': 'enphase_envoy',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'seven_days_consumption',
'unique_id': '1234_seven_days_consumption',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_sensor[envoy_tot_cons_metered][sensor.envoy_1234_energy_consumption_last_seven_days-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Envoy 1234 Energy consumption last seven days',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensor[envoy_tot_cons_metered][sensor.envoy_1234_energy_consumption_today-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.envoy_1234_energy_consumption_today',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Energy consumption today',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy consumption today',
'platform': 'enphase_envoy',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'daily_consumption',
'unique_id': '1234_daily_consumption',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_sensor[envoy_tot_cons_metered][sensor.envoy_1234_energy_consumption_today-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Envoy 1234 Energy consumption today',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.envoy_1234_energy_consumption_today',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensor[envoy_tot_cons_metered][sensor.envoy_1234_energy_production_last_seven_days-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -43897,6 +44077,67 @@
'state': '4.321',
})
# ---
# name: test_sensor[envoy_tot_cons_metered][sensor.envoy_1234_lifetime_energy_consumption-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Lifetime energy consumption',
'options': dict({
'sensor': dict({
'suggested_display_precision': 3,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.MEGA_WATT_HOUR: 'MWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Lifetime energy consumption',
'platform': 'enphase_envoy',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'lifetime_consumption',
'unique_id': '1234_lifetime_consumption',
'unit_of_measurement': <UnitOfEnergy.MEGA_WATT_HOUR: 'MWh'>,
})
# ---
# name: test_sensor[envoy_tot_cons_metered][sensor.envoy_1234_lifetime_energy_consumption-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Envoy 1234 Lifetime energy consumption',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.MEGA_WATT_HOUR: 'MWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensor[envoy_tot_cons_metered][sensor.envoy_1234_lifetime_energy_production-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
+267 -2
View File
@@ -6,13 +6,14 @@ from typing import Any
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
from pyenphase import EnvoyData
from pyenphase.const import PHASENAMES, PhaseNames
from pyenphase.models.acb import ACBChargeStatus, EnvoyACB
from pyenphase.models.meters import CtType
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.enphase_envoy.const import Platform
from homeassistant.components.enphase_envoy.const import DOMAIN, Platform
from homeassistant.components.enphase_envoy.coordinator import SCAN_INTERVAL
from homeassistant.components.enphase_envoy.sensor import aggregate_acb_sleep_state
from homeassistant.components.sensor import SensorStateClass
@@ -23,8 +24,14 @@ from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import TemperatureConverter
from . import setup_integration
from .conftest import _load_json_2_meter_data, _load_json_2_production_data
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
load_json_object_fixture,
snapshot_platform,
)
@pytest.mark.parametrize(
@@ -1413,6 +1420,264 @@ async def test_sensor_missing_data(
assert entity_state.state == STATE_UNKNOWN
def reference_fixture(fixture: str) -> EnvoyData:
"""Load reference fixture in envoy data model."""
reference_data = EnvoyData()
json_fixture: dict[str, Any] = load_json_object_fixture(f"{fixture}.json", DOMAIN)
_load_json_2_production_data(reference_data, json_fixture)
_load_json_2_meter_data(reference_data, json_fixture)
return reference_data
@pytest.mark.parametrize(
("mock_envoy", "ref_fixture"),
[
(
"envoy_metered_batt_relay_none",
"envoy_metered_batt_relay",
)
],
indirect=["mock_envoy"],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensor_load_none_data(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_envoy: AsyncMock,
ref_fixture: str,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test enphase_envoy sensor platform load None data handling."""
with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, config_entry)
ENTITY_BASE = f"{Platform.SENSOR}.envoy_{mock_envoy.serial_number}"
# these have None data and should show up as unknown
for entity in (
"lifetime_energy_production",
"lifetime_energy_consumption",
"current_battery_discharge",
"backfeed_ct_energy_delivered",
"lifetime_energy_production_l1",
"lifetime_energy_consumption_l1",
"backfeed_ct_energy_delivered_l1",
"current_battery_discharge_l1",
):
assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{entity}"))
assert entity_state.state == STATE_UNKNOWN
# restore None data to operational state
reference_data = reference_fixture(ref_fixture)
mock_envoy.data.system_production = reference_data.system_production
mock_envoy.data.system_consumption = reference_data.system_consumption
mock_envoy.data.ctmeters[CtType.BACKFEED] = reference_data.ctmeters[CtType.BACKFEED]
mock_envoy.data.ctmeters[CtType.STORAGE] = reference_data.ctmeters[CtType.STORAGE]
mock_envoy.data.system_production_phases = reference_data.system_production_phases
mock_envoy.data.system_consumption_phases = reference_data.system_consumption_phases
mock_envoy.data.ctmeters_phases[CtType.BACKFEED] = reference_data.ctmeters_phases[
CtType.BACKFEED
]
mock_envoy.data.ctmeters_phases[CtType.STORAGE][PhaseNames.PHASE_1] = (
reference_data.ctmeters_phases[CtType.STORAGE][PhaseNames.PHASE_1]
)
# force HA to detect changed data by changing raw
mock_envoy.data.raw = {"I": "am changed"}
# Move time to next update
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# all these should now no longer be in unknown state
for entity in (
"lifetime_energy_production",
"lifetime_energy_consumption",
"current_battery_discharge",
"backfeed_ct_energy_delivered",
"lifetime_energy_production_l1",
"lifetime_energy_consumption_l1",
"backfeed_ct_energy_delivered_l1",
"current_battery_discharge_l1",
):
assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{entity}"))
assert entity_state.state != STATE_UNKNOWN
@pytest.mark.parametrize(
("mock_envoy"),
[
"envoy_metered_batt_relay",
],
indirect=["mock_envoy"],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensor_none_data(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_envoy: AsyncMock,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test enphase_envoy sensor platform None data handling."""
with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, config_entry)
ENTITY_BASE = f"{Platform.SENSOR}.envoy_{mock_envoy.serial_number}"
for entity in (
"lifetime_energy_production",
"lifetime_energy_consumption",
"lifetime_balanced_net_energy_consumption",
"backfeed_ct_energy_delivered",
"lifetime_energy_production_l1",
"lifetime_energy_consumption_l1",
"lifetime_balanced_net_energy_consumption_l1",
):
assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{entity}"))
# force None data to test 'if == none' code sections
mock_envoy.data.system_production = None
mock_envoy.data.system_consumption = None
mock_envoy.data.system_net_consumption = None
mock_envoy.data.ctmeters[CtType.BACKFEED] = None
mock_envoy.data.system_production_phases = None
mock_envoy.data.system_consumption_phases = None
mock_envoy.data.system_net_consumption_phases = None
# force HA to detect changed data by changing raw
mock_envoy.data.raw = {"I": "am changed"}
# Move time to next update
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# all these should now be in unknown state
for entity in (
"lifetime_energy_production",
"lifetime_energy_consumption",
"lifetime_balanced_net_energy_consumption",
"backfeed_ct_energy_delivered",
"lifetime_energy_production_l1",
"lifetime_energy_consumption_l1",
"lifetime_balanced_net_energy_consumption_l1",
):
assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{entity}"))
assert entity_state.state == STATE_UNKNOWN
@pytest.mark.parametrize(
("mock_envoy"),
[
"envoy_metered_batt_relay",
],
indirect=["mock_envoy"],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensor_phase_values_none_data(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_envoy: AsyncMock,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test enphase_envoy sensor platform phase None data handling."""
with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, config_entry)
ENTITY_BASE = f"{Platform.SENSOR}.envoy_{mock_envoy.serial_number}"
for entity in (
"lifetime_energy_production_l1",
"lifetime_energy_consumption_l1",
"lifetime_balanced_net_energy_consumption_l1",
"backfeed_ct_energy_delivered_l1",
):
assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{entity}"))
# force None data to test 'if == none' code sections
mock_envoy.data.system_production_phases[PhaseNames.PHASE_1] = None
mock_envoy.data.system_consumption_phases[PhaseNames.PHASE_1] = None
mock_envoy.data.system_net_consumption_phases[PhaseNames.PHASE_1] = None
mock_envoy.data.ctmeters_phases[CtType.BACKFEED][PhaseNames.PHASE_1] = None
# force HA to detect changed data by changing raw
mock_envoy.data.raw = {"I": "am changed"}
# Move time to next update
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# all these should now be in unknown state
for entity in (
"lifetime_energy_production_l1",
"lifetime_energy_consumption_l1",
"lifetime_balanced_net_energy_consumption_l1",
"backfeed_ct_energy_delivered_l1",
):
assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{entity}"))
assert entity_state.state == STATE_UNKNOWN
@pytest.mark.parametrize(
("mock_envoy"),
[
"envoy_metered_batt_relay",
],
indirect=["mock_envoy"],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensor_phase_values_missing_data(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_envoy: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test enphase_envoy sensor platform missing phase data handling."""
with patch("homeassistant.components.enphase_envoy.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, config_entry)
ENTITY_BASE = f"{Platform.SENSOR}.envoy_{mock_envoy.serial_number}"
for entity in (
"lifetime_energy_production_l1",
"lifetime_energy_consumption_l1",
"lifetime_balanced_net_energy_consumption_l1",
"backfeed_ct_energy_delivered_l1",
):
assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{entity}"))
# test handling of missing phase data
del mock_envoy.data.system_production_phases[PhaseNames.PHASE_1]
del mock_envoy.data.system_consumption_phases[PhaseNames.PHASE_1]
del mock_envoy.data.system_net_consumption_phases[PhaseNames.PHASE_1]
del mock_envoy.data.ctmeters_phases[CtType.BACKFEED][PhaseNames.PHASE_1]
# force HA to detect changed data by changing raw
mock_envoy.data.raw = {"I": "am changed"}
# Move time to next update
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# all these should now be in unknown state
for entity in (
"lifetime_energy_production_l1",
"lifetime_energy_consumption_l1",
"lifetime_balanced_net_energy_consumption_l1",
"backfeed_ct_energy_delivered_l1",
):
assert (entity_state := hass.states.get(f"{ENTITY_BASE}_{entity}"))
assert entity_state.state == STATE_UNKNOWN
@pytest.mark.parametrize(
("mock_envoy"),
[