Skip to content

Commit a778885

Browse files
optimusbastitolwi
authored andcommitted
feat(stream): per-PV sensors via firmware-agnostic dual-path mapping
The Stream family firmware >=1.0.1.88 stopped emitting per-PV powGetPv1..4 fields - they now publish plugInInfoPv{1-4}Amp + plugInInfoPv{1-4}Vol instead. This patch adds a StreamPvWattsSensorEntity helper that: - Subscribes to plugInInfoPv*Amp keys - Auto-derives the matching Vol key by suffix replacement - Computes Watts on-the-fly via Amp x Vol multiplication - Injects the result under a synthetic mqtt_key (pvWatts_*) to avoid unique_id collision with the legitimate AmpSensorEntity subscriber - Delegates to the parent WattsSensorEntity for auto-enable, attribute mapping, and energy() helper integration Backward-compatible: existing powGetPv* sensors remain (older firmware). New sensors are auto-enabled only when the new firmware fields appear. Closes #584
1 parent 39a00a9 commit a778885

4 files changed

Lines changed: 129 additions & 0 deletions

File tree

custom_components/ecoflow_cloud/devices/const.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,12 @@
341341
STREAM_POWER_PV_4 = "Power PV 4"
342342
STREAM_IN_AMPS_PV_1 = "Power PV1 In Amps"
343343
STREAM_IN_AMPS_PV_2 = "Power PV2 In Amps"
344+
STREAM_IN_AMPS_PV_3 = "Power PV3 In Amps"
345+
STREAM_IN_AMPS_PV_4 = "Power PV4 In Amps"
344346
STREAM_IN_VOL_PV_1 = "Power PV1 Volts"
345347
STREAM_IN_VOL_PV_2 = "Power PV2 Volts"
348+
STREAM_IN_VOL_PV_3 = "Power PV3 Volts"
349+
STREAM_IN_VOL_PV_4 = "Power PV4 Volts"
346350
STREAM_POWER_PV_SUM = "Power PV Sum"
347351
STREAM_GET_SYS_LOAD = "Power Sys Load" # powGetSysLoad
348352
STREAM_GET_SYS_LOAD_FROM_BP = "Power Sys Load From Battery" # powGetSysLoadFromBp

custom_components/ecoflow_cloud/devices/public/stream_ac.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from custom_components.ecoflow_cloud.devices.public.data_bridge import to_plain
1111
from custom_components.ecoflow_cloud.number import BatteryBackupLevel
1212
from custom_components.ecoflow_cloud.sensor import (
13+
AmpSensorEntity,
1314
CapacitySensorEntity,
1415
CumulativeCapacitySensorEntity,
1516
CyclesSensorEntity,
@@ -24,6 +25,9 @@
2425
VoltSensorEntity,
2526
WattsSensorEntity,
2627
)
28+
from custom_components.ecoflow_cloud.devices.public.stream_pv_helpers import (
29+
StreamPvWattsSensorEntity,
30+
)
2731
from custom_components.ecoflow_cloud.switch import EnabledEntity
2832

2933

