changeset 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
files pw2.ini pw2log.py
diffstat 2 files changed, 149 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pw2.ini	Wed Nov 20 13:12:45 2019 +1030
@@ -0,0 +1,11 @@
+[pw2log]
+logfile=/home/darius/projects/pw2log/pw2.log
+pidfile=/home/darius/projects/pw2log/pw2.pid
+
+[db]
+dsn=user=pw dbname=pw
+logtime=30
+
+[pw]
+ip=10.0.2.64
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pw2log.py	Wed Nov 20 13:12:45 2019 +1030
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+
+import configparser
+import daemon
+import daemon.pidfile
+import datetime
+import logging
+from logging.handlers import RotatingFileHandler
+import psycopg2
+import requests
+import sys
+import tesla_powerwall # https://github.com/jrester/tesla_powerwall
+import time
+
+# Standard in 3.7..
+class NullContextManager(object):
+    def __init__(self, dummy_resource=None):
+        self.dummy_resource = dummy_resource
+    def __enter__(self):
+        return self.dummy_resource
+    def __exit__(self, *args):
+        pass
+
+# Otherwise it's very noisy
+logging.getLogger('tesla_powerwall').setLevel(logging.WARN)
+
+def main():
+    if len(sys.argv) != 2:
+        print('Bad usage', file = sys.stderr)
+        print('\t%s conf.ini' % (sys.argv[0]), file = sys.stderr)
+        sys.exit(1)
+
+    cp = configparser.ConfigParser()
+    cp.read(sys.argv[1])
+    if not cp.has_section('db'):
+        print('Config file missing db section', file = sys.stderr)
+        sys.exit(1)
+    if not cp.has_option('db', 'dsn'):
+        print('db section missing dsn parameter', file = sys.stderr)
+        sys.exit(1)
+    if not cp.has_option('db', 'logtime'):
+        print('db section missing logtime parameter', file = sys.stderr)
+        sys.exit(1)
+
+    if not cp.has_section('pw'):
+        print('Config file missing pw section', file = sys.stderr)
+        sys.exit(1)
+    if not cp.has_option('pw', 'ip'):
+        print('pw section missing ip parameter', file = sys.stderr)
+        sys.exit(1)
+
+    if cp.has_option('pw2log', 'logfile'):
+        logfile = cp.get('pw2log', 'logfile')
+    else:
+        logfile = None
+    if cp.has_option('pw2log', 'pidfile'):
+        pidfile = cp.get('pw2log', 'pidfile')
+    else:
+        pidfile = None
+
+    global logger
+    logger = logging.getLogger('pw2log')
+    logger.setLevel(logging.WARN)
+    fmt = logging.Formatter('%(asctime)s: %(message)s', datefmt = '%Y/%m/%d %H:%M:%S')
+    if logfile == None:
+        ch = logging.StreamHandler()
+        ch.setFormatter(fmt)
+        logger.addHandler(ch)
+    else:
+        fh = RotatingFileHandler(logfile, maxBytes = 2000, backupCount = 10)
+        fh.setFormatter(fmt)
+        logger.addHandler(fh)
+
+    if pidfile == None:
+        ctx = NullContextManager()
+    else:
+        ctx = daemon.DaemonContext(pidfile = daemon.pidfile.PIDLockFile(pidfile))
+
+    with ctx:
+        logger.critical('Starting')
+        collectdata(cp.get('pw', 'ip'), cp.get('db', 'dsn'), cp.getint('db', 'logtime'))
+
+def collectdata(pwip, dsn, logtime):
+    dbh = psycopg2.connect(dsn)
+    cur = dbh.cursor()
+
+    pw = tesla_powerwall.PowerWall(pwip)
+
+    while True:
+        try:
+            # As per.. https://github.com/vloschiavo/powerwall2
+            # |          | Load         | Grid              | Battery              | Solar            |
+            # |==========+==============+===================+======================+==================|
+            # | Positive | Supply house | Drawing from grid | Drawing from battery | Solar generation |
+            # | Negative | n/a          | Feeding grid      | Charging battery     | n/a              |
+            #
+            grid = pw.grid
+            load = pw.load
+            battery = pw.battery
+            solar = pw.solar
+            charge = pw.charge
+        except requests.ConnectionError as e:
+            logger.error('Error communicating with Powerwall: ' + str(e))
+            time.sleep(300)
+            continue
+        try:
+            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)',
+            (datetime.datetime.now(), grid.instant_average_voltage, grid.frequency, grid.instant_power, load.instant_power, battery.instant_power, charge, solar.instant_power))
+            dbh.commit()
+        except psycopg2.OperationalError as e:
+            logger.error('Reconnecting after database error:' + str(e))
+            time.sleep(60)
+            dbh = psycopg2.connect(dsn)
+            cur = dbh.cursor()
+            continue
+
+        time.sleep(logtime)
+
+def createdb(dbh):
+    cur = dbh.cursor()
+    cur.execute('''
+CREATE TABLE pw2 (
+    date TIMESTAMP WITH TIME ZONE PRIMARY KEY,
+    grid_voltage REAL,
+    grid_freq REAL,
+    grid_power REAL,
+    load_power REAL,
+    battery_power REAL,
+    battery_charge REAL,
+    solar_power REAL
+);
+''')
+    cur.execute('''
+CREATE INDEX IF NOT EXISTS pw2_date_brin_idx ON pw2 USING brin (date);
+''')
+
+if __name__ == '__main__':
+    main()