# HG changeset patch # User Daniel O'Connor # Date 1569413209 -34200 # Node ID 718b963b0dfadc24e2ced665cc17ed854d946be1 # Parent efe1954da8ca46b21ce59ee10643f7c2fd5ab922# Parent 00845d271007e889815247e05cd0ddf43d5aad50 merge diff -r 00845d271007 -r 718b963b0dfa README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Wed Sep 25 21:36:49 2019 +0930 @@ -0,0 +1,6 @@ +sudo cp vanlogger.service /lib/systemd/system/ +sudo cp 99-giant-ips.rules /lib/udev/rules.d/ +sudo systemctl daemon-reload +sudo udevadm control --reload +sudo systemctl start vanllogger.service + diff -r 00845d271007 -r 718b963b0dfa graph.py --- a/graph.py Wed Sep 25 21:35:40 2019 +0930 +++ b/graph.py Wed Sep 25 21:36:49 2019 +0930 @@ -23,8 +23,8 @@ self.annofn = annofn columns = [ - Column('main_voltage', 'Battery Voltage', 'eprolog', 'Vdc'), - Column('aux_voltage', 'Aux Voltage', 'eprolog', 'Vdc'), + Column('main_voltage', 'Battery Voltage', 'eprolog', 'Vdc', (10, 30)), + Column('aux_voltage', 'Aux Voltage', 'eprolog', 'Vdc', (10, 30)), Column('battery_curr', 'Battery Current', 'eprolog', 'A'), Column('amp_hours', 'Battery Amp Hours', 'eprolog', 'Ah'), Column('state_of_charge', 'State of Charge', 'eprolog', '%', (0, 100), annofn = lambda xdata, ydata: 'DoD: %.1f' % (100 - ydata.min())), @@ -66,27 +66,30 @@ if args.days is not None and args.days < 0: parser.error('days must be non-negative') - # Can specify.. - # Start and end - # Start and days or Start - # End and days or End - # Nothing - # Want to end up with a start & end + # If it's before 6am then plot yesterday + today = datetime.date.today() + if datetime.datetime.now().hour < 6: + today -= datetime.timedelta(days = 1) + selector = [args.start is not None, args.end is not None, args.days is not None] - if selector == [True, True, False]: + if selector == [True, True, False]: # Start and end pass - elif selector == [True, False, True] or selector == [True, False, False]: + elif selector == [True, False, True] or selector == [True, False, False]: # Start and days or start if args.days == None: args.days = 1 args.end = args.start + datetime.timedelta(days = args.days) - elif selector == [False, True, True] or selector == [False, True, False]: + elif selector == [False, True, True] or selector == [False, True, False]: # End and days or end if args.days == None: args.days = 1 args.start = args.end - datetime.timedelta(days = args.days) - elif selector == [False, False, True]: - end = datetime.date.today() - end = datetime.datetime(start.year, start.month, start.day) + elif selector == [False, False, True]: # Days + args.end = today + datetime.timedelta(days = 1) + args.end = datetime.datetime(args.end.year, args.end.month, args.end.day) args.start = args.end - datetime.timedelta(days = args.days) + elif selector == [False, False, False]: # Nothing + args.start = today + args.start = datetime.datetime(args.start.year, args.start.month, args.start.day) + args.end = args.start + datetime.timedelta(days = 1) else: parser.error('can\'t specify days, start and end simultaneously') @@ -116,10 +119,6 @@ args.start = args.start.replace(tzinfo = lt) if args.end.tzinfo == None: args.end = args.end.replace(tzinfo = lt) - startlt = args.start - endlt = args.end - args.start = args.start.astimezone(utc) - args.end = args.end.astimezone(utc) graph(args.graphfn, cur, cols, args.start, args.end, lt, utc) def graph(fname, cur, _cols, start, end, lt, utc): @@ -165,10 +164,10 @@ ary = numpy.array(cur.fetchall()) if ary.shape[0] == 0: print('No data for ' + c.rowname) - return + continue - # Create TZ naive from POSIX stamp, then convert to TZ aware UTC then adjust to local time - c.xdata = map(lambda f: datetime.datetime.fromtimestamp(f).replace(tzinfo = utc).astimezone(lt), ary[:,0]) + # Convert epoch stamp to datetime with UTC timezone + c.xdata = map(lambda f: datetime.datetime.utcfromtimestamp(f).replace(tzinfo = utc), ary[:,0]) c.ydata = ary[:,1] if c.conv != None: c.ydata = map(c.conv, c.ydata) @@ -190,6 +189,10 @@ colouridx += 1 ax.append(c) + if len(ax1lines) == 0 and len(ax2lines) == 0: + print('Nothing to plot') + return + # Load the right backend for display or save if fname == None: import matplotlib.pylab @@ -209,7 +212,7 @@ ax1.set_ylim(line.limits[0], line.limits[1]) if line.annotation != None: annotations.append(line.annotation) - ax1.legend(loc = 'upper left') + ax1.legend(loc = 'center left') if len(ax2lines) > 0: ax2 = ax1.twinx() @@ -222,21 +225,24 @@ if line.annotation != None: annotations.append(line.annotation) - ax2.legend(loc = 'upper right') + ax2.legend(loc = 'center right') if len(annotations) > 0: - ax1.text(0.02, 0.5, reduce(lambda a, b: a + '\n' + b, annotations), + ax1.text(0.02, 0.3, reduce(lambda a, b: a + '\n' + b, annotations), transform = ax1.transAxes, bbox = dict(facecolor = 'blue', alpha = 0.5), ha = 'left', va = 'top') ndays = int(max(1, round(((end - start).total_seconds()) / 86400))) + once = False for ax in fig.get_axes(): - if ndays > 1: - ax.set_title('%s to %s' % (start.strftime('%Y-%m-%d'), end.strftime('%Y-%m-%d'))) - else: - ax.set_title('%s' % (start.strftime('%Y-%m-%d'))) - ax.set_xlim([start, end]) - ax.format_xdata = lambda d: matplotlib.dates.num2date(d).strftime('%d %b %H:%M') + if not once: + once = True + if ndays > 1: + ax.set_title('%s to %s' % (start.astimezone(lt).strftime('%Y-%m-%d'), end.astimezone(lt).strftime('%Y-%m-%d'))) + else: + ax.set_title('%s' % (start.astimezone(lt).strftime('%Y-%m-%d'))) + ax.set_xlim([start.astimezone(utc), end.astimezone(utc)]) ax.xaxis.grid(True) + ax.xaxis.axis_date(lt) ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%d %b\n%H:%M')) ax.xaxis.set_major_locator(matplotlib.dates.HourLocator(interval = 2 * ndays)) ax.xaxis.set_minor_locator(matplotlib.dates.MinuteLocator(interval = 5 * ndays)) @@ -250,52 +256,7 @@ matplotlib.pyplot.show() else: canvas = matplotlib.backends.backend_agg.FigureCanvasAgg(fig) # Sets canvas in fig too - fig.savefig(startdt.strftime(fname)) - -def updatedb(cur, data): - mkdb(cur) - for d in data['reads']['data']: - ts = datetime.datetime.strptime(d['t_stamp'], '%Y-%m-%dT%H:%M:%SZ') - # Note we rename *energy* to *power* here to match what it actually means - vals = [ts, d['battery_charge'], d['battery_energy'], d['energy_consumed'], d['energy_expected'], d['energy_exported'], d['energy_generated'], - d['energy_imported'], d['estimated_savings'], d['pv_forecast'], d['pv_generation']['battery_energy'], - d['pv_generation']['grid_energy'], d['pv_generation']['site_energy'], d['site_consumption']['battery_energy'], - d['site_consumption']['grid_energy'], d['site_consumption']['pv_energy']] - skip = True - for v in vals[1:]: - if v != None: - skip = False - break - if skip: - print('Skipping empty record at ' + str(ts)) - continue - cur.execute('INSERT OR IGNORE INTO agl(t_stamp, battery_charge, battery_power, power_consumed, power_expected, power_exported, power_generated, power_imported, estimated_savings, pv_forecast, pv_gen_battery, pv_gen_grid, pv_gen_site, site_cons_battery, site_cons_grid, site_cons_pv) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', vals) - -def gettoken(username, password): - authblob = json.encoder.JSONEncoder().encode({'email' : username, 'password' : password}) - reply = requests.request('POST', loginurl, data = authblob, headers = {'Content-Type' : 'application/json'}) - if reply.status_code != 200: - return None - return json.decoder.JSONDecoder().decode(reply.content)['access_token'] - -def getdata(token, startdate, enddate): - #print('getting ' + startdate.strftime('%Y-%m-%d')) - reply = requests.request('GET', dataurl, params = { - 'startDate' : startdate.strftime('%Y-%m-%d'), - 'endDate' : enddate.strftime('%Y-%m-%d'), - 'granularity' : 'Minute', - 'metrics' : 'read', - 'units' : 'W', - }, headers = { 'Authorization' : 'Bearer ' + token}) - - if reply.status_code != 200: - return None - - return json.decoder.JSONDecoder().decode(reply.content) - -def logout(token): - reply = requests.request('GET', logouturl, headers = { 'Authorization' : 'Bearer ' + token}) - return reply.status_code == 200 + fig.savefig(start.astimezone(lt).strftime(fname)) if __name__ == '__main__': main() diff -r 00845d271007 -r 718b963b0dfa vanlogger.py --- a/vanlogger.py Wed Sep 25 21:35:40 2019 +0930 +++ b/vanlogger.py Wed Sep 25 21:36:49 2019 +0930 @@ -5,10 +5,9 @@ sys.path.append('/home/pi/logger') import datetime import epro.epro as epro -import json +import giant.giant as giant import serial import sqlite3 -import subprocess import sys def create(cur): @@ -44,21 +43,35 @@ ''') cur.execute(''' -CREATE TABLE IF NOT EXISTS victron( - tstamp INTEGER NOT NULL, - ACIn_L1_volts REAL NOT NULL, - ACIn_L1_freq REAL NOT NULL, - ACIn_L1_curent REAL NOT NULL, - ACIn_active BOOLEAN NOT NULL, - ACOut_L1_volts REAL NOT NULL, - ACOut_L1_freq REAL NOT NULL, - ACOut_L1_curent REAL NOT NULL, - Battery_Voltage REAL NOT NULL, - Battery_Current REAL NOT NULL +CREATE TABLE IF NOT EXISTS giantlog( + tstamp INTEGER NOT NULL, + ac_act_power REAL NOT NULL, + ac_app_power REAL NOT NULL, + ac_frequency REAL NOT NULL, + ac_volts REAL NOT NULL, + batt_chr_curr REAL NOT NULL, + batt_dis_curr REAL NOT NULL, + battery_cap REAL NOT NULL, + battery_volts REAL NOT NULL, + batt_volt_ofs REAL NOT NULL, + bus_voltage REAL NOT NULL, + grid_frequency REAL NOT NULL, + grid_volts REAL NOT NULL, + hs_temperature REAL NOT NULL, + load_pct REAL NOT NULL, + pv1_chrg_pow REAL NOT NULL, + pv1_current REAL NOT NULL, + pv1_volts REAL NOT NULL, + scc1_volts REAL NOT NULL, + scc1_charging BOOLEAN NOT NULL, + switch BOOLEAN NOT NULL, + float_charge BOOLEAN NOT NULL, + ac_charging BOOLEAN NOT NULL, + sbu_prio BOOLEAN NOT NULL, + b_volt_steady BOOLEAN NOT NULL, + charging BOOLEAN NOT NULL ); ''') - - def log_epro(p, cur): # Check we have all the packets we need in the queue @@ -122,23 +135,61 @@ row['tstamp'] = int(datetime.datetime.now().strftime('%s')) cur.execute('INSERT INTO eprolog VALUES (:tstamp, :main_voltage, :aux_voltage, :battery_curr, :amp_hours, :state_of_charge, :time_remaining, :battery_temp, :auto_sync_volts, :auto_sync_curr, :e501, :alarm_test, :light, :display_test, :temp_sensor, :aux_hv, :aux_lv, :installer_lock, :main_hv, :main_lv, :low_battery, :battery_flat, :battery_full, :battery_charged, :no_sync, :monitor_reset)', row) +def log_giant(gstat, cur): + row = {} + row['ac_act_power'] = gstat['ACActPower'] + row['ac_app_power'] = gstat['ACAppPower'] + row['ac_frequency'] = gstat['ACFreq'] + row['ac_volts'] = gstat['ACVolts'] + row['batt_chr_curr'] = gstat['BattChrCurr'] + row['batt_dis_curr'] = gstat['BattDisCurr'] + row['battery_cap'] = gstat['BattCap'] + row['battery_volts'] = gstat['BattVolts'] + row['batt_volt_ofs'] = gstat['BattVoltOfs'] + row['bus_voltage'] = gstat['BusVolts'] + row['grid_frequency'] = gstat['GridFreq'] + row['grid_volts'] = gstat['GridVolts'] + row['hs_temperature'] = gstat['HSTemp'] + row['load_pct'] = gstat['LoadPct'] + row['pv1_chrg_pow'] = gstat['PVChrgPow1'] + row['pv1_current'] = gstat['PVCurr1'] + row['pv1_volts'] = gstat['PVVolt1'] + row['scc1_volts'] = gstat['SCC1Volt'] + row['scc1_charging'] = gstat['Status']['SCC1Charging'] + row['switch'] = gstat['Status']['Switch'] + row['float_charge'] = gstat['Status']['FloatCharge'] + if gstat['Status']['ChargeType'] == 'Both' or gstat['Status']['ChargeType'] == 'AC': + row['ac_charging'] = True + else: + row['ac_charging'] = False + row['sbu_prio'] = gstat['Status']['SBUPrio'] + row['b_volt_steady'] = gstat['Status']['BattVoltSteady'] + row['charging'] = gstat['Status']['Charging'] + + row['tstamp'] = int(datetime.datetime.now().strftime('%s')) + cur.execute('INSERT INTO giantlog VALUES(:tstamp, :ac_act_power, :ac_app_power, :ac_frequency, :ac_volts, :batt_chr_curr, :batt_dis_curr, :battery_cap, :battery_volts, :batt_volt_ofs, :bus_voltage, :grid_frequency, :grid_volts, :hs_temperature, :load_pct, :pv1_chrg_pow, :pv1_current, :pv1_volts, :scc1_volts, :scc1_charging, :switch, :float_charge, :ac_charging, :sbu_prio, :b_volt_steady, :charging)', row) + def main(): print 'Started' dbh = sqlite3.connect('/home/pi/vanlogger/log.db') cur = dbh.cursor() create(cur) - #s = serial.Serial('/dev/ttyS0', 2400, parity='E') - s = serial.Serial('/dev/ttyS0', 2400) + s = serial.Serial('/dev/ttyS0', 2400, parity='E') s.timeout = 0.2 p = epro.Processor() + ips = giant.GiantIPS() then = None lasteprolog = datetime.datetime.now() + lastgiantlog = datetime.datetime.now() while True: if datetime.datetime.now() - lasteprolog > datetime.timedelta(hours = 1): print('Stale ePro data') sys.exit(1) + if datetime.datetime.now() - lastgiantlog > datetime.timedelta(hours = 1): + print('Stale Giant data') + sys.exit(1) dolog = False if then == None or datetime.datetime.now() - then > datetime.timedelta(seconds = 60): dolog = True @@ -149,5 +200,16 @@ log_epro(p, cur) dbh.commit() + gstat = None + try: + gstat = ips.getStatus() + except: + pass + if gstat != None and dolog: + lastgiantlog = datetime.datetime.now() + log_giant(gstat, cur) + dbh.commit() + #print(gstat) + if __name__ == '__main__': main() diff -r 00845d271007 -r 718b963b0dfa vanlogger.service