view pw2log.py @ 3:ec3c8b63d8e7

Collect frequency correctly...
author Daniel O'Connor <darius@dons.net.au>
date Tue, 09 Feb 2021 22:10:25 +1030
parents 393a879db9eb
children 8990981c60a0
line wrap: on
line source

#!/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
# https://github.com/jrester/tesla_powerwall
from tesla_powerwall import APIError, 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)
    for opt in ('ip', 'username', 'password'):
        if not cp.has_option('pw', opt):
            print('pw section missing %s parameter' % (opt,), 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)
        keepfh = None
    else:
        fh = RotatingFileHandler(logfile, maxBytes = 2000, backupCount = 10)
        fh.setFormatter(fmt)
        logger.addHandler(fh)
        keepfhs = [fh.stream.fileno()] # XXX: gross

    if pidfile == None:
        ctx = NullContextManager()
    else:
        try:
            #fh = open('/tmp/pw2errs.log', 'a')
            fh = None
            ctx = daemon.DaemonContext(pidfile = daemon.pidfile.PIDLockFile(pidfile), stdout = fh, stderr = fh, files_preserve = keepfhs)
        except Exception as e:
            logger.critical('Unable to get daemon context')

    try:
        with ctx:
            logger.critical('Starting')
            try:
                collectdata(cp.get('pw', 'ip'), cp.get('pw', 'username'), cp.get('pw', 'password'), cp.get('db', 'dsn'), cp.getint('db', 'logtime'))
            except Exception as e:
                 logger.critical('Unable to collect data: ' + str(e))
    except Exception as e:
        logger.critical('Unable to enter daemon context: ' + str(e))

def collectdata(pwip, username, password, dsn, logtime):
    dbh = psycopg2.connect(dsn)
    cur = dbh.cursor()
    pw = None

    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              |
            #
            if not pw:
                first = True
                pw = Powerwall(pwip)
                pw.login(username, password)
                pw.detect_and_pin_version()
            meters = pw.get_meters()
            grid_volts = meters.site.avarage_voltage
            grid_freq = meters.site.frequency
            grid_power = meters.site.instant_power
            load_power = meters.load.instant_power
            battery_power = meters.battery.instant_power
            solar_power = meters.solar.instant_power
            charge = pw.get_charge()
        except APIError as e:
            pw = None
            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_volts, grid_freq, grid_power, load_power, battery_power, charge, solar_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
        if first:
            logger.error('Logged OK')
            first = False

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