Skip to content

Commit b8ee461

Browse files
authored
Add Ecoflow Smart Meter & Stream AC & Stream Ultra devices (tolwi#483)
* Add Smart Meter device * Rename L1/L2/L3 connections * Adaptation variable params * Add Stream AC device * Add serial number * Add new entities (manual discovered) for smart meter and stream ac * Add detection of device by name Necessary when there is no productName in json flow * Adjust for Stream Ultra device name * Add powGetSchuko2 Entity * Add documentation for new devices - Generate conf for Smart meter / Stream AC & Ultra - Fix of the gen.py to generate conf
1 parent 2072930 commit b8ee461

22 files changed

+465
-61
lines changed

custom_components/ecoflow_cloud/api/ecoflow_mqtt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def _on_message(self, client, userdata, message):
9595
try:
9696
for sn, device in self.__devices.items():
9797
if device.update_data(message.payload, message.topic):
98-
_LOGGER.debug(f"Message for {sn} and Topic {message.topic}")
98+
_LOGGER.debug(f"Message for {sn} and Topic {message.topic} : {message.payload}")
9999
except UnicodeDecodeError as error:
100100
_LOGGER.error(
101101
f"UnicodeDecodeError: {error}. Ignoring message and waiting for the next one."

custom_components/ecoflow_cloud/api/public_api.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,15 @@ async def fetch_all_available_devices(self) -> list[EcoflowDeviceInfo]:
4343
response = await self.call_api("/device/list")
4444
result = list()
4545
for device in response["data"]:
46+
_LOGGER.debug(str(device))
4647
sn = device["sn"]
4748
product_name = device.get("productName", "undefined")
49+
if product_name == "undefined" :
50+
from ..devices.registry import device_by_product
51+
device_list = list(device_by_product.keys())
52+
for devicetype in device_list:
53+
if "deviceName" in device and device["deviceName"].lower().startswith(devicetype.lower()):
54+
product_name = devicetype
4855
device_name = device.get("deviceName", f"{product_name}-{sn}")
4956
status = int(device["online"])
5057
result.append(
@@ -94,11 +101,14 @@ async def quota_all(self, device_sn: str | None):
94101
target_devices = [device_sn]
95102

96103
for sn in target_devices:
97-
raw = await self.call_api(
98-
"/device/quota/all", {"sn": self.devices[sn].device_info.sn}
99-
)
100-
if "data" in raw:
101-
self.devices[sn].data.update_data({"params": raw["data"]})
104+
try:
105+
raw = await self.call_api("/device/quota/all", {"sn": sn})
106+
if "data" in raw:
107+
self.devices[sn].data.update_data({"params": raw["data"]})
108+
except Exception as exception:
109+
_LOGGER.error(exception, exc_info=True)
110+
_LOGGER.error("Erreur recuperation %s", sn)
111+
102112

103113
async def call_api(self, endpoint: str, params: dict[str, str] = None) -> dict:
104114
async with aiohttp.ClientSession() as session:

custom_components/ecoflow_cloud/config_flow.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,17 +146,18 @@ def remove_device(self, sn: str):
146146
identifiers = {(ECOFLOW_DOMAIN, f"{sn}")}
147147

148148
device = device_reg.async_get_device(identifiers=identifiers)
149-
# _LOGGER.info(f".. getting device by %s: %s", str(identifiers), str(device))
149+
_LOGGER.debug(f".. getting device by %s: %s", str(identifiers), str(device))
150150

151151
# Remove all entities for this device
152-
ent_reg: EntityRegistry = er.async_get(self.hass)
153-
entities = er.async_entries_for_device(ent_reg, device.id)
152+
if getattr(device, "id", None) is not None :
153+
ent_reg: EntityRegistry = er.async_get(self.hass)
154+
entities = er.async_entries_for_device(ent_reg, device.id)
154155

155-
for entity in entities:
156-
ent_reg.async_remove(entity.entity_id)
156+
for entity in entities:
157+
ent_reg.async_remove(entity.entity_id)
157158

158-
# Remove the device from the device registry
159-
device_reg.async_remove_device(device.id)
159+
# Remove the device from the device registry
160+
device_reg.async_remove_device(device.id)
160161

161162
async def async_step_user(self, user_input: dict[str, Any] | None = None):
162163
if self.config_entry: # reconfigure flow
@@ -386,7 +387,8 @@ async def async_step_remove_device(self, user_input: dict[str, Any] | None = Non
386387
),
387388
)
388389
target_device = self.local_devices[user_input[CONF_SELECT_DEVICE_KEY]]
389-
self.new_data[CONF_DEVICE_LIST].pop(target_device.sn)
390+
if target_device.sn in self.new_data[CONF_DEVICE_LIST]:
391+
self.new_data[CONF_DEVICE_LIST].pop(target_device.sn)
390392

391393
self.remove_device(target_device.sn)
392394
return await self.update_or_create()

custom_components/ecoflow_cloud/devices/const.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,45 @@
275275
MAIN_MODE = "Main mode"
276276
REMOTE_MODE = "Remote startup/shutdown"
277277
POWER_SUB_MODE = "Sub-mode"
278+
279+
# Smart Meter
280+
SMART_METER_POWER_GLOBAL = "Power Grid Global"
281+
SMART_METER_POWER_L1 = "Power Grid L1"
282+
SMART_METER_POWER_L2 = "Power Grid L2"
283+
SMART_METER_POWER_L3 = "Power Grid L3"
284+
SMART_METER_IN_AMPS_L1 = "Power Grid (L1) In Amps"
285+
SMART_METER_IN_AMPS_L2 = "Power Grid (L2) In Amps"
286+
SMART_METER_IN_AMPS_L3 = "Power Grid (L3) In Amps"
287+
SMART_METER_VOLT_L1 = "Power Grid (L1) Volts"
288+
SMART_METER_VOLT_L2 = "Power Grid (L2) Volts"
289+
SMART_METER_VOLT_L3 = "Power Grid (L3) Volts"
290+
SMART_METER_RECORD_ACTIVE_TODAY = "Lifetime consumption"
291+
SMART_METER_RECORD_ACTIVE_TOTAL = "Lifetime net usage"
292+
SMART_METER_RECORD_REACTIVE_TOTAL = "Lifetime injection"
293+
SMART_METER_RECORD_ACTIVE_TODAY_L1 = "L1 Lifetime net usage"
294+
SMART_METER_RECORD_ACTIVE_TODAY_L2 = "L2 Lifetime net usage"
295+
SMART_METER_RECORD_ACTIVE_TODAY_L3 = "L3 Lifetime net usage"
296+
297+
# Stream AC
298+
STREAM_POWER_AC = "Power AC" # <0 import from home to battery / >0 export from battery to home
299+
STREAM_POWER_AC_SYS = "Power AC SYS" # <0 import from home to battery / >0 export from battery to home
300+
STREAM_POWER_PV = "Power PV" # >0 external pv power estimate injection to home/battery
301+
STREAM_GET_SYS_LOAD = "Power Sys Load" # powGetSysLoad
302+
STREAM_GET_SYS_LOAD_FROM_GRID = "Power Sys Load From Grid" # powGetSysLoadFromGrid
303+
STREAM_GET_SCHUKO1 = "Power SCHUKO1" # powGetSchuko1
304+
STREAM_GET_SCHUKO2 = "Power SCHUKO2" # powGetSchuko2
305+
STREAM_POWER_GRID = "Power Grid" # power from smart meter or shelly
306+
STREAM_POWER_BATTERY = "Power Battery" # <0 discharge battery / >0 charge batterie
307+
STREAM_POWER_BATTERY_SOC = "Power Battery SOC" # <0 discharge battery / >0 charge batterie
308+
STREAM_BATTERY_LEVEL = "Battery Level"
309+
STREAM_DESIGN_CAPACITY = "Design Capacity"
310+
STREAM_FULL_CAPACITY = "Full Capacity"
311+
STREAM_REMAIN_CAPACITY = "Remain Capacity"
312+
STREAM_STR_BATTERY_LEVEL = "Battery Level %s "
313+
STREAM_STR_DESIGN_CAPACITY = "Design Capacity %s "
314+
STREAM_STR_FULL_CAPACITY = "Full Capacity %s "
315+
STREAM_STR_REMAIN_CAPACITY = "Remain Capacity %s "
316+
STREAM_IN_POWER = "In Power"
317+
STREAM_STR_IN_POWER = "In Power %s"
318+
STREAM_OUT_POWER = "Out Power"
319+
STREAM_STR_OUT_POWER = "Out Power %s"

custom_components/ecoflow_cloud/devices/data_holder.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,9 @@ def update_data(self, raw: dict[str, Any]):
9292
return
9393
if raw["moduleSn"] != self.module_sn:
9494
return
95-
self.params.update(raw["params"])
96-
self.params_time = dt.utcnow()
95+
if "params" in raw:
96+
self.params.update(raw["params"])
97+
self.params_time = dt.utcnow()
9798

9899
except Exception as error:
99100
_LOGGER.error("Error updating data: %s", error)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from custom_components.ecoflow_cloud.api import EcoflowApiClient
2+
from custom_components.ecoflow_cloud.devices import const, BaseDevice
3+
from custom_components.ecoflow_cloud.entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, \
4+
BaseSelectEntity
5+
from custom_components.ecoflow_cloud.sensor import WattsSensorEntity, InAmpSensorEntity, MilliVoltSensorEntity, \
6+
EnergySensorEntity
7+
8+
class SmartMeter(BaseDevice):
9+
def sensors(self, client: EcoflowApiClient) -> list[BaseSensorEntity]:
10+
return [
11+
WattsSensorEntity(client, self, "powGetSysGrid", const.SMART_METER_POWER_GLOBAL),
12+
13+
WattsSensorEntity(client, self, "gridConnectionPowerL1", const.SMART_METER_POWER_L1, False),
14+
WattsSensorEntity(client, self, "gridConnectionPowerL2", const.SMART_METER_POWER_L2, False),
15+
WattsSensorEntity(client, self, "gridConnectionPowerL3", const.SMART_METER_POWER_L3, False),
16+
17+
InAmpSensorEntity(client, self, "gridConnectionAmpL1", const.SMART_METER_IN_AMPS_L1, False),
18+
InAmpSensorEntity(client, self, "gridConnectionAmpL2", const.SMART_METER_IN_AMPS_L2, False),
19+
InAmpSensorEntity(client, self, "gridConnectionAmpL3", const.SMART_METER_IN_AMPS_L3, False),
20+
21+
MilliVoltSensorEntity(client, self, "gridConnectionVolL1", const.SMART_METER_VOLT_L1, False),
22+
MilliVoltSensorEntity(client, self, "gridConnectionVolL2", const.SMART_METER_VOLT_L2, False),
23+
MilliVoltSensorEntity(client, self, "gridConnectionVolL3", const.SMART_METER_VOLT_L3, False),
24+
25+
EnergySensorEntity(client, self, "gridConnectionDataRecord.todayActive", const.SMART_METER_RECORD_ACTIVE_TODAY),
26+
EnergySensorEntity(client, self, "gridConnectionDataRecord.totalActiveEnergy", const.SMART_METER_RECORD_ACTIVE_TOTAL),
27+
EnergySensorEntity(client, self, "gridConnectionDataRecord.totalReactiveEnergy", const.SMART_METER_RECORD_REACTIVE_TOTAL),
28+
29+
EnergySensorEntity(client, self, "gridConnectionDataRecord.todayActiveL1", const.SMART_METER_RECORD_ACTIVE_TODAY_L1,False),
30+
EnergySensorEntity(client, self, "gridConnectionDataRecord.todayActiveL2", const.SMART_METER_RECORD_ACTIVE_TODAY_L2, False),
31+
EnergySensorEntity(client, self, "gridConnectionDataRecord.todayActiveL3", const.SMART_METER_RECORD_ACTIVE_TODAY_L3, False),
32+
33+
34+
]
35+
36+
def numbers(self, client: EcoflowApiClient) -> list[BaseNumberEntity]:
37+
return []
38+
39+
def switches(self, client: EcoflowApiClient) -> list[BaseSwitchEntity]:
40+
return []
41+
42+
def selects(self, client: EcoflowApiClient) -> list[BaseSelectEntity]:
43+
return []
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from custom_components.ecoflow_cloud.api import EcoflowApiClient
2+
from custom_components.ecoflow_cloud.devices import const, BaseDevice
3+
from custom_components.ecoflow_cloud.entities import BaseSensorEntity, BaseNumberEntity, BaseSwitchEntity, \
4+
BaseSelectEntity
5+
from custom_components.ecoflow_cloud.sensor import WattsSensorEntity,LevelSensorEntity,CapacitySensorEntity, \
6+
InWattsSensorEntity,OutWattsSensorEntity, RemainSensorEntity, MilliVoltSensorEntity, TempSensorEntity, \
7+
CyclesSensorEntity
8+
9+
class StreamAC(BaseDevice):
10+
def sensors(self, client: EcoflowApiClient) -> list[BaseSensorEntity]:
11+
return [
12+
13+
WattsSensorEntity(client, self, "sysGridConnectionPower", const.STREAM_POWER_AC_SYS),
14+
WattsSensorEntity(client, self, "powGetSysLoad", const.STREAM_GET_SYS_LOAD),
15+
WattsSensorEntity(client, self, "powGetSysLoadFromGrid", const.STREAM_GET_SYS_LOAD_FROM_GRID),
16+
WattsSensorEntity(client, self, "powGetSchuko1", const.STREAM_GET_SCHUKO1, False),
17+
WattsSensorEntity(client, self, "powGetSchuko2", const.STREAM_GET_SCHUKO2, False),
18+
WattsSensorEntity(client, self, "gridConnectionPower", const.STREAM_POWER_AC),
19+
WattsSensorEntity(client, self, "powGetSysGrid", const.STREAM_POWER_GRID),
20+
WattsSensorEntity(client, self, "powGetPvSum", const.STREAM_POWER_PV),
21+
WattsSensorEntity(client, self, "powGetBpCms", const.STREAM_POWER_BATTERY),
22+
LevelSensorEntity(client, self, "f32ShowSoc", const.STREAM_POWER_BATTERY_SOC),
23+
LevelSensorEntity(client, self, "soc", const.STREAM_POWER_BATTERY)
24+
.attr("designCap", const.ATTR_DESIGN_CAPACITY, 0)
25+
.attr("fullCap", const.ATTR_FULL_CAPACITY, 0)
26+
.attr("remainCap", const.ATTR_REMAIN_CAPACITY, 0),
27+
CapacitySensorEntity(client, self, "designCap", const.STREAM_DESIGN_CAPACITY,False),
28+
CapacitySensorEntity(client, self, "fullCap", const.STREAM_FULL_CAPACITY, False),
29+
CapacitySensorEntity(client, self, "remainCap", const.STREAM_REMAIN_CAPACITY,False),
30+
31+
MilliVoltSensorEntity(client, self, "vol", const.BATTERY_VOLT, False)
32+
.attr("minCellVol", const.ATTR_MIN_CELL_VOLT, 0)
33+
.attr("maxCellVol", const.ATTR_MAX_CELL_VOLT, 0),
34+
MilliVoltSensorEntity(client, self, "minCellVol", const.MIN_CELL_VOLT, False),
35+
MilliVoltSensorEntity(client, self, "maxCellVol", const.MAX_CELL_VOLT, False),
36+
LevelSensorEntity(client, self, "soh", const.SOH),
37+
38+
TempSensorEntity(client, self, "temp", const.BATTERY_TEMP)
39+
.attr("minCellTemp", const.ATTR_MIN_CELL_TEMP, 0)
40+
.attr("maxCellTemp", const.ATTR_MAX_CELL_TEMP, 0),
41+
TempSensorEntity(client, self, "minCellTemp", const.MIN_CELL_TEMP, False),
42+
TempSensorEntity(client, self, "maxCellTemp", const.MAX_CELL_TEMP, False),
43+
44+
CyclesSensorEntity(client, self, "cycles", const.CYCLES),
45+
46+
InWattsSensorEntity(client, self, "inputWatts", const.STREAM_IN_POWER),
47+
OutWattsSensorEntity(client, self, "outputWatts", const.STREAM_OUT_POWER),
48+
49+
RemainSensorEntity(client, self, "remainTime", const.REMAINING_TIME),
50+
]
51+
# moduleWifiRssi
52+
def numbers(self, client: EcoflowApiClient) -> list[BaseNumberEntity]:
53+
return []
54+
55+
def switches(self, client: EcoflowApiClient) -> list[BaseSwitchEntity]:
56+
return []
57+
58+
def selects(self, client: EcoflowApiClient) -> list[BaseSelectEntity]:
59+
return []
Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import logging
2+
3+
_LOGGER = logging.getLogger(__name__)
4+
15
plain_to_status: dict[str, str] = {
26
"pd": "pdStatus",
37
"mppt": "mpptStatus",
@@ -13,42 +17,39 @@
1317

1418

1519
def to_plain(raw_data: dict[str, any]) -> dict[str, any]:
20+
new_params = {}
21+
prefix = ""
1622
if "typeCode" in raw_data:
17-
prefix = status_to_plain.get(
23+
prefix1 = status_to_plain.get(
1824
raw_data["typeCode"], "unknown_" + raw_data["typeCode"]
1925
)
20-
new_params = {}
21-
if "params" in raw_data:
22-
for k, v in raw_data["params"].items():
23-
new_params[f"{prefix}.{k}"] = v
24-
if "param" in raw_data:
25-
for k, v in raw_data["param"].items():
26-
new_params[f"{prefix}.{k}"] = v
27-
28-
result = {"params": new_params}
29-
for k, v in raw_data.items():
30-
if k != "param" and k != "params":
31-
result[k] = v
32-
33-
return result
34-
else:
35-
if "cmdFunc" in raw_data and "cmdId" in raw_data:
36-
new_params = {}
37-
prefix = f"{raw_data['cmdFunc']}_{raw_data['cmdId']}"
38-
39-
if "param" in raw_data:
40-
for k, v in raw_data["param"].items():
41-
new_params[f"{prefix}.{k}"] = v
42-
43-
if "params" in raw_data:
44-
for k, v in raw_data["params"].items():
45-
new_params[f"{prefix}.{k}"] = v
46-
47-
result = {"params": new_params}
48-
for k, v in raw_data.items():
49-
if k != "param" and k != "params":
50-
result[k] = v
51-
52-
return result
53-
54-
return raw_data
26+
prefix += f"{prefix1}."
27+
elif "cmdFunc" in raw_data and "cmdId" in raw_data:
28+
prefix += f"{raw_data['cmdFunc']}_{raw_data['cmdId']}."
29+
else :
30+
prefix += ""
31+
32+
if "param" in raw_data:
33+
for k, v in raw_data["param"].items():
34+
new_params[f"{prefix}{k}"] = v
35+
36+
if "params" in raw_data:
37+
for k, v in raw_data["params"].items():
38+
new_params[f"{prefix}{k}"] = v
39+
40+
for k, v in raw_data.items():
41+
if k != "param" and k != "params":
42+
new_params[f"{prefix}{k}"] = v
43+
44+
new_params2 = {}
45+
for k, v in new_params.items():
46+
new_params2[k] = v
47+
if isinstance(v, dict):
48+
for k2, v2 in v.items():
49+
new_params2[f"{k}.{k2}"] = v2
50+
51+
result = {"params": new_params2}
52+
_LOGGER.debug(str(result))
53+
54+
return result
55+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from ...api import EcoflowApiClient
2+
from ...sensor import StatusSensorEntity
3+
from ..internal.smart_meter import SmartMeter as InternalSmartMeter
4+
from .data_bridge import to_plain
5+
6+
7+
class SmartMeter(InternalSmartMeter):
8+
9+
def _prepare_data(self, raw_data) -> dict[str, any]:
10+
res = super()._prepare_data(raw_data)
11+
res = to_plain(res)
12+
return res
13+
14+
def _status_sensor(self, client: EcoflowApiClient) -> StatusSensorEntity:
15+
return StatusSensorEntity(client, self)
16+
17+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from ...api import EcoflowApiClient
2+
from ...sensor import StatusSensorEntity
3+
from ..internal.stream_ac import StreamAC as InternalStreamAC
4+
from .data_bridge import to_plain
5+
6+
7+
class StreamAC(InternalStreamAC):
8+
9+
def _prepare_data(self, raw_data) -> dict[str, any]:
10+
res = super()._prepare_data(raw_data)
11+
res = to_plain(res)
12+
return res
13+
14+
def _status_sensor(self, client: EcoflowApiClient) -> StatusSensorEntity:
15+
return StatusSensorEntity(client, self)
16+
17+

0 commit comments

Comments
 (0)