@@ -178,6 +182,22 @@ def sensors(self, client: EcoflowApiClient) -> list[SensorEntity]:
178182
# "powConsumptionMeasurement": 2,
179183
# "powGetBpCms": 1915.0862,
180184
WattsSensorEntity(client, self, "powGetBpCms", const.STREAM_POWER_BATTERY),
185+
# Per-PV power, voltage, current (Stream Ultra / Ultra X / AC Pro).
186+
# Per-PV reporting depends on the firmware version installed:
187+
# - Firmware < 1.0.1.88: powGetPv / powGetPv2..4 emitted, per-PV
188+
# watts are correct, plugInInfoPv*Amp returns 0.
189+
# - Firmware >= 1.0.1.88: powGetPv* returns 0, per-PV data lives
190+
# in plugInInfoPv{,2,3,4}Amp + plugInInfoPv{,2,3,4}Vol instead.
191+
# See issues #582, #584. To stay firmware-agnostic we register BOTH
192+
# mapping variants with auto_enable=True. HA enables whichever set
193+
# first sees a non-zero value.
194+
#
195+
# New-firmware path (computed amp x vol via StreamPvWattsSensorEntity)
196+
StreamPvWattsSensorEntity(client, self, "plugInInfoPvAmp", const.STREAM_POWER_PV_1, False, True),
197+
StreamPvWattsSensorEntity(client, self, "plugInInfoPv2Amp", const.STREAM_POWER_PV_2, False, True),
198+
StreamPvWattsSensorEntity(client, self, "plugInInfoPv3Amp", const.STREAM_POWER_PV_3, False, True),
199+
StreamPvWattsSensorEntity(client, self, "plugInInfoPv4Amp", const.STREAM_POWER_PV_4, False, True),
200+
# Legacy-firmware path (powGetPv* keys)
181201
# "powGetPv": 0.0,
182202
WattsSensorEntity(client, self, "powGetPv", const.STREAM_POWER_PV_1, False, True),
183203
# "powGetPv2": 0.0,
@@ -186,6 +206,15 @@ def sensors(self, client: EcoflowApiClient) -> list[SensorEntity]:
186206
WattsSensorEntity(client, self, "powGetPv3", const.STREAM_POWER_PV_3, False, True),
187207
# "powGetPv4": 0.0,
188208
WattsSensorEntity(client, self, "powGetPv4", const.STREAM_POWER_PV_4, False, True),
209+
# Per-PV voltage + current (emitted by all Stream firmware versions)
210+
VoltSensorEntity(client, self, "plugInInfoPvVol", const.STREAM_IN_VOL_PV_1, False, True),
211+
VoltSensorEntity(client, self, "plugInInfoPv2Vol", const.STREAM_IN_VOL_PV_2, False, True),
212+
VoltSensorEntity(client, self, "plugInInfoPv3Vol", const.STREAM_IN_VOL_PV_3, False, True),
213+
VoltSensorEntity(client, self, "plugInInfoPv4Vol", const.STREAM_IN_VOL_PV_4, False, True),
214+
AmpSensorEntity(client, self, "plugInInfoPvAmp", const.STREAM_IN_AMPS_PV_1, False, True),
215+
AmpSensorEntity(client, self, "plugInInfoPv2Amp", const.STREAM_IN_AMPS_PV_2, False, True),
216+
AmpSensorEntity(client, self, "plugInInfoPv3Amp", const.STREAM_IN_AMPS_PV_3, False, True),
217+
AmpSensorEntity(client, self, "plugInInfoPv4Amp", const.STREAM_IN_AMPS_PV_4, False, True),
189218
# "powGetPvSum": 2051.3975,
190219
WattsSensorEntity(client, self, "powGetPvSum", const.STREAM_POWER_PV_SUM),
191220
# "powGetSchuko1": 0.0,

custom_components/ecoflow_cloud/devices/public/stream_microinverter.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,23 @@
1616
VoltSensorEntity,
1717
WattsSensorEntity,
1818
)
19+
from custom_components.ecoflow_cloud.devices.public.stream_pv_helpers import (
20+
StreamPvWattsSensorEntity,
21+
)
1922

2023

