Add River 3 telemetry and schema-based diagnostics

Decode River 3 separately and accumulate partial reports against a field-level checklist. Keep watch mode running after completion.

Label fields and nested records from the pinned pr705 schema. Preserve unknown values and document unverified meanings and routing.

Add synthetic regressions, the River 3 field reference, schema-generation tooling, and upstream attribution.
This commit is contained in:
tim
2026-10-08 14:08:20 +02:00
parent bac953bf48
commit c2faeb669a
14 changed files with 3055 additions and 263 deletions
+18 -9
View File
@@ -24,6 +24,9 @@ from bmspy.classes import BMSInfoField, BMSScalarField
from bmspy.ecoflow_river2 import (
River2Decoder, SERIAL_PREFIXES as _RIVER2_PREFIXES, decode_river2_payload,
)
from bmspy.ecoflow_river3 import (
River3Decoder, SERIAL_PREFIXES as _RIVER3_PREFIXES, decode_river3_payload,
)
from bmspy.ecoflow_snapshot import EcoFlowSnapshot
from bmspy.utilities import debugger
@@ -35,8 +38,6 @@ MANUFACTURER_KEY = 0xB5B5
# Packet version and encryption type are independent. R601/R603 use V2
# packets even when the BLE advertisement selects Type 7 encryption.
def _default_packet_version(serial_number: str) -> int:
return 2 if serial_number.startswith(_RIVER2_PREFIXES) else 3
@@ -391,7 +392,11 @@ class EcoFlowBTDevice:
_default_packet_version(serial_number) if packet_version is None else packet_version
)
self._is_river2 = serial_number.startswith(_RIVER2_PREFIXES)
self._measurement_decoder = River2Decoder() if self._is_river2 else None
self._is_river3 = serial_number.startswith(_RIVER3_PREFIXES)
self._measurement_decoder: River2Decoder | River3Decoder | None = (
River2Decoder() if self._is_river2 else
River3Decoder() if self._is_river3 else None
)
self._debug = debug
self._state: _State = _State.CREATED
@@ -543,7 +548,7 @@ class EcoFlowBTDevice:
key = hashlib.md5(self._sn.encode()).digest()
iv = hashlib.md5(self._sn[::-1].encode()).digest()
self._enc = _Type1Encryption(key, iv)
self._assembler = _RawHeaderHelper(self._enc)
self._assembler = _RawHeaderHelper(self._enc, xor_payload=self._is_river3)
# Type 1: send probe and authenticate immediately without waiting for
# the probe response (mirrors ha-ef-ble's _type_1_session).
self._set_state(_State.AUTHENTICATING)
@@ -606,7 +611,7 @@ class EcoFlowBTDevice:
data = self._enc.decrypt(encrypted_data)
session_key = self._derive_session_key(data[16:18], data[:16])
self._enc = _Type7Encryption(session_key, self._enc.iv)
self._assembler = _EncFrameHelper(self._enc)
self._assembler = _EncFrameHelper(self._enc, xor_payload=self._is_river3)
await self._do_auth_status()
self._auth_fallback_task = asyncio.create_task(self._auth_type7_fallback())
@@ -778,8 +783,9 @@ class EcoFlowBTDevice:
class _EncFrameHelper:
"""Encode/decode EncPacket frames (Type 7, AES-CBC)."""
def __init__(self, enc: _Type7Encryption) -> None:
def __init__(self, enc: _Type7Encryption, xor_payload: bool = False) -> None:
self._enc = enc
self._xor_payload = xor_payload
self._buf = bytearray()
def encode(self, pkt: EcoFlowPacket) -> bytes:
@@ -793,7 +799,7 @@ class _EncFrameHelper:
packets = []
for raw in raw_list:
decrypted = self._enc.decrypt(raw)
pkt = EcoFlowPacket.from_bytes(decrypted)
pkt = EcoFlowPacket.from_bytes(decrypted, xor_payload=self._xor_payload)
if pkt is not None:
packets.append(pkt)
return packets
@@ -802,8 +808,9 @@ class _EncFrameHelper:
class _RawHeaderHelper:
"""Encode/decode RawHeader frames (Type 1, AES-CBC)."""
def __init__(self, enc: _Type1Encryption) -> None:
def __init__(self, enc: _Type1Encryption, xor_payload: bool = False) -> None:
self._enc = enc
self._xor_payload = xor_payload
self._buf = bytearray()
def encode(self, pkt: EcoFlowPacket) -> bytes:
@@ -820,7 +827,9 @@ class _RawHeaderHelper:
version = header[1]
inner_overhead = 15 if version >= 3 else 13
inner_len = inner_overhead + payload_length
pkt = EcoFlowPacket.from_bytes(header + decrypted[:inner_len])
pkt = EcoFlowPacket.from_bytes(
header + decrypted[:inner_len], xor_payload=self._xor_payload,
)
if pkt is not None:
packets.append(pkt)
return packets
+294 -205
View File
@@ -1,26 +1,18 @@
"""
EcoFlow BLE protocol decoder — River 2 (struct-packed) and River 3 (protobuf).
"""Shared EcoFlow binary helpers and diagnostic formatting.
River 2 (serial R601/R603/R611/R613):
Little-endian packed C structs; cmd_set=0x20.
River 3 (serial R651/R653/R654/R655):
Protobuf-encoded (standard_protocol_mr521.proto, package mr521).
cmd_set=0xFE, cmd_id=0x15, src=0x02 → DisplayPropertyUpload.
cmd_set=0xFE, cmd_id=0x16, src=0x02 → RuntimePropertyUpload (cmdId inferred).
Sources:
https://github.com/rabits/ef-ble-reverse
https://github.com/rabits/ha-ef-ble
Model-specific layouts, annotations, and measurement decoding live in
``ecoflow_river2`` and ``ecoflow_river3``. Model definitions are loaded only
when formatting needs them, so the models can use these parsing helpers
without circular imports. Legacy ``decode_*`` entry points remain available.
"""
from __future__ import annotations
import math
import struct
from typing import NamedTuple
from typing import Any, NamedTuple
# ── Protobuf wire-format decoder ───────────────────────────────────────────────
from .ecoflow_river3_schema import MESSAGES as _MESSAGES, ENUMS as _ENUMS
class ProtoField(NamedTuple):
@@ -29,27 +21,156 @@ class ProtoField(NamedTuple):
signed: bool = False # int32 fields that may be negative
class FieldDef(NamedTuple):
name: str
fmt: str # struct format char(s)
unit: str = ""
scale: float = 1.0
skip: bool = False
class WireField(NamedTuple):
number: int
wire_type: int
encoded: bytes
# ── Protobuf wire-format parsing ──────────────────────────────────────────────
def _pb_read_varint(data: bytes, pos: int) -> tuple[int, int]:
result, shift = 0, 0
while pos < len(data):
b = data[pos]; pos += 1
result = 0
for shift in range(0, 70, 7):
if pos >= len(data):
raise ValueError("truncated varint")
b = data[pos]
pos += 1
if shift == 63 and b > 1:
raise ValueError("varint exceeds 64 bits")
result |= (b & 0x7F) << shift
if not (b & 0x80):
return result, pos
shift += 7
raise ValueError("truncated varint")
raise ValueError("varint exceeds 64 bits")
def decode_protobuf(
def parse_protobuf_fields(data: bytes) -> list[WireField]:
"""Read complete top-level fields, retaining repeated fields and raw bytes.
Reject the whole message on malformed input. Length-delimited values stay
opaque here: nested messages are not separate top-level measurements.
"""
fields = []
pos = 0
while pos < len(data):
start = pos
tag, pos = _pb_read_varint(data, pos)
number, wire_type = tag >> 3, tag & 7
if not 1 <= number < (1 << 29):
raise ValueError("invalid protobuf field number")
if wire_type == 0:
_, pos = _pb_read_varint(data, pos)
elif wire_type in (1, 5):
pos += 8 if wire_type == 1 else 4
elif wire_type == 2:
size, pos = _pb_read_varint(data, pos)
pos += size
else:
raise ValueError(f"unsupported protobuf wire type {wire_type}")
if pos > len(data):
raise ValueError("truncated protobuf field")
fields.append(WireField(number, wire_type, data[start:pos]))
return fields
def decode_schema_values(
data: bytes, message: str, *, _depth: int = 0,
) -> dict[str, Any]:
"""Read reported schema fields as JSON-compatible values, without defaults.
Repeated fields stay lists and messages stay dictionaries. Enums retain
their code and declared name. Unknown fields retain their wire encoding.
Reject malformed known fields atomically, including malformed children.
"""
if _depth >= 16:
raise ValueError("maximum nesting depth reached")
grouped: dict[int, list[WireField]] = {}
for field in parse_protobuf_fields(data):
grouped.setdefault(field.number, []).append(field)
def scalar(kind: str, encoded: bytes, pos: int) -> tuple[Any, int]:
value, pos = _protobuf_scalar(kind, encoded, pos)
if isinstance(value, float) and not math.isfinite(value):
raise ValueError(f"non-finite {kind}")
if kind in _ENUMS:
value = {"code": value, "name": _ENUMS[kind].get(value)}
return value, pos
result = {}
for number, fields in grouped.items():
definition = _MESSAGES[message].get(number)
if definition is None:
opaque = []
for field in fields:
entry = {"wire_type": field.wire_type, "raw": field.encoded.hex()}
if field.wire_type == 0:
_, pos = _pb_read_varint(field.encoded, 0)
entry["value"], _ = _pb_read_varint(field.encoded, pos)
opaque.append(entry)
result[f"field_{number}"] = opaque
continue
name, kind, repeated = definition
expected = 2 if kind in _MESSAGES else 0 if kind in _ENUMS else _SCALAR_WIRE_TYPES[kind]
values = []
chunks = []
try:
for field in fields:
packed = repeated and expected != 2 and field.wire_type == 2
if field.wire_type != expected and not packed:
raise ValueError(f"wire type {field.wire_type}, expected {expected}")
_, pos = _pb_read_varint(field.encoded, 0)
if field.wire_type == 2:
length, pos = _pb_read_varint(field.encoded, pos)
chunk = field.encoded[pos:pos + length]
if packed:
cursor = 0
while cursor < len(chunk):
value, cursor = scalar(kind, chunk, cursor)
values.append(value)
elif kind in _MESSAGES:
chunks.append(chunk)
elif kind == "string":
values.append(chunk.decode("utf-8"))
else:
values.append("0x" + chunk.hex())
else:
value, _ = scalar(kind, field.encoded, pos)
values.append(value)
if kind in _MESSAGES:
if repeated:
values = [decode_schema_values(chunk, kind, _depth=_depth + 1) for chunk in chunks]
else:
# Protobuf merges multiple occurrences of a singular message
# within one report. A later report replaces the whole field.
values = [decode_schema_values(b"".join(chunks), kind, _depth=_depth + 1)]
result[name] = values if repeated else values[-1]
except (ValueError, struct.error) as exc:
raise ValueError(f"{message}.{name}: {exc}") from exc
return result
# ── Diagnostic formatting ─────────────────────────────────────────────────────
def format_protobuf(
data: bytes,
field_names: dict[int, ProtoField] | None = None,
indent: int = 0,
) -> list[str]:
"""Decode protobuf wire format into human-readable lines.
"""Format protobuf wire values as human-readable lines.
With *field_names* dict the output uses field names and units instead of
generic "field N: value" labels. Unknown field numbers fall back to the
generic form. Works without any external protobuf library.
generic "field N: value" labels. Unknown field numbers fall back to the
generic form. Works without any external protobuf library.
"""
lines: list[str] = []
pad = " " * indent
@@ -64,7 +185,7 @@ def decode_protobuf(
wire_type = tag & 0x07
pf = field_names.get(field_num) if field_names else None
label = pf.name if pf else f"field {field_num:3d}"
unit = f" {pf.unit}" if pf and pf.unit else ""
unit = f" {pf.unit}" if pf and pf.unit else ""
try:
if wire_type == 0: # varint
val, pos = _pb_read_varint(data, pos)
@@ -79,7 +200,7 @@ def decode_protobuf(
length, pos = _pb_read_varint(data, pos)
chunk = data[pos:pos + length]; pos += length
try:
nested = decode_protobuf(chunk, indent=indent + 1)
nested = format_protobuf(chunk, indent=indent + 1)
if nested:
lines.append(f"{pad}{label}: {{")
lines.extend(nested)
@@ -104,173 +225,120 @@ def decode_protobuf(
return lines
# ── River 2 — struct field descriptor ─────────────────────────────────────────
class FieldDef(NamedTuple):
name: str
fmt: str # struct format char(s)
unit: str = ""
scale: float = 1.0
skip: bool = False
# ── River 3 — DisplayPropertyUpload field map ──────────────────────────────────
# Source: standard_protocol_mr521.proto (package mr521)
# Routing: src=0x02, cmd_set=0xFE, cmd_id=0x15
_DISPLAY_PROPERTY_UPLOAD: dict[int, ProtoField] = {
# ── status / error ──
1: ProtoField("errcode"),
2: ProtoField("sys_status"),
6: ProtoField("energy_backup_state"),
130: ProtoField("dev_online_flag"),
212: ProtoField("dev_sleep_state"),
213: ProtoField("pd_err_code"),
214: ProtoField("llc_err_code"),
215: ProtoField("mppt_err_code"),
232: ProtoField("llc_inv_err_code"),
450: ProtoField("inv_err_code"),
140: ProtoField("bms_err_code"),
# ── total power flows ──
3: ProtoField("pow_in_sum_w", "W"),
4: ProtoField("pow_out_sum_w", "W"),
# ── AC ──
54: ProtoField("pow_get_ac_in", "W"),
368: ProtoField("pow_get_ac_out", "W"),
367: ProtoField("flow_info_ac_out"),
298: ProtoField("ac_out_open"),
211: ProtoField("ac_out_freq", "Hz"),
209: ProtoField("plug_in_info_ac_in_chg_pow_max", "W"),
458: ProtoField("plug_in_info_ac_in_chg_hal_pow_max", "W"),
202: ProtoField("plug_in_info_ac_charger_flag"),
# ── DC / solar input ──
361: ProtoField("pow_get_pv", "W"),
360: ProtoField("flow_info_pv"),
364: ProtoField("plug_in_info_pv_charger_flag"),
356: ProtoField("plug_in_info_pv_dc_amp_max", "A"),
365: ProtoField("plug_in_info_pv_chg_amp_max", "A"),
366: ProtoField("plug_in_info_pv_chg_vol_max", "V"),
148: ProtoField("pv_chg_type"),
# ── DC 12 V output ──
37: ProtoField("pow_get_12v", "W"),
33: ProtoField("flow_info_12v"),
75: ProtoField("btn_dc_12v_out_switch"),
74: ProtoField("dc_out_open"),
# ── USB outputs ──
9: ProtoField("pow_get_qcusb1", "W"),
10: ProtoField("pow_get_qcusb2", "W"),
11: ProtoField("pow_get_typec1", "W"),
12: ProtoField("pow_get_typec2", "W"),
76: ProtoField("btn_usb_switch"),
# ── battery (BMS composite / CMS) ──
158: ProtoField("pow_get_bms", "W"), # +chg / -dsg
262: ProtoField("cms_batt_soc", "%"),
263: ProtoField("cms_batt_soh", "%"),
102: ProtoField("cms_batt_temp", "°C", signed=True),
268: ProtoField("cms_dsg_rem_time", "min"),
269: ProtoField("cms_chg_rem_time", "min"),
270: ProtoField("cms_max_chg_soc", "%"),
271: ProtoField("cms_min_dsg_soc", "%"),
275: ProtoField("cms_bms_run_state"),
282: ProtoField("cms_chg_dsg_state"),
287: ProtoField("cms_batt_full_cap", "mAh"),
288: ProtoField("cms_batt_design_cap", "mAh"),
289: ProtoField("cms_batt_remain_cap", "mAh"),
459: ProtoField("cms_batt_pow_out_max", "W"),
460: ProtoField("cms_batt_pow_in_max", "W"),
462: ProtoField("cms_batt_full_energy", "Wh"),
# ── BMS cell-level ──
242: ProtoField("bms_batt_soc", "%"),
243: ProtoField("bms_batt_soh", "%"),
248: ProtoField("bms_design_cap", "mAh"),
254: ProtoField("bms_dsg_rem_time", "min"),
255: ProtoField("bms_chg_rem_time", "min"),
258: ProtoField("bms_min_cell_temp", "°C", signed=True),
259: ProtoField("bms_max_cell_temp", "°C", signed=True),
260: ProtoField("bms_min_mos_temp", "°C", signed=True),
261: ProtoField("bms_max_mos_temp", "°C", signed=True),
281: ProtoField("bms_chg_dsg_state"),
# ── energy backup ──
7: ProtoField("energy_backup_en"),
8: ProtoField("energy_backup_start_soc", "%"),
# ── standby / display config ──
5: ProtoField("lcd_light"),
17: ProtoField("dev_standby_time", "s"),
18: ProtoField("screen_off_time", "s"),
19: ProtoField("ac_standby_time", "s"),
20: ProtoField("dc_standby_time", "s"),
290: ProtoField("ble_standby_time", "s"),
# ── misc ──
25: ProtoField("xboost_en"),
30: ProtoField("pcs_fan_level"),
195: ProtoField("en_beep"),
358: ProtoField("low_power_alarm", "%"),
355: ProtoField("ups_alram"),
# Protobuf types, not value guesses, determine how known fields are rendered.
_SCALAR_WIRE_TYPES = {
**dict.fromkeys(("bool", "int32", "int64", "uint32", "uint64", "sint32", "sint64"), 0),
**dict.fromkeys(("double", "fixed64", "sfixed64"), 1),
"string": 2, "bytes": 2,
**dict.fromkeys(("float", "fixed32", "sfixed32"), 5),
}
# ── River 3 — RuntimePropertyUpload field map ──────────────────────────────────
# Source: standard_protocol_mr521.proto (package mr521)
# Note: cmd_id for RuntimePropertyUpload is not confirmed in ha-ef-ble river3.py;
# packets will be decoded if the device sends them on any cmd_id with these
# field numbers. Add the correct (src, cmd_set, cmd_id) key when known.
_RUNTIME_PROPERTY_UPLOAD: dict[int, ProtoField] = {
# ── temperatures ──
26: ProtoField("temp_pcs_dc", "°C"),
27: ProtoField("temp_pcs_ac", "°C"),
28: ProtoField("temp_pv_h", "°C"),
29: ProtoField("temp_pv_l", "°C"),
379: ProtoField("temp_pv", "°C"),
# ── AC voltages / currents ──
67: ProtoField("plug_in_info_ac_out_vol", "V"),
68: ProtoField("plug_in_info_ac_in_vol", "V"),
223: ProtoField("plug_in_info_ac_in_amp", "A"),
224: ProtoField("plug_in_info_ac_out_amp", "A"),
# ── solar / DC input ──
380: ProtoField("plug_in_info_pv_vol", "V"),
381: ProtoField("plug_in_info_pv_amp", "A"),
41: ProtoField("plug_in_info_pv_h_vol", "V"),
221: ProtoField("plug_in_info_pv_h_amp", "A"),
44: ProtoField("plug_in_info_pv_l_vol", "V"),
222: ProtoField("plug_in_info_pv_l_amp", "A"),
# ── 12 V output ──
382: ProtoField("plug_in_info_12v_vol", "V"),
383: ProtoField("plug_in_info_12v_amp", "A"),
# ── BMS battery voltage / current ──
244: ProtoField("bms_batt_vol", "V"),
245: ProtoField("bms_batt_amp", "A"),
264: ProtoField("cms_batt_vol", "V"),
265: ProtoField("cms_batt_amp", "A"),
# ── LLC / bus internals ──
384: ProtoField("llc_bat_vol", "V"),
385: ProtoField("llc_bat_cur", "A"),
386: ProtoField("llc_bus_vol", "V"),
# ── firmware versions ──
176: ProtoField("pd_firm_ver"),
177: ProtoField("iot_firm_ver"),
178: ProtoField("mppt_firm_ver"),
179: ProtoField("llc_firm_ver"),
241: ProtoField("bms_firm_ver"),
_FIXED_FORMATS = {
"float": "f", "double": "d", "fixed32": "I", "fixed64": "Q",
"sfixed32": "i", "sfixed64": "q",
}
# ── Message registry ───────────────────────────────────────────────────────────
#
# Struct-based (River 2): value is ("label", [FieldDef, ...], "struct")
# Protobuf-based (River 3): value is ("label", {field_num: ProtoField}, "proto")
_REGISTRY: dict[tuple[int, int, int], tuple[str, list | dict, str]] = {
# ── River 3: src / cmd_set / cmd_id ────────────────────────────────────────
(0x02, 0xFE, 0x15): ("DisplayPropertyUpload", _DISPLAY_PROPERTY_UPLOAD, "proto"),
(0x02, 0xFE, 0x16): ("RuntimePropertyUpload", _RUNTIME_PROPERTY_UPLOAD, "proto"),
}
def _protobuf_scalar(kind: str, data: bytes, pos: int) -> tuple[int | float | bool, int]:
if kind in _FIXED_FORMATS:
fmt = "<" + _FIXED_FORMATS[kind]
value = struct.unpack_from(fmt, data, pos)[0]
return value, pos + struct.calcsize(fmt)
value, pos = _pb_read_varint(data, pos)
if kind == "bool":
return bool(value), pos
if kind == "uint32" and value > 0xFFFFFFFF:
raise ValueError("value outside uint32 range")
if kind.startswith("sint"):
value = (value >> 1) ^ -(value & 1)
elif kind.startswith("int") or kind in _ENUMS:
bits = 64 if kind == "int64" else 32
value &= (1 << bits) - 1
if value >= (1 << (bits - 1)):
value -= 1 << bits
return value, pos
# ── Struct decoder ─────────────────────────────────────────────────────────────
def _protobuf_scalar_text(kind: str, value: int | float | bool) -> str:
if kind in _ENUMS:
name = _ENUMS[kind].get(value)
return f"{name} ({value})" if name is not None else f"{value} (unknown {kind})"
if kind == "bool":
return "true" if value else "false"
if kind in ("float", "double"):
return f"{value:.3f}"
return str(value)
def _decode_struct(label: str, fields: list[FieldDef], data: bytes) -> list[str]:
def format_schema_protobuf(
data: bytes, message: str, indent: int = 0, *, _depth: int = 0,
) -> list[str]:
"""Format known scalar, enum, packed, string and nested message types.
Unknown fields remain numeric/raw. Never infer a field's type from an empty
value or label a malformed field as a normal measurement.
"""
from .ecoflow_river3 import PROTO_ANNOTATIONS
pad = " " * indent
if _depth >= 16:
return [f"{pad}<maximum nesting depth reached, raw: {data.hex()}>"]
try:
fields = parse_protobuf_fields(data)
except ValueError as exc:
return [f"{pad}<malformed {message}: {exc}, raw: {data.hex()}>"]
definitions = _MESSAGES[message]
annotations = PROTO_ANNOTATIONS.get(message, {})
lines = []
for field in fields:
_, pos = _pb_read_varint(field.encoded, 0)
definition = definitions.get(field.number)
if definition is None:
if field.wire_type == 0:
value, _ = _pb_read_varint(field.encoded, pos)
value = str(value)
else:
value = f"0x{field.encoded[pos:].hex()} (wire type {field.wire_type}, unknown field)"
lines.append(f"{pad}field {field.number:3d}: {value}")
continue
name, kind, repeated = definition
annotation = annotations.get(field.number)
unit = f" {annotation.unit}" if annotation and annotation.unit else ""
expected = 2 if kind in _MESSAGES else 0 if kind in _ENUMS else _SCALAR_WIRE_TYPES[kind]
packed = repeated and expected != 2 and field.wire_type == 2
if field.wire_type != expected and not packed:
lines.append(f"{pad}{name}: <wire type {field.wire_type}, expected {expected}, "
f"raw: {field.encoded.hex()}>")
continue
try:
if field.wire_type == 2:
length, pos = _pb_read_varint(field.encoded, pos)
chunk = field.encoded[pos:pos + length]
if packed:
values, cursor = [], 0
while cursor < len(chunk):
value, cursor = _protobuf_scalar(kind, chunk, cursor)
values.append(_protobuf_scalar_text(kind, value))
lines.append(f"{pad}{name}: [{', '.join(values)}]{unit}")
elif kind in _MESSAGES:
if not chunk:
lines.append(f"{pad}{name}: {{}}")
else:
lines.append(f"{pad}{name}: {{")
lines.extend(format_schema_protobuf(chunk, kind, indent + 1, _depth=_depth + 1))
lines.append(f"{pad}}}")
elif kind == "string":
lines.append(f"{pad}{name}: {chunk.decode('utf-8')!r}")
else:
lines.append(f"{pad}{name}: 0x{chunk.hex()}")
else:
value, _ = _protobuf_scalar(kind, field.encoded, pos)
lines.append(f"{pad}{name}: {_protobuf_scalar_text(kind, value)}{unit}")
except (ValueError, struct.error) as exc:
lines.append(f"{pad}{name}: <invalid {kind}: {exc}, raw: {field.encoded.hex()}>")
return lines
def _format_struct(label: str, fields: list[FieldDef], data: bytes) -> list[str]:
lines: list[str] = [f"[{label}]"]
active = list(fields)
while active:
@@ -305,21 +373,24 @@ def _decode_struct(label: str, fields: list[FieldDef], data: bytes) -> list[str]
return lines
# ── Public API ─────────────────────────────────────────────────────────────────
def _message_definition(src: int, cmd_set: int, cmd_id: int) -> tuple[str, list | dict, str] | None:
from .ecoflow_river2 import MESSAGE_DEFINITIONS as river2
from .ecoflow_river3 import MESSAGE_DEFINITIONS as river3
key = (src, cmd_set, cmd_id)
return river2.get(key) or river3.get(key)
def decode_payload(
def format_payload(
src: int,
cmd_set: int,
cmd_id: int,
payload: bytes,
) -> list[str] | None:
"""Decode a payload by (src, cmd_set, cmd_id).
"""Format a payload selected by (src, cmd_set, cmd_id).
Returns annotated lines, or None if no definition is registered.
Callers should fall back to the generic protobuf/hex decoder on None.
Return annotated lines, or None if no definition is registered.
Callers can fall back to the generic protobuf/hex formatter on None.
"""
if not payload:
return None
@@ -328,31 +399,49 @@ def decode_payload(
return None
label, fields, kind = entry
if kind == "struct":
return _decode_struct(label, fields, payload) # type: ignore[arg-type]
# proto
lines = [f"[{label}]"]
lines += [" " + ln for ln in decode_protobuf(payload, field_names=fields)] # type: ignore[arg-type]
return _format_struct(label, fields, payload) # type: ignore[arg-type]
suffix = ", routing unconfirmed" if label == "RuntimePropertyUpload" else ""
lines = [f"[{label}{suffix}]"]
lines += [" " + ln for ln in format_schema_protobuf(payload, label)]
return lines
def describe_protobuf_field(src: int, cmd_set: int, cmd_id: int, number: int) -> str | None:
"""Return a registered protobuf field name, or None for an unknown field."""
entry = _message_definition(src, cmd_set, cmd_id)
if entry is None or entry[2] != "proto":
return None
definition = entry[1].get(number)
return definition.name if definition is not None else None
def describe_message(src: int, cmd_set: int, cmd_id: int) -> str | None:
entry = _message_definition(src, cmd_set, cmd_id)
return entry[0] if entry else None
# Load model definitions only when needed to avoid circular imports.
def _message_definition(src: int, cmd_set: int, cmd_id: int):
from .ecoflow_river2 import MESSAGE_DEFINITIONS
# ── Compatibility entry points ────────────────────────────────────────────────
# Keep existing imports working. New callers use format_* for diagnostic text
# and import numeric decoders from the corresponding model module.
key = (src, cmd_set, cmd_id)
return MESSAGE_DEFINITIONS.get(key) or _REGISTRY.get(key)
decode_protobuf = format_protobuf
decode_schema_protobuf = format_schema_protobuf
decode_payload = format_payload
def decode_river2_payload(src: int, cmd_set: int, cmd_id: int, payload: bytes):
def decode_river2_payload(
src: int, cmd_set: int, cmd_id: int, payload: bytes,
) -> dict[str, int | float] | None:
"""Compatibility entry point for the River 2 numeric decoder."""
from .ecoflow_river2 import decode_river2_payload as decode
return decode(src, cmd_set, cmd_id, payload)
format_payload = decode_payload
format_protobuf = decode_protobuf
def decode_river3_payload(
src: int, cmd_set: int, cmd_id: int, payload: bytes,
) -> dict[str, int | float] | None:
"""Compatibility entry point for the River 3 numeric decoder."""
from .ecoflow_river3 import decode_river3_payload as decode
return decode(src, cmd_set, cmd_id, payload)
+339
View File
@@ -0,0 +1,339 @@
"""River 3 field annotations, numeric decoding, and measurement interpretation.
Names and encoded types come from the generated pr705 schema. Units and
measurement mappings are curated here. Runtime routing remains unconfirmed.
"""
import json
import math
import struct
from bmspy.classes import BMSField, BMSInfoField, BMSMultiField, BMSScalarField
from bmspy.ecoflow_proto import (
ProtoField, _pb_read_varint, decode_schema_values, parse_protobuf_fields,
)
from bmspy.ecoflow_river3_schema import ENUMS as _ENUMS, MESSAGES as _MESSAGES
from bmspy.ecoflow_snapshot import EcoFlowSnapshot
SERIAL_PREFIXES = ("R651", "R653", "R654", "R655")
# ── River 3 — DisplayPropertyUpload field map ──────────────────────────────────
# Curated unit annotations. Complete names/types come from the pinned pr705
# schema below, including fields not yet observed on a physical device.
# Routing: src=0x02, cmd_set=0xFE, cmd_id=0x15
_DISPLAY_PROPERTY_UPLOAD: dict[int, ProtoField] = {
# ── status / error ──
1: ProtoField("errcode"),
2: ProtoField("sys_status"),
6: ProtoField("energy_backup_state"),
130: ProtoField("dev_online_flag"),
212: ProtoField("dev_sleep_state"),
213: ProtoField("pd_err_code"),
214: ProtoField("llc_err_code"),
215: ProtoField("mppt_err_code"),
232: ProtoField("llc_inv_err_code"),
450: ProtoField("inv_err_code"),
140: ProtoField("bms_err_code"),
# ── total power flows ──
3: ProtoField("pow_in_sum_w", "W"),
4: ProtoField("pow_out_sum_w", "W"),
# ── AC ──
54: ProtoField("pow_get_ac_in", "W"),
368: ProtoField("pow_get_ac_out", "W"),
367: ProtoField("flow_info_ac_out"),
298: ProtoField("ac_out_open"),
211: ProtoField("ac_out_freq", "Hz"),
209: ProtoField("plug_in_info_ac_in_chg_pow_max", "W"),
458: ProtoField("plug_in_info_ac_in_chg_hal_pow_max", "W"),
202: ProtoField("plug_in_info_ac_charger_flag"),
# ── DC / solar input ──
361: ProtoField("pow_get_pv", "W"),
360: ProtoField("flow_info_pv"),
364: ProtoField("plug_in_info_pv_charger_flag"),
356: ProtoField("plug_in_info_pv_dc_amp_max", "A"),
365: ProtoField("plug_in_info_pv_chg_amp_max", "A"),
366: ProtoField("plug_in_info_pv_chg_vol_max", "V"),
148: ProtoField("pv_chg_type"),
# ── DC 12 V output ──
37: ProtoField("pow_get_12v", "W"),
33: ProtoField("flow_info_12v"),
75: ProtoField("btn_dc_12v_out_switch"),
74: ProtoField("dc_out_open"),
# ── USB outputs ──
9: ProtoField("pow_get_qcusb1", "W"),
10: ProtoField("pow_get_qcusb2", "W"),
11: ProtoField("pow_get_typec1", "W"),
12: ProtoField("pow_get_typec2", "W"),
76: ProtoField("btn_usb_switch"),
# ── battery (BMS composite / CMS) ──
158: ProtoField("pow_get_bms", "W"), # +chg / -dsg
262: ProtoField("cms_batt_soc", "%"),
263: ProtoField("cms_batt_soh", "%"),
102: ProtoField("cms_batt_temp", "°C", signed=True),
268: ProtoField("cms_dsg_rem_time", "min"),
269: ProtoField("cms_chg_rem_time", "min"),
270: ProtoField("cms_max_chg_soc", "%"),
271: ProtoField("cms_min_dsg_soc", "%"),
275: ProtoField("cms_bms_run_state"),
282: ProtoField("cms_chg_dsg_state"),
287: ProtoField("cms_batt_full_cap", "mAh"),
288: ProtoField("cms_batt_design_cap", "mAh"),
289: ProtoField("cms_batt_remain_cap", "mAh"),
459: ProtoField("cms_batt_pow_out_max", "W"),
460: ProtoField("cms_batt_pow_in_max", "W"),
462: ProtoField("cms_batt_full_energy", "Wh"),
# ── BMS cell-level ──
242: ProtoField("bms_batt_soc", "%"),
243: ProtoField("bms_batt_soh", "%"),
248: ProtoField("bms_design_cap", "mAh"),
254: ProtoField("bms_dsg_rem_time", "min"),
255: ProtoField("bms_chg_rem_time", "min"),
258: ProtoField("bms_min_cell_temp", "°C", signed=True),
259: ProtoField("bms_max_cell_temp", "°C", signed=True),
260: ProtoField("bms_min_mos_temp", "°C", signed=True),
261: ProtoField("bms_max_mos_temp", "°C", signed=True),
281: ProtoField("bms_chg_dsg_state"),
# ── energy backup ──
7: ProtoField("energy_backup_en"),
8: ProtoField("energy_backup_start_soc", "%"),
# ── standby / display config ──
5: ProtoField("lcd_light"),
17: ProtoField("dev_standby_time", "s"),
18: ProtoField("screen_off_time", "s"),
19: ProtoField("ac_standby_time", "s"),
20: ProtoField("dc_standby_time", "s"),
290: ProtoField("ble_standby_time", "s"),
# ── misc ──
25: ProtoField("xboost_en"),
30: ProtoField("pcs_fan_level"),
195: ProtoField("en_beep"),
358: ProtoField("low_power_alarm", "%"),
355: ProtoField("ups_alram"),
}
# ── River 3 — RuntimePropertyUpload field map ──────────────────────────────────
# Curated unit annotations; complete names/types come from pr705.
# The 0x16 routing is unconfirmed for River 3. Keep it diagnostic-only:
# do not require this message for collection or map it into measurements.
_RUNTIME_PROPERTY_UPLOAD: dict[int, ProtoField] = {
# ── temperatures ──
26: ProtoField("temp_pcs_dc", "°C"),
27: ProtoField("temp_pcs_ac", "°C"),
28: ProtoField("temp_pv_h", "°C"),
29: ProtoField("temp_pv_l", "°C"),
379: ProtoField("temp_pv", "°C"),
# ── AC voltages / currents ──
67: ProtoField("plug_in_info_ac_out_vol", "V"),
68: ProtoField("plug_in_info_ac_in_vol", "V"),
223: ProtoField("plug_in_info_ac_in_amp", "A"),
224: ProtoField("plug_in_info_ac_out_amp", "A"),
# ── solar / DC input ──
380: ProtoField("plug_in_info_pv_vol", "V"),
381: ProtoField("plug_in_info_pv_amp", "A"),
41: ProtoField("plug_in_info_pv_h_vol", "V"),
221: ProtoField("plug_in_info_pv_h_amp", "A"),
44: ProtoField("plug_in_info_pv_l_vol", "V"),
222: ProtoField("plug_in_info_pv_l_amp", "A"),
# ── 12 V output ──
382: ProtoField("plug_in_info_12v_vol", "V"),
383: ProtoField("plug_in_info_12v_amp", "A"),
# ── BMS battery voltage / current ──
244: ProtoField("bms_batt_vol", "V"),
245: ProtoField("bms_batt_amp", "A"),
264: ProtoField("cms_batt_vol", "V"),
265: ProtoField("cms_batt_amp", "A"),
# ── LLC / bus internals ──
384: ProtoField("llc_bat_vol", "V"),
385: ProtoField("llc_bat_cur", "A"),
386: ProtoField("llc_bus_vol", "V"),
# ── firmware versions ──
176: ProtoField("pd_firm_ver"),
177: ProtoField("iot_firm_ver"),
178: ProtoField("mppt_firm_ver"),
179: ProtoField("llc_firm_ver"),
241: ProtoField("bms_firm_ver"),
}
def _complete_proto_fields(message: str, annotations: dict[int, ProtoField]) -> dict[int, ProtoField]:
result = {}
for number, (name, kind, _) in _MESSAGES[message].items():
annotation = annotations.get(number)
# Never carry a legacy unit across a renamed or repurposed field.
unit = annotation.unit if annotation and annotation.name == name else ""
result[number] = ProtoField(name, unit, signed=kind in ("int32", "int64"))
return result
_DISPLAY_PROPERTY_UPLOAD = _complete_proto_fields("DisplayPropertyUpload", _DISPLAY_PROPERTY_UPLOAD)
_RUNTIME_PROPERTY_UPLOAD = _complete_proto_fields("RuntimePropertyUpload", _RUNTIME_PROPERTY_UPLOAD)
PROTO_ANNOTATIONS = {
"DisplayPropertyUpload": _DISPLAY_PROPERTY_UPLOAD,
"RuntimePropertyUpload": _RUNTIME_PROPERTY_UPLOAD,
}
MESSAGE_DEFINITIONS = {
(0x02, 0xFE, 0x15): ("DisplayPropertyUpload", _DISPLAY_PROPERTY_UPLOAD, "proto"),
(0x02, 0xFE, 0x16): ("RuntimePropertyUpload", _RUNTIME_PROPERTY_UPLOAD, "proto"),
}
# Verified DisplayPropertyUpload fields shared with proto/pr705.proto.
# Wire type 5 is float32; wire type 0 is uint32 or signed int32.
_RIVER3_TELEMETRY_WIRE_TYPES = {
3: 5, 4: 5, 158: 5, 262: 5,
102: 0, 259: 0, 268: 0, 269: 0, 282: 0,
}
def decode_river3_payload(
src: int,
cmd_set: int,
cmd_id: int,
payload: bytes,
) -> dict[str, int | float] | None:
"""Read known River 3 measurements, preserving protobuf field presence.
Reports can be partial: absent fields are not zero measurements. Unknown
fields are skipped, but malformed messages are rejected as a whole. This
decoder does not interpret unconfirmed runtime message routing.
"""
if (src, cmd_set, cmd_id) != (0x02, 0xFE, 0x15):
return None
values: dict[str, int | float] = {}
try:
for field in parse_protobuf_fields(payload):
if _RIVER3_TELEMETRY_WIRE_TYPES.get(field.number) != field.wire_type:
continue
definition = _DISPLAY_PROPERTY_UPLOAD[field.number]
_, pos = _pb_read_varint(field.encoded, 0)
if field.wire_type == 0:
value, _ = _pb_read_varint(field.encoded, pos)
if definition.signed:
value &= 0xFFFFFFFF
if value >= (1 << 31):
value -= 1 << 32
elif value > 0xFFFFFFFF:
continue
else:
value = struct.unpack_from("<f", field.encoded, pos)[0]
values[definition.name] = value
except (ValueError, struct.error):
return None
return values
def _record_display_fields(snapshot: EcoFlowSnapshot, payload: bytes) -> None:
"""Expose all reported display fields, not just the normalized summary."""
namespace = "ef_river3_display"
try:
values = decode_schema_values(payload, "DisplayPropertyUpload")
except ValueError as exc:
snapshot.record_protocol_error(namespace, str(exc))
return
definitions = {
name: (number, kind, repeated)
for number, (name, kind, repeated) in _MESSAGES["DisplayPropertyUpload"].items()
}
fields: dict[str, BMSField] = {}
for name, value in values.items():
help = f"River 3 DisplayPropertyUpload.{name} (reported protocol value)"
definition = definitions.get(name)
if definition is None:
help += "; unknown field encoding"
if definition is not None:
number, kind, repeated = definition
annotation = _DISPLAY_PROPERTY_UPLOAD.get(number)
units = annotation.unit or None if annotation else None
if not repeated and kind in _ENUMS:
code, label = value["code"], value["name"]
text = f"{label} ({code})" if label is not None else f"{code} (unknown {kind})"
fields[name] = BMSScalarField(help=help, raw_value=code, value=text, units=units)
continue
if not repeated and isinstance(value, (int, float, bool)):
fields[name] = BMSScalarField(help=help, raw_value=value, value=str(value), units=units)
continue
# JSON preserves nested/repeated groups and distinguishes {}, [], and "".
# Strings stay strings; do not guess nested messages from their contents.
text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, allow_nan=False)
fields[name] = BMSInfoField(help=help, info=text)
snapshot.record_protocol_fields(namespace, fields)
class River3Decoder:
"""Apply model measurements while retaining per-connection source priorities."""
def __init__(self) -> None:
self.reset()
def reset(self) -> None:
"""Reset source priorities before a new connection, not the snapshot."""
self._river3_temp_priority = 0
def update(
self, snapshot: EcoFlowSnapshot, src: int, cmd_set: int, cmd_id: int, payload: bytes,
) -> None:
if (src, cmd_set, cmd_id) == (0x02, 0xFE, 0x15):
_record_display_fields(snapshot, payload)
values = decode_river3_payload(src, cmd_set, cmd_id, payload)
if values is None:
return
def scalar(name: str, help: str, value: int | float | bool, units: str | None = None):
setattr(snapshot, name, BMSScalarField(
help=help, raw_value=value, value=str(value), units=units,
))
soc = values.get("cms_batt_soc")
if soc is not None and math.isfinite(soc) and 0 <= soc <= 100:
scalar("bms_capacity_charge_ratio", "Battery Charge", soc / 100)
for source, name, help in (
("pow_in_sum_w", "ef_input_power_watts", "Total Input Power"),
("pow_out_sum_w", "ef_output_power_watts", "Total Output Power"),
):
watts = values.get(source)
if watts is not None and math.isfinite(watts) and watts >= 0:
scalar(name, help, watts, "W")
# Upstream uses the maximum cell temperature. Prefer that measurement
# over the composite temperature, even if separate packets arrive later.
for source, priority in (("cms_batt_temp", 1), ("bms_max_cell_temp", 2)):
temp = values.get(source)
if temp is not None and priority >= self._river3_temp_priority:
snapshot.bms_temperature_celcius = BMSMultiField(
help="Battery Temperature", label="sensor",
raw_values={1: temp}, values={1: str(temp)}, units="°C",
)
self._river3_temp_priority = priority
# Signed battery-side watts, not total AC input, determine direction.
watts = values.get("pow_get_bms")
if watts is not None and math.isfinite(watts):
scalar("bms_charge_is_charging", "Battery Charging", watts > 0)
scalar("bms_charge_is_discharging", "Battery Discharging", watts < 0)
if "cms_chg_dsg_state" in values:
scalar("ef_charge_state", "Charge State (device code)", values["cms_chg_dsg_state"])
for source, name, help in (
("cms_chg_rem_time", "ef_charge_remaining_time_minutes", "Time to Charge"),
("cms_dsg_rem_time", "ef_discharge_remaining_time_minutes", "Time to Discharge"),
):
if source in values:
if values[source] == 0xFFFFFFFF:
setattr(snapshot, name, None)
else:
# Do not assume River 2's 5999 convention applies here.
scalar(name, help, values[source], "min")
snapshot.ef_remaining_time_minutes = None
for state, remaining in (
(snapshot.bms_charge_is_charging, snapshot.ef_charge_remaining_time_minutes),
(snapshot.bms_charge_is_discharging, snapshot.ef_discharge_remaining_time_minutes),
):
if state is not None and state.raw_value and remaining is not None:
scalar("ef_remaining_time_minutes", "Remaining Time", remaining.raw_value, "min")
break
+559
View File
@@ -0,0 +1,559 @@
# SPDX-License-Identifier: Apache-2.0
# Source: https://github.com/rabits/ha-ef-ble/blob/511e04702c83ddcc90cdcd575a1bbeffe2f061ad/proto/pr705.proto
# Generated by tools/generate_ecoflow_river3_schema.py. Do not edit by hand.
SCHEMA_COMMIT = '511e04702c83ddcc90cdcd575a1bbeffe2f061ad'
# Field number: (name, protobuf type, repeated). Units are curated separately.
MESSAGES = {
'DisplayPropertyUpload': {
1: ('errcode', 'uint32', False),
2: ('sys_status', 'uint32', False),
3: ('pow_in_sum_w', 'float', False),
4: ('pow_out_sum_w', 'float', False),
5: ('lcd_light', 'uint32', False),
6: ('energy_backup_state', 'uint32', False),
7: ('energy_backup_en', 'bool', False),
8: ('energy_backup_start_soc', 'uint32', False),
9: ('pow_get_qcusb1', 'float', False),
10: ('pow_get_qcusb2', 'float', False),
11: ('pow_get_typec1', 'float', False),
12: ('pow_get_typec2', 'float', False),
13: ('flow_info_qcusb1', 'uint32', False),
14: ('flow_info_qcusb2', 'uint32', False),
15: ('flow_info_typec1', 'uint32', False),
16: ('flow_info_typec2', 'uint32', False),
17: ('dev_standby_time', 'uint32', False),
18: ('screen_off_time', 'uint32', False),
19: ('ac_standby_time', 'uint32', False),
20: ('dc_standby_time', 'uint32', False),
22: ('ac_always_on_flag', 'bool', False),
23: ('ac_always_on_mini_soc', 'uint32', False),
25: ('xboost_en', 'bool', False),
30: ('pcs_fan_level', 'uint32', False),
31: ('flow_info_pv_h', 'uint32', False),
32: ('flow_info_pv_l', 'uint32', False),
33: ('flow_info_12v', 'uint32', False),
34: ('flow_info_24v', 'uint32', False),
35: ('pow_get_pv_h', 'float', False),
36: ('pow_get_pv_l', 'float', False),
37: ('pow_get_12v', 'float', False),
38: ('pow_get_24v', 'float', False),
39: ('plug_in_info_pv_h_flag', 'uint32', False),
40: ('plug_in_info_pv_h_type', 'uint32', False),
42: ('plug_in_info_pv_l_flag', 'uint32', False),
43: ('plug_in_info_pv_l_type', 'uint32', False),
45: ('flow_info_ac2dc', 'uint32', False),
46: ('flow_info_dc2ac', 'uint32', False),
47: ('flow_info_ac_in', 'uint32', False),
48: ('flow_info_ac_hv_out', 'uint32', False),
49: ('flow_info_ac_lv_out', 'uint32', False),
50: ('flow_info_5p8_in', 'uint32', False),
51: ('flow_info_5p8_out', 'uint32', False),
52: ('pow_get_llc', 'float', False),
53: ('pow_get_ac', 'float', False),
54: ('pow_get_ac_in', 'float', False),
55: ('pow_get_ac_hv_out', 'float', False),
56: ('pow_get_ac_lv_out', 'float', False),
57: ('pow_get_ac_lv_tt30_out', 'float', False),
58: ('pow_get_5p8', 'float', False),
61: ('plug_in_info_ac_in_flag', 'uint32', False),
62: ('plug_in_info_ac_in_feq', 'uint32', False),
63: ('plug_in_info_5p8_flag', 'uint32', False),
64: ('plug_in_info_5p8_type', 'uint32', False),
65: ('plug_in_info_5p8_detail', 'uint32', False),
70: ('pow_get_pv2', 'float', False),
72: ('btn_ac_out_lv_switch', 'bool', False),
73: ('btn_ac_out_hv_switch', 'bool', False),
74: ('dc_out_open', 'bool', False),
75: ('btn_dc_12v_out_switch', 'bool', False),
76: ('btn_usb_switch', 'bool', False),
77: ('pow_get_dcp2', 'float', False),
78: ('flow_info_dcp2_in', 'uint32', False),
79: ('flow_info_dcp2_out', 'uint32', False),
80: ('plug_in_info_pv2_dc_amp_max', 'uint32', False),
81: ('plug_in_info_pv2_chg_amp_max', 'uint32', False),
82: ('plug_in_info_pv2_chg_vol_max', 'uint32', False),
83: ('plug_in_info_dcp2_in_flag', 'bool', False),
84: ('plug_in_info_dcp2_dsg_chg_type', 'uint32', False),
85: ('plug_in_info_dcp2_charger_flag', 'bool', False),
86: ('plug_in_info_dcp2_type', 'uint32', False),
87: ('plug_in_info_dcp2_detail', 'uint32', False),
88: ('plug_in_info_dcp2_sn', 'string', False),
89: ('plug_in_info_dcp2_run_state', 'uint32', False),
90: ('plug_in_info_dcp2_firm_ver', 'uint32', False),
91: ('plug_in_info_dcp2_resv', 'ResvInfo', False),
102: ('cms_batt_temp', 'int32', False),
105: ('pow_get_dc_bidi', 'float', False),
106: ('plug_in_info_dc_bidi_flag', 'bool', False),
107: ('plug_in_info_acp_chg_pow_max', 'uint32', False),
108: ('plug_in_info_acp_chg_hal_pow_max', 'uint32', False),
109: ('plug_in_info_acp_dsg_pow_max', 'uint32', False),
110: ('plug_in_info_acp_flag', 'uint32', False),
111: ('plug_in_info_acp_dsg_chg', 'uint32', False),
112: ('plug_in_info_acp_charger_flag', 'bool', False),
113: ('plug_in_info_acp_type', 'uint32', False),
114: ('plug_in_info_acp_detail', 'uint32', False),
115: ('plug_in_info_acp_sn', 'string', False),
116: ('plug_in_info_acp_run_state', 'uint32', False),
117: ('plug_in_info_acp_err_code', 'uint32', False),
118: ('plug_in_info_acp_firm_ver', 'uint32', False),
119: ('plug_in_info_acp_resv', 'ResvInfo', False),
123: ('generator_conn_dev_errcode', 'uint32', False),
124: ('plug_in_info_ac_in_chg_mode', 'AC_IN_CHG_MODE', False),
125: ('pv_dc_chg_setting_list', 'PvDcChgSettingList', False),
126: ('current_time_task_v2_item', 'TimeTaskItemV2', False),
127: ('generator_low_power_en', 'bool', False),
128: ('generator_low_power_threshold', 'uint32', False),
129: ('generator_lpg_monitor_en', 'bool', False),
130: ('dev_online_flag', 'bool', False),
131: ('fuels_liquefied_gas_lpg_uint', 'uint32', False),
132: ('fuels_liquefied_gas_lng_uint', 'uint32', False),
133: ('utc_timezone', 'int32', False),
134: ('utc_timezone_id', 'string', False),
135: ('utc_set_mode', 'bool', False),
136: ('plug_in_info_pv_chg_max_list', 'PvChgMaxList', False),
137: ('plug_in_info_pv2_chg_max_list', 'PvChgMaxList', False),
138: ('sp_charger_car_batt_vol_setting', 'uint32', False),
139: ('sp_charger_car_batt_vol', 'float', False),
140: ('bms_err_code', 'uint32', False),
141: ('err_code_record_list', 'ErrcodeRecordList', False),
142: ('wireless_coordinate_dev_list', 'WirelessCoordinateList', False),
143: ('wireless_oil_self_start', 'bool', False),
144: ('wireless_oil_on_soc', 'uint32', False),
145: ('wireless_oil_off_soc', 'uint32', False),
146: ('bypass_out_disable', 'bool', False),
147: ('output_power_off_memory', 'bool', False),
148: ('pv_chg_type', 'uint32', False),
152: ('flow_info_bms_dsg', 'uint32', False),
153: ('flow_info_bms_chg', 'uint32', False),
154: ('flow_info_4p8_1_in', 'uint32', False),
155: ('flow_info_4p8_1_out', 'uint32', False),
156: ('flow_info_4p8_2_in', 'uint32', False),
157: ('flow_info_4p8_2_out', 'uint32', False),
158: ('pow_get_bms', 'float', False),
159: ('pow_get_4p8_1', 'float', False),
160: ('pow_get_4p8_2', 'float', False),
161: ('plug_in_info_4p8_1_in_flag', 'uint32', False),
162: ('plug_in_info_4p8_1_type', 'uint32', False),
163: ('plug_in_info_4p8_1_detail', 'uint32', False),
164: ('plug_in_info_4p8_2_in_flag', 'uint32', False),
165: ('plug_in_info_4p8_2_type', 'uint32', False),
166: ('plug_in_info_4p8_2_detail', 'uint32', False),
170: ('plug_in_info_pv_l_charger_flag', 'bool', False),
171: ('plug_in_info_pv_h_charger_flag', 'bool', False),
180: ('plug_in_info_pv_l_dc_amp_max', 'uint32', False),
181: ('plug_in_info_pv_h_dc_amp_max', 'uint32', False),
182: ('fast_charge_switch', 'uint32', False),
183: ('plug_in_info_4p8_1_dsg_chg_type', 'uint32', False),
184: ('plug_in_info_4p8_1_resv', 'ResvInfo', False),
185: ('plug_in_info_4p8_1_sn', 'string', False),
186: ('plug_in_info_4p8_1_firm_ver', 'uint32', False),
187: ('plug_in_info_4p8_2_dsg_chg_type', 'uint32', False),
188: ('plug_in_info_4p8_2_resv', 'ResvInfo', False),
189: ('plug_in_info_4p8_2_sn', 'string', False),
190: ('plug_in_info_4p8_2_firm_ver', 'uint32', False),
191: ('plug_in_info_5p8_dsg_chg', 'uint32', False),
192: ('plug_in_info_5p8_resv', 'ResvInfo', False),
193: ('plug_in_info_5p8_sn', 'string', False),
194: ('plug_in_info_5p8_firm_ver', 'uint32', False),
195: ('en_beep', 'bool', False),
200: ('llc_GFCI_flag', 'bool', False),
202: ('plug_in_info_ac_charger_flag', 'bool', False),
203: ('plug_in_info_5p8_charger_flag', 'bool', False),
204: ('plug_in_info_5p8_run_state', 'uint32', False),
205: ('plug_in_info_4p8_1_charger_flag', 'bool', False),
206: ('plug_in_info_4p8_1_run_state', 'uint32', False),
207: ('plug_in_info_4p8_2_charger_flag', 'bool', False),
208: ('plug_in_info_4p8_2_run_state', 'uint32', False),
209: ('plug_in_info_ac_in_chg_pow_max', 'uint32', False),
210: ('plug_in_info_5p8_chg_pow_max', 'uint32', False),
211: ('ac_out_freq', 'uint32', False),
212: ('dev_sleep_state', 'uint32', False),
213: ('pd_err_code', 'uint32', False),
214: ('llc_err_code', 'uint32', False),
215: ('mppt_err_code', 'uint32', False),
216: ('plug_in_info_5p8_err_code', 'uint32', False),
217: ('plug_in_info_4p8_1_err_code', 'uint32', False),
218: ('plug_in_info_4p8_2_err_code', 'uint32', False),
219: ('time_task_current', 'SetTimeTaskWrite', False),
226: ('pcs_fan_err_flag', 'uint32', False),
227: ('llc_hv_lv_flag', 'uint32', False),
232: ('llc_inv_err_code', 'uint32', False),
233: ('plug_in_info_pv_h_chg_vol_max', 'uint32', False),
234: ('plug_in_info_pv_l_chg_vol_max', 'uint32', False),
235: ('plug_in_info_pv_l_chg_amp_max', 'uint32', False),
236: ('plug_in_info_pv_h_chg_amp_max', 'uint32', False),
237: ('plug_in_info_5p8_dsg_pow_max', 'uint32', False),
238: ('plug_in_info_ac_out_dsg_pow_max', 'uint32', False),
242: ('bms_batt_soc', 'float', False),
243: ('bms_batt_soh', 'float', False),
248: ('bms_design_cap', 'uint32', False),
254: ('bms_dsg_rem_time', 'uint32', False),
255: ('bms_chg_rem_time', 'uint32', False),
258: ('bms_min_cell_temp', 'int32', False),
259: ('bms_max_cell_temp', 'int32', False),
260: ('bms_min_mos_temp', 'int32', False),
261: ('bms_max_mos_temp', 'int32', False),
262: ('cms_batt_soc', 'float', False),
263: ('cms_batt_soh', 'float', False),
268: ('cms_dsg_rem_time', 'uint32', False),
269: ('cms_chg_rem_time', 'uint32', False),
270: ('cms_max_chg_soc', 'uint32', False),
271: ('cms_min_dsg_soc', 'uint32', False),
272: ('cms_oil_on_soc', 'uint32', False),
273: ('cms_oil_off_soc', 'uint32', False),
274: ('cms_oil_self_start', 'bool', False),
275: ('cms_bms_run_state', 'uint32', False),
281: ('bms_chg_dsg_state', 'uint32', False),
282: ('cms_chg_dsg_state', 'uint32', False),
283: ('ac_hv_always_on', 'bool', False),
284: ('ac_lv_always_on', 'bool', False),
285: ('time_task_conflict_flag', 'uint32', False),
286: ('time_task_change_cnt', 'uint32', False),
287: ('cms_batt_full_cap', 'uint32', False),
288: ('cms_batt_design_cap', 'uint32', False),
289: ('cms_batt_remain_cap', 'uint32', False),
290: ('ble_standby_time', 'uint32', False),
297: ('pow_get_dc', 'float', False),
298: ('ac_out_open', 'bool', False),
299: ('generator_fuels_type', 'uint32', False),
300: ('generator_remain_time', 'uint32', False),
301: ('generator_run_time', 'uint32', False),
302: ('generator_total_output', 'uint32', False),
303: ('generator_abnormal_state', 'uint32', False),
304: ('fuels_oil_val', 'uint32', False),
305: ('fuels_liquefied_gas_type', 'uint32', False),
306: ('fuels_liquefied_gas_uint', 'uint32', False),
307: ('fuels_liquefied_gas_val', 'float', False),
308: ('fuels_liquefied_gas_consume_per_hour', 'float', False),
309: ('fuels_liquefied_gas_remain_val', 'uint32', False),
310: ('generator_perf_mode', 'uint32', False),
311: ('generator_engine_open', 'uint32', False),
312: ('generator_out_pow_max', 'uint32', False),
313: ('generator_ac_out_pow_max', 'uint32', False),
314: ('generator_dc_out_pow_max', 'uint32', False),
315: ('generator_sub_battery_temp', 'int32', False),
316: ('generator_sub_battery_soc', 'uint32', False),
317: ('generator_sub_battery_state', 'uint32', False),
319: ('generator_maintence_state', 'bool', False),
330: ('generator_pcs_err_code', 'uint32', False),
355: ('ups_alram', 'bool', False),
356: ('plug_in_info_pv_dc_amp_max', 'uint32', False),
357: ('led_mode', 'uint32', False),
358: ('low_power_alarm', 'uint32', False),
359: ('silence_chg_watt', 'uint32', False),
360: ('flow_info_pv', 'uint32', False),
361: ('pow_get_pv', 'float', False),
362: ('plug_in_info_pv_flag', 'bool', False),
363: ('plug_in_info_pv_type', 'uint32', False),
364: ('plug_in_info_pv_charger_flag', 'bool', False),
365: ('plug_in_info_pv_chg_amp_max', 'uint32', False),
366: ('plug_in_info_pv_chg_vol_max', 'uint32', False),
367: ('flow_info_ac_out', 'uint32', False),
368: ('pow_get_ac_out', 'float', False),
414: ('flow_info_pv2', 'uint32', False),
421: ('plug_in_info_pv2_flag', 'bool', False),
422: ('plug_in_info_pv2_type', 'uint32', False),
423: ('flow_info_dcp_in', 'uint32', False),
424: ('flow_info_dcp_out', 'uint32', False),
425: ('pow_get_dcp', 'float', False),
426: ('plug_in_info_dcp_in_flag', 'bool', False),
427: ('plug_in_info_dcp_type', 'uint32', False),
428: ('plug_in_info_dcp_detail', 'uint32', False),
430: ('plug_in_info_pv2_charger_flag', 'bool', False),
431: ('plug_in_info_dcp_dsg_chg_type', 'uint32', False),
432: ('plug_in_info_dcp_resv', 'ResvInfo', False),
433: ('plug_in_info_dcp_sn', 'string', False),
434: ('plug_in_info_dcp_firm_ver', 'uint32', False),
435: ('plug_in_info_dcp_charger_flag', 'bool', False),
436: ('plug_in_info_dcp_run_state', 'uint32', False),
437: ('dcdc_err_code', 'uint32', False),
438: ('plug_in_info_dcp_err_code', 'uint32', False),
439: ('plug_in_info_dcp2_err_code', 'uint32', False),
450: ('inv_err_code', 'uint32', False),
451: ('generator_pv_hybrid_mode_open', 'bool', False),
452: ('generator_pv_hybrid_mode_soc_max', 'uint32', False),
453: ('generator_care_mode_open', 'bool', False),
454: ('generator_care_mode_start_time', 'uint32', False),
455: ('ac_energy_saving_open', 'bool', False),
456: ('multi_bp_chg_dsg_mode', 'uint32', False),
457: ('plug_in_info_5p8_chg_hal_pow_max', 'uint32', False),
458: ('plug_in_info_ac_in_chg_hal_pow_max', 'uint32', False),
459: ('cms_batt_pow_out_max', 'uint32', False),
460: ('cms_batt_pow_in_max', 'uint32', False),
462: ('cms_batt_full_energy', 'uint32', False),
463: ('display_statistics_sum', 'DisplayStatisticsRecordList', False),
467: ('storm_pattern_enable', 'bool', False),
468: ('storm_pattern_open_flag', 'bool', False),
469: ('storm_pattern_end_time', 'uint32', False),
581: ('sp_charger_chg_mode', 'SP_CHARGER_CHG_MODE', False),
582: ('sp_charger_run_state', 'uint32', False),
583: ('sp_charger_is_connect_car', 'bool', False),
591: ('installment_payment_serve_enable', 'bool', False),
592: ('serve_middlemen', 'SERVE_MIDDLEMEN', False),
593: ('installment_payment_overdue_limit', 'INSTALLMENT_PAYMENT_OVERDUE_LIMIT', False),
594: ('installment_payment_state', 'INSTALLMENT_PAYMENT_STATE', False),
595: ('installment_payment_start_utc_time', 'uint32', False),
596: ('installment_payment_overdue_limit_utc_time', 'uint32', False),
597: ('sp_charger_chg_open', 'bool', False),
598: ('sp_charger_chg_pow_limit', 'float', False),
599: ('module_bluetooth_snr', 'float', False),
600: ('module_bluetooth_rssi', 'float', False),
601: ('module_wifi_snr', 'float', False),
602: ('module_wifi_rssi', 'float', False),
603: ('sp_charger_chg_pow_max', 'float', False),
},
'DisplayStatisticsRecordList': {
1: ('list_info', 'StatisticsRecordItem', True),
},
'ErrcodeRecordItem': {
1: ('errcode', 'uint32', False),
2: ('errcode_timestamp', 'uint32', False),
},
'ErrcodeRecordList': {
1: ('list_info', 'ErrcodeRecordItem', True),
},
'PvChgMaxItem': {
1: ('pv_chg_vol_type', 'uint32', False),
2: ('pv_chg_amp_max', 'uint32', False),
3: ('pv_chg_amp_mini', 'uint32', False),
},
'PvChgMaxList': {
1: ('pv_chg_max_item', 'PvChgMaxItem', True),
},
'PvDcChgSetting': {
1: ('pv_plug_index', 'PV_PLUG_INDEX', False),
2: ('pv_chg_vol_spec', 'PV_CHG_VOL_SPEC', False),
3: ('pv_chg_amp_limit', 'uint32', False),
},
'PvDcChgSettingList': {
1: ('list_info', 'PvDcChgSetting', True),
},
'ResvInfo': {
1: ('resv_info', 'uint32', True),
},
'RuntimePropertyUpload': {
21: ('ac_phase_type', 'uint32', False),
24: ('pcs_work_mode', 'uint32', False),
26: ('temp_pcs_dc', 'float', False),
27: ('temp_pcs_ac', 'float', False),
28: ('temp_pv_h', 'float', False),
29: ('temp_pv_l', 'float', False),
41: ('plug_in_info_pv_h_vol', 'float', False),
44: ('plug_in_info_pv_l_vol', 'float', False),
59: ('plug_in_info_ac_out_type', 'uint32', False),
60: ('plug_in_info_ac_out_freq', 'uint32', False),
66: ('plug_in_info_5p8_freq', 'uint32', False),
67: ('plug_in_info_ac_out_vol', 'float', False),
68: ('plug_in_info_ac_in_vol', 'float', False),
69: ('plug_in_info_5p8_vol', 'float', False),
71: ('plug_in_info_pv2_amp', 'float', False),
92: ('plug_in_info_dcp2_vol', 'float', False),
93: ('plug_in_info_dcp2_amp', 'float', False),
103: ('plug_in_info_dc_bidi_vol', 'float', False),
104: ('plug_in_info_dc_bidi_amp', 'float', False),
120: ('plug_in_info_acp_vol', 'float', False),
121: ('plug_in_info_acp_amp', 'float', False),
122: ('plug_in_info_acp_freq', 'uint32', False),
167: ('plug_in_info_4p8_1_vol', 'float', False),
168: ('plug_in_info_4p8_2_vol', 'float', False),
169: ('plug_in_info_bms_vol', 'float', False),
172: ('pd_mppt_comm_err', 'bool', False),
173: ('pd_llc_comm_err', 'bool', False),
174: ('pd_bms_comm_err', 'bool', False),
175: ('pd_iot_comm_err', 'bool', False),
176: ('pd_firm_ver', 'uint32', False),
177: ('iot_firm_ver', 'uint32', False),
178: ('mppt_firm_ver', 'uint32', False),
179: ('llc_firm_ver', 'uint32', False),
221: ('plug_in_info_pv_h_amp', 'float', False),
222: ('plug_in_info_pv_l_amp', 'float', False),
223: ('plug_in_info_ac_in_amp', 'float', False),
224: ('plug_in_info_ac_out_amp', 'float', False),
225: ('plug_in_info_5p8_amp', 'float', False),
231: ('llc_inv_firm_ver', 'uint32', False),
239: ('plug_in_info_4p8_1_amp', 'float', False),
240: ('plug_in_info_4p8_2_amp', 'float', False),
241: ('bms_firm_ver', 'uint32', False),
244: ('bms_batt_vol', 'float', False),
245: ('bms_batt_amp', 'float', False),
246: ('bms_bal_state', 'uint32', False),
247: ('bms_full_cap', 'uint32', False),
249: ('bms_remain_cap', 'uint32', False),
250: ('bms_alm_state', 'uint32', False),
251: ('bms_pro_state', 'uint32', False),
252: ('bms_flt_state', 'uint32', False),
253: ('bms_err_code', 'uint32', False),
256: ('bms_min_cell_vol', 'uint32', False),
257: ('bms_max_cell_vol', 'uint32', False),
264: ('cms_batt_vol', 'float', False),
265: ('cms_batt_amp', 'float', False),
266: ('cms_chg_req_vol', 'float', False),
267: ('cms_chg_req_amp', 'float', False),
276: ('bms_overload_icon', 'uint32', False),
277: ('bms_warn_icon', 'uint32', False),
278: ('bms_high_temp_icon', 'uint32', False),
279: ('bms_low_temp_icon', 'uint32', False),
280: ('bms_limit_icon', 'uint32', False),
291: ('bms_alm_state_2', 'uint32', False),
292: ('bms_pro_state_2', 'uint32', False),
293: ('display_property_full_upload_period', 'int32', False),
294: ('display_property_incremental_upload_period', 'int32', False),
295: ('runtime_property_full_upload_period', 'int32', False),
296: ('runtime_property_incremental_upload_period', 'int32', False),
320: ('generator_run_state', 'uint32', False),
321: ('generator_oil_val_real', 'uint32', False),
322: ('generator_engine_spd', 'int32', False),
323: ('generator_engine_oil_val', 'uint32', False),
324: ('generator_engine_activation_cnt', 'uint32', False),
325: ('generator_engine_head_temp', 'int32', False),
326: ('generator_sub_battery_vol', 'uint32', False),
327: ('generator_sub_battery_amp', 'int32', False),
328: ('generator_commc_cnt_ems_to_psdr', 'uint32', False),
329: ('generator_commc_cnt_psdr_to_ems', 'uint32', False),
331: ('generator_pcs_fan_level', 'uint32', False),
332: ('generator_pcs_bus_vol', 'float', False),
333: ('generator_pcs_bus_amp', 'float', False),
334: ('generator_pcs_temp1', 'int32', False),
335: ('generator_pcs_temp2', 'int32', False),
336: ('generator_pcs_fules_type', 'uint32', False),
379: ('temp_pv', 'float', False),
380: ('plug_in_info_pv_vol', 'float', False),
381: ('plug_in_info_pv_amp', 'float', False),
382: ('plug_in_info_12v_vol', 'float', False),
383: ('plug_in_info_12v_amp', 'float', False),
440: ('temp_pv2', 'float', False),
442: ('plug_in_info_pv2_vol', 'float', False),
443: ('plug_in_info_dcp_vol', 'float', False),
444: ('pd_dcdc_comm_err', 'bool', False),
445: ('pd_inv_comm_err', 'bool', False),
446: ('dcdc_firm_ver', 'uint32', False),
447: ('inv_firm_ver', 'uint32', False),
448: ('plug_in_info_dcp_amp', 'float', False),
464: ('runtime_statistics_sum', 'RuntimeStatisticsRecordList', False),
},
'RuntimeStatisticsRecordList': {
1: ('list_info', 'StatisticsRecordItem', True),
},
'SetTimeTaskWrite': {
1: ('task_index', 'uint32', False),
2: ('is_valid', 'bool', False),
3: ('is_cfg', 'bool', False),
4: ('is_enable', 'bool', False),
5: ('conflict_flag', 'uint32', False),
6: ('type', 'uint32', False),
7: ('time_mode', 'uint32', False),
8: ('time_param', 'uint32', False),
9: ('time_table', 'uint32', True),
},
'StatisticsRecordItem': {
1: ('statistics_object', 'STATISTICS_OBJECT', False),
2: ('statistics_content', 'uint32', False),
},
'TimeTaskItemV2': {
1: ('task_index', 'uint32', False),
2: ('is_cfg', 'bool', False),
3: ('is_enable', 'bool', False),
4: ('conflict_flag', 'uint32', False),
5: ('time_mode', 'TIME_TASK_MODE', False),
6: ('time_param', 'uint32', False),
7: ('time_table', 'uint32', False),
8: ('task_type', 'TIME_TASK_TYPE', False),
9: ('task_param', 'uint32', False),
},
'WirelessCoordinateList': {
1: ('connect_flag', 'bool', False),
2: ('dev_type', 'uint32', False),
3: ('dev_detail', 'uint32', False),
4: ('dev_sn', 'string', False),
5: ('dev_firm_ver', 'uint32', False),
6: ('dev_err_code', 'uint32', False),
},
}
ENUMS = {
'AC_IN_CHG_MODE': {
0: 'AC_IN_CHG_MODE_SELF_DEF_POW',
1: 'AC_IN_CHG_MODE_BAT_OPTIMAL_POW',
2: 'AC_IN_CHG_MODE_SILENCE',
},
'INSTALLMENT_PAYMENT_OVERDUE_LIMIT': {
0: 'INSTALLMENT_PAYMENT_OVERDUE_LIMIT_NONE',
1: 'INSTALLMENT_PAYMENT_OVERDUE_LIMIT_CLOSE_OUTPUT',
},
'INSTALLMENT_PAYMENT_STATE': {
0: 'INSTALLMENT_PAYMENT_STATE_WAIT_FOT_ACTIVATE',
1: 'INSTALLMENT_PAYMENT_STATE_ACTIVATED',
2: 'INSTALLMENT_PAYMENT_STATE_OVERDUE',
3: 'INSTALLMENT_PAYMENT_STATE_SETTLE',
4: 'INSTALLMENT_PAYMENT_STATE_MANUAL_LOCKDOWN',
},
'PV_CHG_VOL_SPEC': {
0: 'PV_CHG_VOL_SPEC_RESV',
1: 'PV_CHG_VOL_SPEC_12V',
2: 'PV_CHG_VOL_SPEC_24V',
3: 'PV_CHG_VOL_SPEC_48V',
},
'PV_PLUG_INDEX': {
0: 'PV_PLUG_INDEX_RESV',
1: 'PV_PLUG_INDEX_1',
2: 'PV_PLUG_INDEX_2',
},
'SERVE_MIDDLEMEN': {
0: 'SERVE_MIDDLEMEN_NONE',
1: 'SERVE_MIDDLEMEN_SUNNOVA',
},
'SP_CHARGER_CHG_MODE': {
0: 'SP_CHARGER_CHG_MODE_IDLE',
1: 'SP_CHARGER_CHG_MODE_DRIVING_CHG',
2: 'SP_CHARGER_CHG_MODE_BAT_MAINTENANCE',
3: 'SP_CHARGER_CHG_MODE_PARKING_CHG',
},
'STATISTICS_OBJECT': {
0: 'STATISTICS_OBJECT_START',
1: 'STATISTICS_OBJECT_DEV_WORK_TIME',
2: 'STATISTICS_OBJECT_AC_OUT_ENERGY',
3: 'STATISTICS_OBJECT_DC12V_OUT_ENERGY',
4: 'STATISTICS_OBJECT_TYPEC_OUT_ENERGY',
5: 'STATISTICS_OBJECT_USBA_OUT_ENERGY',
6: 'STATISTICS_OBJECT_AC_IN_ENERGY',
7: 'STATISTICS_OBJECT_PV_IN_ENERGY',
8: 'STATISTICS_OBJECT_AC_IN_0W_100W_TIME',
9: 'STATISTICS_OBJECT_AC_IN_OVER_100W_TIME',
10: 'STATISTICS_OBJECT_AC_OUT_0W_50W_TIME',
11: 'STATISTICS_OBJECT_AC_OUT_50W_100W_TIME',
12: 'STATISTICS_OBJECT_AC_OUT_100W_200W_TIME',
13: 'STATISTICS_OBJECT_AC_OUT_OVER_200W_TIME',
14: 'STATISTICS_OBJECT_AC_OUT_200W_300W_TIME',
15: 'STATISTICS_OBJECT_AC_OUT_300W_400W_TIME',
16: 'STATISTICS_OBJECT_AC_OUT_400W_500W_TIME',
17: 'STATISTICS_OBJECT_AC_OUT_OVER_500W_TIME',
18: 'STATISTICS_OBJECT_PV_IN_TIME',
19: 'STATISTICS_OBJECT_TYPEC_IN_TIME',
20: 'STATISTICS_OBJECT_DC_OUT_0W_60W_TIME',
21: 'STATISTICS_OBJECT_DC_OUT_OVER_60W_TIME',
22: 'STATISTICS_OBJECT_TYPEC_OUT_0W_30W_TIME',
23: 'STATISTICS_OBJECT_TYPEC_OUT_30W_60W_TIME',
24: 'STATISTICS_OBJECT_TYPEC_OUT_OVER_60W_TIME',
25: 'STATISTICS_OBJECT_USBA_OUT_TIME',
26: 'STATISTICS_OBJECT_LED_OUT_TIME',
},
'TIME_TASK_MODE': {
0: 'TIME_TASK_MODE_RESV',
1: 'TIME_TASK_MODE_PER_WEEK',
2: 'TIME_TASK_MODE_ONCE',
},
'TIME_TASK_TYPE': {
0: 'TIME_TASK_TYPE_AC_CHG',
1: 'TIME_TASK_TYPE_AC_DSG',
2: 'TIME_TASK_TYPE_AC2_DSG',
3: 'TIME_TASK_TYPE_DC_CHG',
4: 'TIME_TASK_TYPE_DC2_CHG',
5: 'TIME_TASK_TYPE_DC_DSG',
6: 'TIME_TASK_TYPE_OIL_ON',
7: 'TIME_TASK_TYPE_OIL_OFF',
8: 'TIME_TASK_TYPE_USB_CHG',
9: 'TIME_TASK_TYPE_USB_DSG',
},
}