|
1 | 1 | import argparse |
| 2 | +import datetime |
2 | 3 | import json |
3 | 4 | import time |
4 | 5 | import sys |
5 | | -from rich import print_json |
6 | 6 | import logging |
| 7 | +import itertools |
| 8 | + |
| 9 | +from rich import print_json |
7 | 10 | from pylontech import * |
| 11 | +from pymongo import MongoClient |
| 12 | + |
8 | 13 | logger = logging.getLogger(__name__) |
9 | 14 |
|
10 | 15 |
|
| 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 | + |
11 | 64 | def run(argv: list[str]): |
12 | 65 | parser = argparse.ArgumentParser(description="Pylontech RS485 poller") |
13 | 66 |
|
14 | 67 | parser.add_argument("source_host", help="Telnet host") |
| 68 | + |
15 | 69 | parser.add_argument("--source-port", help="Telnet host", default=23) |
16 | 70 | 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) |
18 | 72 | 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") |
19 | 77 |
|
20 | 78 | args = parser.parse_args(argv[1:]) |
21 | 79 |
|
22 | 80 | level = logging.DEBUG if args.debug else logging.INFO |
23 | 81 | logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p', level=level) |
24 | 82 |
|
25 | | - print(args) |
26 | | - |
27 | 83 | cc = 0 |
28 | 84 | spinner = ['|', '/', '-', '\\'] |
29 | 85 |
|
30 | 86 | while True: |
31 | 87 | try: |
32 | 88 | logging.debug("Preparing client...") |
33 | 89 | 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 | + |
34 | 99 | logging.info("About to start polling...") |
35 | 100 | bats = p.scan_for_batteries(2, 10) |
36 | | - logging.info("Have battery stack data") |
37 | 101 |
|
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)}) |
39 | 104 |
|
40 | 105 | for b in p.poll_parameters(bats.range()): |
41 | | - #print_json(json.dumps(b)) |
42 | 106 | cc += 1 |
43 | | - |
| 107 | + |
44 | 108 | if sys.stdout.isatty(): |
45 | 109 | sys.stdout.write('\r' + spinner[cc % len(spinner)]) |
46 | 110 | sys.stdout.flush() |
47 | 111 |
|
| 112 | + # print(print_json(json.dumps(minimize(b)))) |
| 113 | + collection_hist.insert_one(minimize(b)) |
| 114 | + |
48 | 115 | time.sleep(args.interval / 1000.0) |
49 | 116 | except (KeyboardInterrupt, SystemExit): |
50 | 117 | exit(0) |
|
0 commit comments