Business operations API: get_battery_soc/get_household_wattage/get_device_inventory

This commit is contained in:
kfj001 committed 2026-10-16 04:58:33 +00:00
1 parent 363c7cc6a2
commit b79adc5d54
7 files changed
+307 -112

No files matched your search

+121 -1
View File
@@ -330,7 +330,13 @@ class LocalDeviceClient:
return self.send_rpc(CommunicationCmd.EMS)
def query_a_power(self) -> Dict[str, Any]:
return self.send_rpc(CommunicationCmd.DEVICE_APOWER_INFO)
"""``queryAPowerData`` — apower query (``arguments={"update": 2}``).
Note: the tested unit answers ``{"result": 1, "reason": -1}`` here;
prefer :meth:`query_bms_info` / :meth:`get_battery_soc` for charge.
"""
return self.send_rpc(CommunicationCmd.DEVICE_APOWER_INFO,
data={"update": 2})
def query_warning_list(self) -> Dict[str, Any]:
return self.send_rpc(CommunicationCmd.DEVICE_ALARMS)
@@ -347,3 +353,117 @@ class LocalDeviceClient:
def query_modes_v2(self) -> Dict[str, Any]:
return self.send_rpc(CommunicationCmd.MODE_V2)
# --------------------------------------------------- business operations
# Intention-revealing reads: each runs the app's query sequence for one
# question and returns ``{"value", "source", "detail", "reply", ...}``.
# Transport errors propagate (caller picks the next port/device);
# answered-but-empty replies yield ``value: None``.
def get_device_inventory(self) -> Dict[str, Any]:
"""Discovery + device info: identities, versions, FHP/BMS serials.
Mirrors the direct-connect open: a best-effort discovery query
(empty serial), then ``queryDeviceInfo``. Returns the parsed
device-info reply plus an ``identity`` summary (``ibg_sn``,
``fhp_sn``/``bms_sn`` lists, ``protocol_ver``).
"""
try:
self.send_rpc(CommunicationCmd.QUERY_DEVICE_INFO,
equip_no="",
data={"minProtocolVer": "V1.00.00"})
except Exception:
log.debug("discovery probe failed (best-effort)", exc_info=True)
reply = self.send_rpc(
CommunicationCmd.QUERY_DEVICE_INFO,
data={"minProtocolVer": "V1.00.00"})
data = reply.get("dataArea", {})
if not isinstance(data, dict):
data = {}
return {
"reply": reply,
"ibg_sn": reply.get("equipNo"),
"fhp_sn": list(data.get("FHP_SN", []) or []),
"bms_sn": list(data.get("BMS_SN", []) or []),
"protocol_ver": data.get("protocolVer"),
"result": data.get("result"),
}
def get_battery_soc(self) -> Dict[str, Any]:
"""Aggregate battery charge percentage for this device.
Tries apower, then per-pack BMS reads (``id`` 1..2 — the id-less
slot answers all zeros), skipping empty placeholder slots.
Returns ``extract_soc`` fields plus ``reply``/``cmd`` context;
``value`` is ``None`` when nothing answered with charge data.
"""
from .battery import _is_empty_pack, extract_soc
try: # best-effort open, mirrors the direct-connect flow
self.send_rpc(CommunicationCmd.QUERY_DEVICE_INFO,
equip_no="",
data={"minProtocolVer": "V1.00.00"})
except Exception:
log.debug("discovery probe failed (best-effort)", exc_info=True)
last: Dict[str, Any] = {"value": None, "source": None,
"detail": {}, "reply": None, "cmd": None}
for cmd, data in (
(CommunicationCmd.DEVICE_APOWER_INFO, {"update": 2}),
(CommunicationCmd.BMS, {"id": 1}),
(CommunicationCmd.BMS, {"id": 2}),
):
try:
reply = self.send_rpc(cmd, data=data)
except (ConnectionError, socket.error, TimeoutError):
raise
except Exception as exc: # envelope-level failure; next attempt
log.debug("get_battery_soc %s error: %s", cmd, exc)
continue
result = extract_soc(reply)
result.update({"reply": reply, "cmd": str(cmd)})
if (result["value"] is not None
and not _is_empty_pack(reply, result)):
return result
last = result
if last["value"] is not None and _is_empty_pack(
last.get("reply") or {}, last):
# every attempt hit placeholder slots: unknown, not 0 %.
last = dict(last, value=None, source=None)
return last
def get_household_wattage(self) -> Dict[str, Any]:
"""Live household wattage draw for this device.
Tries realtime detail (``totalPower``), then per-switch load
status, then EMS current×voltage math. Returns ``extract_power``
fields plus ``reply``/``cmd`` context; ``value`` is ``None`` when
nothing answered with power data.
"""
from .power import _power_detail, ems_load_watts, extract_power
last: Dict[str, Any] = {"value": None, "source": None,
"detail": {}, "reply": None, "cmd": None}
for name, call in (
("detail", self.query_detail_info),
("load", self.query_load_status),
("ems", self.query_ems_info),
):
try:
reply = call()
except (ConnectionError, socket.error, TimeoutError):
raise
except Exception as exc: # envelope-level failure; next attempt
log.debug("get_household_wattage %s error: %s", name, exc)
continue
result = extract_power(reply)
if result["value"] is None and name == "ems":
watts = ems_load_watts(reply)
if watts is not None:
result = {"value": watts, "source": "ems(loadCurr*V)",
"detail": _power_detail(
reply.get("dataArea", reply))}
result.update({"reply": reply, "cmd": name})
if result["value"] is not None:
return result
last = result
return last