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()