Mercurial > ~darius > hgwebdir.cgi > pw2log
comparison pw2log.py @ 0:a5a196b3ba63
Initial version of powerwall logger
author | Daniel O'Connor <darius@dons.net.au> |
---|---|
date | Wed, 20 Nov 2019 13:12:45 +1030 |
parents | |
children | 7edf54ec37f2 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:a5a196b3ba63 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 import configparser | |
4 import daemon | |
5 import daemon.pidfile | |
6 import datetime | |
7 import logging | |
8 from logging.handlers import RotatingFileHandler | |
9 import psycopg2 | |
10 import requests | |
11 import sys | |
12 import tesla_powerwall # https://github.com/jrester/tesla_powerwall | |
13 import time | |
14 | |
15 # Standard in 3.7.. | |
16 class NullContextManager(object): | |
17 def __init__(self, dummy_resource=None): | |
18 self.dummy_resource = dummy_resource | |
19 def __enter__(self): | |
20 return self.dummy_resource | |
21 def __exit__(self, *args): | |
22 pass | |
23 | |
24 # Otherwise it's very noisy | |
25 logging.getLogger('tesla_powerwall').setLevel(logging.WARN) | |
26 | |
27 def main(): | |
28 if len(sys.argv) != 2: | |
29 print('Bad usage', file = sys.stderr) | |
30 print('\t%s conf.ini' % (sys.argv[0]), file = sys.stderr) | |
31 sys.exit(1) | |
32 | |
33 cp = configparser.ConfigParser() | |
34 cp.read(sys.argv[1]) | |
35 if not cp.has_section('db'): | |
36 print('Config file missing db section', file = sys.stderr) | |
37 sys.exit(1) | |
38 if not cp.has_option('db', 'dsn'): | |
39 print('db section missing dsn parameter', file = sys.stderr) | |
40 sys.exit(1) | |
41 if not cp.has_option('db', 'logtime'): | |
42 print('db section missing logtime parameter', file = sys.stderr) | |
43 sys.exit(1) | |
44 | |
45 if not cp.has_section('pw'): | |
46 print('Config file missing pw section', file = sys.stderr) | |
47 sys.exit(1) | |
48 if not cp.has_option('pw', 'ip'): | |
49 print('pw section missing ip parameter', file = sys.stderr) | |
50 sys.exit(1) | |
51 | |
52 if cp.has_option('pw2log', 'logfile'): | |
53 logfile = cp.get('pw2log', 'logfile') | |
54 else: | |
55 logfile = None | |
56 if cp.has_option('pw2log', 'pidfile'): | |
57 pidfile = cp.get('pw2log', 'pidfile') | |
58 else: | |
59 pidfile = None | |
60 | |
61 global logger | |
62 logger = logging.getLogger('pw2log') | |
63 logger.setLevel(logging.WARN) | |
64 fmt = logging.Formatter('%(asctime)s: %(message)s', datefmt = '%Y/%m/%d %H:%M:%S') | |
65 if logfile == None: | |
66 ch = logging.StreamHandler() | |
67 ch.setFormatter(fmt) | |
68 logger.addHandler(ch) | |
69 else: | |
70 fh = RotatingFileHandler(logfile, maxBytes = 2000, backupCount = 10) | |
71 fh.setFormatter(fmt) | |
72 logger.addHandler(fh) | |
73 | |
74 if pidfile == None: | |
75 ctx = NullContextManager() | |
76 else: | |
77 ctx = daemon.DaemonContext(pidfile = daemon.pidfile.PIDLockFile(pidfile)) | |
78 | |
79 with ctx: | |
80 logger.critical('Starting') | |
81 collectdata(cp.get('pw', 'ip'), cp.get('db', 'dsn'), cp.getint('db', 'logtime')) | |
82 | |
83 def collectdata(pwip, dsn, logtime): | |
84 dbh = psycopg2.connect(dsn) | |
85 cur = dbh.cursor() | |
86 | |
87 pw = tesla_powerwall.PowerWall(pwip) | |
88 | |
89 while True: | |
90 try: | |
91 # As per.. https://github.com/vloschiavo/powerwall2 | |
92 # | | Load | Grid | Battery | Solar | | |
93 # |==========+==============+===================+======================+==================| | |
94 # | Positive | Supply house | Drawing from grid | Drawing from battery | Solar generation | | |
95 # | Negative | n/a | Feeding grid | Charging battery | n/a | | |
96 # | |
97 grid = pw.grid | |
98 load = pw.load | |
99 battery = pw.battery | |
100 solar = pw.solar | |
101 charge = pw.charge | |
102 except requests.ConnectionError as e: | |
103 logger.error('Error communicating with Powerwall: ' + str(e)) | |
104 time.sleep(300) | |
105 continue | |
106 try: | |
107 cur.execute('INSERT INTO pw2 (date, grid_voltage, grid_freq, grid_power, load_power, battery_power, battery_charge, solar_power) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)', | |
108 (datetime.datetime.now(), grid.instant_average_voltage, grid.frequency, grid.instant_power, load.instant_power, battery.instant_power, charge, solar.instant_power)) | |
109 dbh.commit() | |
110 except psycopg2.OperationalError as e: | |
111 logger.error('Reconnecting after database error:' + str(e)) | |
112 time.sleep(60) | |
113 dbh = psycopg2.connect(dsn) | |
114 cur = dbh.cursor() | |
115 continue | |
116 | |
117 time.sleep(logtime) | |
118 | |
119 def createdb(dbh): | |
120 cur = dbh.cursor() | |
121 cur.execute(''' | |
122 CREATE TABLE pw2 ( | |
123 date TIMESTAMP WITH TIME ZONE PRIMARY KEY, | |
124 grid_voltage REAL, | |
125 grid_freq REAL, | |
126 grid_power REAL, | |
127 load_power REAL, | |
128 battery_power REAL, | |
129 battery_charge REAL, | |
130 solar_power REAL | |
131 ); | |
132 ''') | |
133 cur.execute(''' | |
134 CREATE INDEX IF NOT EXISTS pw2_date_brin_idx ON pw2 USING brin (date); | |
135 ''') | |
136 | |
137 if __name__ == '__main__': | |
138 main() |