Skip to content

Commit f3099fe

Browse files
committed
wip: poller
1 parent be8c67e commit f3099fe

5 files changed

Lines changed: 125 additions & 11 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ socat -v pty,link=/tmp/serial,waitslave tcp:192.168.1.7:23,forever
8989
uv run python ./demos/test-serial.py /tmp/serial 1
9090
```
9191

92+
## How to run mongodb collector
93+
94+
```bash
95+
uv run poller 192.168.1.7 --mongo-url mongodb://mongodb.local:27017
96+
```
97+
9298
# Hardware wiring
9399
The pylontech modules talk using the RS485 line protocol.
94100
## Pylontech side

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependencies = [
3030
"standard-telnetlib",
3131
"Exscript",
3232
"rich",
33+
"pymongo",
3334
]
3435
url = "http://github.com/Frankkkkk/python-pylontech"
3536

src/pylontech/tools.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,16 @@ def to_json_serializable(obj):
4343
import base64
4444

4545
if isinstance(obj, Container):
46-
return {k: to_json_serializable(v) for k, v in obj.items() if k != "_io"}
46+
return {str(k): to_json_serializable(v) for k, v in obj.items() if k != "_io"}
4747
elif isinstance(obj, dict):
48-
return {k: to_json_serializable(v) for k, v in obj.items() if k != "_io"}
48+
return {str(k): to_json_serializable(v) for k, v in obj.items() if k != "_io"}
4949
elif isinstance(obj, list):
5050
return [to_json_serializable(v) for v in obj]
5151
elif isinstance(obj, BytesIO):
5252
return base64.b64encode(obj.getvalue()).decode('utf-8') # or use .hex()
5353
elif isinstance(obj, bytes):
5454
return base64.b64encode(obj).decode('utf-8') # or use obj.hex()
5555
elif hasattr(obj, '__dict__'):
56-
return {k: to_json_serializable(v) for k, v in vars(obj).items()}
56+
return {str(k): to_json_serializable(v) for k, v in vars(obj).items()}
5757
else:
5858
return obj

src/pylontechpoller/poller.py

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,117 @@
11
import argparse
2+
import datetime
23
import json
34
import time
45
import sys
5-
from rich import print_json
66
import logging
7+
import itertools
8+
9+
from rich import print_json
710
from pylontech import *
11+
from pymongo import MongoClient
12+
813
logger = logging.getLogger(__name__)
914

1015

16+
def find_min_max_modules(modules):
17+
all_voltages = []
18+
for module in modules:
19+
for voltage in module["CellVoltages"]:
20+
all_voltages.append((module["NumberOfModule"], voltage))
21+
22+
if not all_voltages:
23+
return None, None
24+
25+
min_pair = min(all_voltages, key=lambda x: x[1])
26+
max_pair = max(all_voltages, key=lambda x: x[1])
27+
28+
return min_pair, max_pair
29+
30+
31+
32+
def minimize(b: json) -> json:
33+
def minimize_module(m: json) -> json:
34+
return {
35+
"n": m["NumberOfModule"],
36+
"v": m["Voltage"],
37+
"cv": m["CellVoltages"],
38+
"current": m["Current"],
39+
"pw": m["Power"],
40+
"cycle": m["CycleNumber"],
41+
"soc": m["StateOfCharge"],
42+
"tempavg": m["AverageBMSTemperature"],
43+
"temps": m["GroupedCellsTemperatures"],
44+
"remaining": m["RemainingCapacity"],
45+
"disbalance": max(m["CellVoltages"]) - min(m["CellVoltages"])
46+
}
47+
48+
modules = b["modules"]
49+
find_min_max_modules(modules)
50+
51+
(min_pair, max_pair) = find_min_max_modules(modules)
52+
# allcv = list(itertools.chain.from_iterable(map(lambda m: m["CellVoltages"], modules)))
53+
# vmin = min(allcv)
54+
# vmax = max(allcv)
55+
56+
return {
57+
"ts": b["timestamp"],
58+
"cvmin": min_pair,
59+
"cvmax": max_pair,
60+
"stack_disbalance": min_pair[1] - max_pair[1],
61+
"modules": list(map(minimize_module, modules)),
62+
}
63+
1164
def run(argv: list[str]):
1265
parser = argparse.ArgumentParser(description="Pylontech RS485 poller")
1366

1467
parser.add_argument("source_host", help="Telnet host")
68+
1569
parser.add_argument("--source-port", help="Telnet host", default=23)
1670
parser.add_argument("--timeout", type=int, help="timeout", default=2)
17-
parser.add_argument("--interval", type=int, help="polling interval in msec", default=500)
71+
parser.add_argument("--interval", type=int, help="polling interval in msec", default=1)
1872
parser.add_argument("--debug", type=bool, help="verbose output", default=False)
73+
parser.add_argument("--mongo-url", type=str, help="mongodb url", default=False)
74+
parser.add_argument("--mongo-db", type=str, help="target mongo database", default="pylontech")
75+
parser.add_argument("--mongo-collection-history", type=str, help="target mongo collection_hist for stack history", default="history")
76+
parser.add_argument("--mongo-collection-meta", type=str, help="target mongo collection_hist for stack data", default="meta")
1977

2078
args = parser.parse_args(argv[1:])
2179

2280
level = logging.DEBUG if args.debug else logging.INFO
2381
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p', level=level)
2482

25-
print(args)
26-
2783
cc = 0
2884
spinner = ['|', '/', '-', '\\']
2985

3086
while True:
3187
try:
3288
logging.debug("Preparing client...")
3389
p = Pylontech(ExscriptTelnetTransport(host=args.source_host, port=args.source_port, timeout=args.timeout))
90+
91+
mongo = MongoClient(args.mongo_url)
92+
db = mongo[args.mongo_db]
93+
94+
collection_meta = db[args.mongo_collection_meta]
95+
96+
collection_hist = db[args.mongo_collection_history]
97+
collection_hist.create_index("createdAt", expireAfterSeconds=3600*24*90)
98+
3499
logging.info("About to start polling...")
35100
bats = p.scan_for_batteries(2, 10)
36-
logging.info("Have battery stack data")
37101

38-
#print_json(json.dumps(to_json_serializable(bats)))
102+
logging.info("Have battery stack data")
103+
collection_meta.insert_one({'ts': datetime.datetime.now().isoformat(), "stack": to_json_serializable(bats)})
39104

40105
for b in p.poll_parameters(bats.range()):
41-
#print_json(json.dumps(b))
42106
cc += 1
43-
107+
44108
if sys.stdout.isatty():
45109
sys.stdout.write('\r' + spinner[cc % len(spinner)])
46110
sys.stdout.flush()
47111

112+
# print(print_json(json.dumps(minimize(b))))
113+
collection_hist.insert_one(minimize(b))
114+
48115
time.sleep(args.interval / 1000.0)
49116
except (KeyboardInterrupt, SystemExit):
50117
exit(0)

uv.lock

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)