Business operations API: get_battery_soc/get_household_wattage/get_device_inventory
This commit is contained in:
1 parent
363c7cc6a2
commit
b79adc5d54
7 files changed
+307
-112
No files matched your search
@@ -59,6 +59,21 @@ out = get_battery("...", host="10.100.1.1")
|
||||
print(out["value"], out["source"]) # e.g. 78.0 currentBatterySoc
|
||||
```
|
||||
|
||||
Business operations (one call per question, on a bound client):
|
||||
|
||||
```python
|
||||
from franklinwh_agate_client.device_local import LocalDeviceClient
|
||||
client = LocalDeviceClient("A03X2516013210060005", host="10.100.1.1",
|
||||
socket_port=9000)
|
||||
client.get_battery_soc() # charge % (apower, then BMS packs 1-2)
|
||||
client.get_household_wattage() # watts (detail, then load, then EMS math)
|
||||
client.get_device_inventory() # IBG/FHP/BMS serials + versions
|
||||
```
|
||||
|
||||
`get_*` returns `{"value", "source", "detail", "reply", ...}` with
|
||||
`value: None` when nothing answered; transport errors propagate so
|
||||
multi-port callers (`get_battery`, `get_power`) can move on.
|
||||
|
||||
## Host probe recipe (run on the machine joined to the device AP)
|
||||
|
||||
The agent container cannot reach `10.100.1.x`; run this on the host:
|
||||
|
||||
@@ -168,34 +168,16 @@ def get_battery(
|
||||
key_radix=key_radix, timeout=timeout,
|
||||
)
|
||||
try:
|
||||
client.send_rpc(CommunicationCmd.QUERY_DEVICE_INFO,
|
||||
equip_no="",
|
||||
data={"minProtocolVer": "V1.00.00"})
|
||||
except Exception as exc: # discovery is best-effort; keep going
|
||||
log.debug("port %d discovery failed: %s", port, exc)
|
||||
attempts: Tuple[Tuple[CommunicationCmd, Any], ...] = (
|
||||
(CommunicationCmd.DEVICE_APOWER_INFO, {"update": 2}),
|
||||
(CommunicationCmd.BMS, {"id": 1}),
|
||||
(CommunicationCmd.BMS, {"id": 2}),
|
||||
(CommunicationCmd.EMS, None),
|
||||
)
|
||||
for cmd, data in attempts:
|
||||
try:
|
||||
reply = client.send_rpc(cmd, data=data)
|
||||
except (ConnectionError, socket.error, TimeoutError) as exc:
|
||||
last_err = exc
|
||||
log.debug("port %d cmd %s failed: %s", port, cmd, exc)
|
||||
break # port unreachable — try next port
|
||||
except Exception as exc: # envelope-level failure; try next cmd
|
||||
last_err = exc
|
||||
log.debug("port %d cmd %s error: %s", port, cmd, exc)
|
||||
continue
|
||||
result = extract_soc(reply)
|
||||
result.update({"cmd": str(cmd), "port": port, "reply": reply})
|
||||
if result["value"] is not None and not _is_empty_pack(
|
||||
reply, result):
|
||||
return result
|
||||
last_err = ValueError(f"reply on port {port} cmd {cmd} has no SoC field")
|
||||
result = client.get_battery_soc()
|
||||
except (ConnectionError, socket.error, TimeoutError) as exc:
|
||||
last_err = exc
|
||||
log.debug("port %d failed: %s", port, exc)
|
||||
continue # port unreachable — try next port
|
||||
if result["value"] is not None:
|
||||
result.update({"port": port})
|
||||
return result
|
||||
last_err = ValueError(
|
||||
f"no SoC field in replies via {host}:{port}")
|
||||
raise ConnectionError(f"no battery reply via {host}:{list(ports)}") from last_err
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -119,33 +119,17 @@ def get_power(
|
||||
sn=sn, host=host, socket_port=port,
|
||||
key_radix=key_radix, timeout=timeout,
|
||||
)
|
||||
for name, call in (
|
||||
("detail", client.query_detail_info),
|
||||
("load", client.query_load_status),
|
||||
("ems", client.query_ems_info),
|
||||
):
|
||||
try:
|
||||
reply = call()
|
||||
except (ConnectionError, socket.error, TimeoutError) as exc:
|
||||
last_err = exc
|
||||
log.debug("port %d %s failed: %s", port, name, exc)
|
||||
break
|
||||
except Exception as exc:
|
||||
last_err = exc
|
||||
log.debug("port %d %s error: %s", port, 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({"cmd": name, "port": port, "reply": reply})
|
||||
if result["value"] is not None:
|
||||
return result
|
||||
last_err = ValueError(
|
||||
f"reply on port {port} {name} has no power field")
|
||||
try:
|
||||
result = client.get_household_wattage()
|
||||
except (ConnectionError, socket.error, TimeoutError) as exc:
|
||||
last_err = exc
|
||||
log.debug("port %d failed: %s", port, exc)
|
||||
continue # port unreachable — try next port
|
||||
if result["value"] is not None:
|
||||
result.update({"port": port})
|
||||
return result
|
||||
last_err = ValueError(
|
||||
f"no power field in replies via {host}:{port}")
|
||||
raise ConnectionError(f"no power reply via {host}:{list(ports)}") from last_err
|
||||
|
||||
|
||||
|
||||
+21
-26
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Host probe v7 — per-pack BMS reads via the library business API.
|
||||
"""Host probe v7 — battery charge via the business operations API.
|
||||
|
||||
The app's ``queryBmsInfoBySocket`` passes ``arguments={"id": pack}``; the
|
||||
id-less query answers the all-zero ``id: 0`` slot. Pack ids 1..2 select
|
||||
the two batteries (``BMS_SN …B1/…B2``); ``batSoc`` is the pack charge.
|
||||
One call per port: ``LocalDeviceClient.get_battery_soc`` (discovery,
|
||||
apower, per-pack BMS reads). Prints the percentage.
|
||||
|
||||
Run from the repo root on the SoftAP-joined host:
|
||||
python3 scripts/host_probe7.py --sn A03X2516013210060005 --ports 9000
|
||||
@@ -18,48 +17,44 @@ import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__))))
|
||||
|
||||
from franklinwh_agate_client.battery import extract_soc # noqa: E402
|
||||
from franklinwh_agate_client.device_local import ( # noqa: E402
|
||||
LocalDeviceClient,
|
||||
)
|
||||
from franklinwh_agate_client.protocol import ( # noqa: E402
|
||||
CommunicationCmd,
|
||||
generate_secret_key,
|
||||
)
|
||||
|
||||
|
||||
def run(label, client, call):
|
||||
try:
|
||||
reply = call()
|
||||
except (ConnectionError, socket.error, TimeoutError) as exc:
|
||||
print("--- %s: error: %s: %s" % (label, type(exc).__name__, exc))
|
||||
return None
|
||||
print("--- %s (%d B reply)" % (label, len(json.dumps(reply))))
|
||||
print(" reply: " + json.dumps(reply, default=str)[:1500])
|
||||
result = extract_soc(reply)
|
||||
print(" ==> soc: %s (source=%s)" % (result["value"], result["source"]))
|
||||
return reply
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--sn", required=True)
|
||||
ap.add_argument("--host", default="10.100.1.1")
|
||||
ap.add_argument("--ports", default="9000")
|
||||
ap.add_argument("--timeout", type=float, default=5.0)
|
||||
ap.add_argument("--raw", action="store_true",
|
||||
help="print the full reply envelope")
|
||||
args = ap.parse_args()
|
||||
print("secret key for %s: %s (radix 16)"
|
||||
% (args.sn, generate_secret_key(args.sn)))
|
||||
for port in [int(p) for p in args.ports.split(",") if p.strip()]:
|
||||
print("=== port %d" % port)
|
||||
client = LocalDeviceClient(args.sn, host=args.host,
|
||||
socket_port=port, timeout=args.timeout)
|
||||
run("bms/id1", client, lambda: client.query_bms_info(1))
|
||||
run("bms/id2", client, lambda: client.query_bms_info(2))
|
||||
run("apower/update2", client, lambda: client.send_rpc(
|
||||
CommunicationCmd.DEVICE_APOWER_INFO, data={"update": 2}))
|
||||
run("ems", client, client.query_ems_info)
|
||||
try:
|
||||
result = client.get_battery_soc()
|
||||
except (ConnectionError, socket.error, TimeoutError) as exc:
|
||||
print("=== port %d: error: %s: %s"
|
||||
% (port, type(exc).__name__, exc))
|
||||
continue
|
||||
print("=== port %d" % port)
|
||||
if args.raw:
|
||||
print(json.dumps(result.get("reply"), indent=2, default=str))
|
||||
return 0
|
||||
value = result["value"]
|
||||
print("==> soc: %s%% (source=%s, cmd=%s)"
|
||||
% (value, result["source"], result["cmd"])
|
||||
if value is not None else "==> soc: unknown")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
raise SystemExit(main())
|
||||
+23
-30
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Host probe v8 — live household wattage via the library business API.
|
||||
"""Host probe v8 — household wattage via the business operations API.
|
||||
|
||||
Three socket APIs: realtime detail (home-page poll) -> ``totalPower``,
|
||||
per-switch load status -> ``SWxExpPower``, EMS telemetry -> watts math.
|
||||
One call per port: ``LocalDeviceClient.get_household_wattage`` (realtime
|
||||
detail → per-switch load → EMS math). Prints the watts.
|
||||
|
||||
Run from the repo root on the SoftAP-joined host:
|
||||
python3 scripts/host_probe8.py --sn A03X2516013210060005 --ports 9000
|
||||
@@ -20,50 +20,43 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(
|
||||
from franklinwh_agate_client.device_local import ( # noqa: E402
|
||||
LocalDeviceClient,
|
||||
)
|
||||
from franklinwh_agate_client.power import ( # noqa: E402
|
||||
ems_load_watts,
|
||||
extract_power,
|
||||
)
|
||||
from franklinwh_agate_client.protocol import ( # noqa: E402
|
||||
generate_secret_key,
|
||||
)
|
||||
|
||||
|
||||
def run(label, client, call):
|
||||
try:
|
||||
reply = call()
|
||||
except (ConnectionError, socket.error, TimeoutError) as exc:
|
||||
print("--- %s: error: %s: %s" % (label, type(exc).__name__, exc))
|
||||
return None
|
||||
print("--- %s" % label)
|
||||
print(" reply: " + json.dumps(reply, default=str)[:2000])
|
||||
result = extract_power(reply)
|
||||
value, source = result["value"], result["source"]
|
||||
if value is None:
|
||||
watts = ems_load_watts(reply)
|
||||
if watts is not None:
|
||||
value, source = watts, "ems(loadCurr*V)"
|
||||
print(" ==> watts: %s (source=%s)" % (value, source))
|
||||
return reply
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--sn", required=True)
|
||||
ap.add_argument("--host", default="10.100.1.1")
|
||||
ap.add_argument("--ports", default="9000")
|
||||
ap.add_argument("--timeout", type=float, default=5.0)
|
||||
ap.add_argument("--raw", action="store_true",
|
||||
help="print the full reply envelope")
|
||||
args = ap.parse_args()
|
||||
print("secret key for %s: %s (radix 16)"
|
||||
% (args.sn, generate_secret_key(args.sn)))
|
||||
for port in [int(p) for p in args.ports.split(",") if p.strip()]:
|
||||
print("=== port %d" % port)
|
||||
client = LocalDeviceClient(args.sn, host=args.host,
|
||||
socket_port=port, timeout=args.timeout)
|
||||
run("detail/1301", client, client.query_detail_info)
|
||||
run("load/1411", client, client.query_load_status)
|
||||
run("ems/1707", client, client.query_ems_info)
|
||||
try:
|
||||
result = client.get_household_wattage()
|
||||
except (ConnectionError, socket.error, TimeoutError) as exc:
|
||||
print("=== port %d: error: %s: %s"
|
||||
% (port, type(exc).__name__, exc))
|
||||
continue
|
||||
print("=== port %d" % port)
|
||||
if args.raw:
|
||||
print(json.dumps(result.get("reply"), indent=2, default=str))
|
||||
return 0
|
||||
value = result["value"]
|
||||
if value is not None:
|
||||
print("==> watts: %gW (source=%s, cmd=%s)"
|
||||
% (value, result["source"], result["cmd"]))
|
||||
else:
|
||||
print("==> watts: unknown")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Offline tests for the business operations (transport stubbed)."""
|
||||
|
||||
import unittest
|
||||
|
||||
from franklinwh_agate_client.device_local import LocalDeviceClient
|
||||
from franklinwh_agate_client.protocol import CommunicationCmd
|
||||
|
||||
|
||||
def _client(handler):
|
||||
client = LocalDeviceClient("SN", host="127.0.0.1", timeout=0.1)
|
||||
calls = []
|
||||
|
||||
def fake_send_rpc(cmd, equip_no=None, operate=None, data=None,
|
||||
envelope=None):
|
||||
calls.append((cmd, equip_no, data))
|
||||
return handler(cmd, equip_no, data)
|
||||
|
||||
client.send_rpc = fake_send_rpc
|
||||
return client, calls
|
||||
|
||||
|
||||
class TestBatterySocOp(unittest.TestCase):
|
||||
def test_apower_win(self):
|
||||
client, calls = _client(
|
||||
lambda cmd, eq, data: {"dataArea": {"currentBatterySoc": 78}})
|
||||
out = client.get_battery_soc()
|
||||
self.assertEqual(out["value"], 78)
|
||||
self.assertEqual(out["source"], "currentBatterySoc")
|
||||
|
||||
def test_bms_pack_fallback(self):
|
||||
def handler(cmd, eq, data):
|
||||
if cmd == CommunicationCmd.DEVICE_APOWER_INFO:
|
||||
return {"dataArea": {"result": 1, "reason": -1}}
|
||||
if (data or {}).get("id") == 1:
|
||||
return {"dataArea": {"opt": 0, "result": 0, "id": 1,
|
||||
"batSoc": 64, "batSoh": 99,
|
||||
"batTotalVolt": 512}}
|
||||
return {"dataArea": {"opt": 0, "result": 0, "id": 0,
|
||||
"batSoc": 0, "batSoh": 0,
|
||||
"batTotalVolt": 0}}
|
||||
|
||||
client, calls = _client(handler)
|
||||
out = client.get_battery_soc()
|
||||
self.assertEqual(out["value"], 64)
|
||||
self.assertEqual(out["source"], "batSoc")
|
||||
# discovery + apower + bms id=1 (empty id=0 never needed)
|
||||
ids = [d.get("id") if isinstance(d, dict) else None
|
||||
for _, _, d in calls]
|
||||
self.assertIn(1, ids)
|
||||
|
||||
def test_empty_slots_skipped(self):
|
||||
client, _ = _client(
|
||||
lambda cmd, eq, data: {"dataArea": {"opt": 0, "result": 0,
|
||||
"id": 0, "batSoc": 0,
|
||||
"batSoh": 0,
|
||||
"batTotalVolt": 0}})
|
||||
out = client.get_battery_soc()
|
||||
self.assertIsNone(out["value"])
|
||||
|
||||
def test_transport_error_propagates(self):
|
||||
client, _ = _client(None)
|
||||
|
||||
def boom(cmd, equip_no=None, operate=None, data=None,
|
||||
envelope=None):
|
||||
raise ConnectionError("down")
|
||||
|
||||
client.send_rpc = boom
|
||||
with self.assertRaises(ConnectionError):
|
||||
client.get_battery_soc()
|
||||
|
||||
|
||||
class TestWattageOp(unittest.TestCase):
|
||||
def test_detail_win(self):
|
||||
client, _ = _client(
|
||||
lambda cmd, eq, data: {"dataArea": {"apowerCount": 2,
|
||||
"totalPower": 1872.5}})
|
||||
out = client.get_household_wattage()
|
||||
self.assertEqual(out["value"], 1872.5)
|
||||
self.assertEqual(out["source"], "totalPower")
|
||||
|
||||
def test_ems_math_fallback(self):
|
||||
def handler(cmd, eq, data):
|
||||
if cmd == CommunicationCmd.EMS:
|
||||
return {"dataArea": {"gridVol1": 120.0, "gridVol2": 120.0,
|
||||
"loadCurr1": 5.0, "loadCurr2": 5.0}}
|
||||
return {"dataArea": {"result": 1, "reason": -1}}
|
||||
|
||||
client, _ = _client(handler)
|
||||
out = client.get_household_wattage()
|
||||
self.assertAlmostEqual(out["value"], 1200.0)
|
||||
self.assertEqual(out["source"], "ems(loadCurr*V)")
|
||||
|
||||
def test_inventory(self):
|
||||
client, _ = _client(
|
||||
lambda cmd, eq, data: {
|
||||
"cmdType": 1102, "equipNo": "IBG",
|
||||
"dataArea": {"opt": 0, "result": 0, "FHP_SN": ["A", "B"],
|
||||
"BMS_SN": ["C"], "protocolVer": "V1"}})
|
||||
out = client.get_device_inventory()
|
||||
self.assertEqual(out["ibg_sn"], "IBG")
|
||||
self.assertEqual(out["fhp_sn"], ["A", "B"])
|
||||
self.assertEqual(out["protocol_ver"], "V1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user