2124
class StreamMicroinveter(BaseDevice):
2225
def sensors(self, client: EcoflowApiClient) -> list[SensorEntity]:
2326
return [
2427
WattsSensorEntity(client, self, "gridConnectionPower", const.STREAM_POWER_AC),
28+
# Per-PV mapping is firmware-dependent. See stream_ac.py comment
29+
# and issues #582/#584. Both variants are registered with
30+
# auto_enable=True so the integration stays firmware-agnostic.
31+
#
32+
# New-firmware path (computed amp x vol via StreamPvWattsSensorEntity)
33+
StreamPvWattsSensorEntity(client, self, "plugInInfoPvAmp", const.STREAM_POWER_PV_1, False, True),
34+
StreamPvWattsSensorEntity(client, self, "plugInInfoPv2Amp", const.STREAM_POWER_PV_2, False, True),
35+
# Legacy-firmware path (powGetPv* keys)
2536
WattsSensorEntity(client, self, "powGetPv", const.STREAM_POWER_PV_1, False, True),
2637
WattsSensorEntity(client, self, "powGetPv2", const.STREAM_POWER_PV_2, False, True),
2738
VoltSensorEntity(client, self, "gridConnectionVol", const.STREAM_POWER_VOL, False),
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Per-PV computed watts for the EcoFlow Stream family.
2+
3+
Stream family firmware (Stream Ultra, Stream Ultra X, Stream AC Pro, Stream
4+
Microinverter) starting from version 1.0.1.88 stopped emitting the legacy
5+
``powGetPv{N}`` per-PV keys. Instead, per-PV data is now published as
6+
``plugInInfoPv{N}Amp`` and ``plugInInfoPv{N}Vol``. As a consequence, the
7+
upstream ``WattsSensorEntity`` mappings against ``powGetPv*`` stay forever
8+
``unknown`` for devices running the newer firmware.
9+
10+
This module provides a drop-in ``WattsSensorEntity`` subclass that:
11+
12+
1. Computes per-PV watts as ``Amp x Vol`` from the same payload tick.
13+
2. Uses a synthetic ``mqtt_key`` so its ``unique_id`` cannot collide with
14+
an ``AmpSensorEntity`` that legitimately subscribes to the same
15+
``plugInInfoPv{N}Amp`` key for the per-PV current sensor.
16+
3. Auto-derives the ``vol_key`` from the ``amp_key`` via suffix replacement,
17+
so the device file only specifies ``amp_key`` once per PV input - less
18+
verbose, harder to typo.
19+
4. Falls through to the upstream base class for attribute mapping, default
20+
value handling, ``with_energy()`` integration helpers, etc., by injecting
21+
the computed value back under the synthetic key and delegating
22+
``_updated`` to ``super()``.
23+
24+
Registering both the legacy ``powGetPv*`` entity and this new helper with
25+
``auto_enable=True`` keeps the integration firmware-agnostic: HA enables
26+
whichever variant first sees a non-zero value.
27+
28+
References:
29+
- https://github.com/tolwi/hassio-ecoflow-cloud/issues/584
30+
- https://github.com/tolwi/hassio-ecoflow-cloud/issues/582
31+
"""
32+
from typing import Any
33+
34+
from custom_components.ecoflow_cloud.sensor import WattsSensorEntity
35+
36+
37+
class StreamPvWattsSensorEntity(WattsSensorEntity):
38+
"""Compute per-PV watts as ``amp x vol`` from EcoFlow Stream payloads.
39+
40+
Example:
41+
StreamPvWattsSensorEntity(client, self, "plugInInfoPv2Amp",
42+
const.STREAM_POWER_PV_2)
43+
# subscribes to plugInInfoPv2Amp + plugInInfoPv2Vol, exposes watts
44+
"""
45+
46+
# Synthetic key prefix - chosen so it cannot exist as a real EcoFlow
47+
# payload field and cannot collide with sibling sensor unique_ids.
48+
_SYNTHETIC_KEY_PREFIX = "pvWatts"
49+
50+
def __init__(
51+
self,
52+
client,
53+
device,
54+
amp_key: str,
55+
title,
56+
enabled: bool = True,
57+
auto_enable: bool = False,
58+
) -> None:
59+
if not amp_key.endswith("Amp"):
60+
raise ValueError(
61+
f"StreamPvWattsSensorEntity expects an amp_key ending in 'Amp', "
62+
f"got: {amp_key!r}"
63+
)
64+
self._amp_key = amp_key
65+
self._vol_key = amp_key[:-3] + "Vol" # plugInInfoPv2Amp -> plugInInfoPv2Vol
66+
synthetic_key = f"{self._SYNTHETIC_KEY_PREFIX}_{amp_key}"
67+
super().__init__(client, device, synthetic_key, title, enabled, auto_enable)
68+
69+
def _updated(self, data: dict[str, Any]) -> None: # type: ignore[override]
70+
amp = data.get(self._amp_key)
71+
vol = data.get(self._vol_key)
72+
if amp is None or vol is None:
73+
# Let the upstream pipeline handle offline / default-value reset
74+
super()._updated(data)
75+
return
76+
try:
77+
watts = float(amp) * float(vol)
78+
except (TypeError, ValueError):
79+
return
80+
# Inject the computed value under our synthetic mqtt_key and let the
81+
# upstream _updated() do the rest (auto-enable, attribute mapping,
82+
# _update_value, schedule_update_ha_state). We deliberately mutate a
83+
# shallow copy to avoid surprising other sensors that read the same
84+
# payload dict afterwards.
85+
super()._updated({**data, self.mqtt_key: watts})

0 commit comments

Comments
 (0)