Last active
April 6, 2022 02:56
-
-
Save tvwerkhoven/4369140 to your computer and use it in GitHub Desktop.
Log Macbook battery status with system_profiler(8) and ioreg(8), see http://home.strw.leidenuniv.nl/~werkhoven/etc/battlog.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# | |
# About | |
# ===== | |
# | |
# Log battery info from command-line with SYSTEM_PROFILER(8) and IOREG(8) to a | |
# user-configurable directory. | |
# | |
# Run with -h to see usage options. | |
# | |
# Documentation available at http://home.strw.leidenuniv.nl/~werkhoven/etc/battlog.html . This script is known to work on OS X 10.7 & 10.9. | |
# | |
# Data logging | |
# ============ | |
# | |
# When an output directory is given with -o, the following properties are | |
# logged: | |
# | |
# 1. From system_profiler(8): | |
# ExternalConnected, CellVoltage, MaxCapacity, Voltage, CurrentCapacity, BatteryInstalled, CycleCount, DesignCapacity, Temperature, FullyCharged, InstantAmperage, Amperage, DesignCycleCount9C | |
# | |
# 2. From ioreg(8): | |
# FirmwareVersion, HardwareRevision, CellRevision, ChargeInformation, ChargeRemaining(mAh), FullyCharged, Charging, FullChargeCapacity(mAh), CycleCount, Condition, Amperage(mA), Voltage(mV), ACChargerInformation, Connected, Wattage(W), Revision, Charging | |
# | |
# Data is stored to two files: | |
# | |
# 1. YYYYMM_MAC_battery-meta_log.txt | |
# Contains full output from system_profiler(8) and ioreg(8) once a month, to backtrace what data is being logged | |
# | |
# 2. YYYYMM_MAC_battery-meta_log.txt | |
# Contains concise output from same tools to use less disk space. | |
# | |
# Using in crontab | |
# ================ | |
# | |
# Example crontab entry: | |
# */15 * * * * $HOME/bin/battery_logger.sh -o $HOME/Documents/batt_log | |
# | |
# This crontab entry will also list uptime every minute (for load checking) | |
# */1 * * * * /bin/echo $(/bin/date +\%Y\%m\%d-\%H\%M\%S) $(/usr/bin/uptime) >> $HOME/Documents/mac_health/00_poweron-log.txt | |
# | |
# | |
# Copyright (c) 2012 Tim van Werkhoven <[email protected]> | |
# This file is licensed under the Creative Commons Attribution-Share Alike | |
# license versions 3.0 or higher, see | |
# http://creativecommons.org/licenses/by-sa/3.0/ | |
USAGE_STR="Usage: $0 -o [DIR] -h -v" | |
while getopts "o:hv" opt; do | |
case $opt in | |
v) | |
DEBUG=1 | |
;; | |
o) | |
OUTDIR=$OPTARG | |
;; | |
h) | |
cat <<USAGE_INFO | |
${USAGE_STR} | |
Log battery info to DIR using system_profiler(8) and ioreg(8) | |
Example: | |
$0 -o ./ (logs to current directory) | |
$0 (runs in debug mode) | |
Run as a cron-job to log battery health over time. | |
$0 accepts the options: | |
-o DIR directory to store data to | |
-v show debug information | |
-h show this help text | |
USAGE_INFO | |
exit | |
;; | |
\?) | |
echo ${USAGE_STR} | |
exit | |
;; | |
esac | |
done | |
# Check if we run in debug mode | |
test ${DEBUG} && echo "Running in debug mode..." | |
# Version of output format | |
APIVER=0.91 | |
# Tool paths | |
SYSPROF=/usr/sbin/system_profiler | |
UPTIME=/usr/bin/uptime | |
UNAME=/usr/bin/uname | |
IOREG=/usr/sbin/ioreg | |
GREP=/usr/bin/grep | |
DATE=/bin/date | |
ECHO=/bin/echo | |
BZIP=/usr/bin/bzip2 | |
SED=/usr/bin/sed | |
CUT=/usr/bin/cut | |
TR=/usr/bin/tr | |
# ============================================================================ | |
# FILE MANAGEMENT | |
# ============================================================================ | |
# Get system MAC address for unique filenames | |
SYSID=$(/sbin/ifconfig en0 | ${GREP} ether | ${TR} -d ':' | ${CUT} -d ' ' -f 2) | |
test ${DEBUG} && echo "Got SYSID: ${SYSID}" | |
# Files to use | |
METAFILE=${OUTDIR}/$(${DATE} +%Y%m)_${SYSID}_battery-meta_log.txt | |
LOGFILE=${OUTDIR}/$(${DATE} +%Y%m)_${SYSID}_battery_log.txt | |
# This is the previous logfile | |
LASTLOGFILE=${OUTDIR}/$(${DATE} -v-1m +%Y%m)_${SYSID}_battery_log.txt | |
test ${DEBUG} && echo "Got METAFILE: ${METAFILE}, LOGFILE: ${LOGFILE}, LASTLOGFILE: ${LASTLOGFILE}" | |
# Make output directory | |
test ${OUTDIR} && mkdir -p ${OUTDIR} | |
# Compress file from last month, if it exists. This will append a zip-suffix to the file so this test will not trigger again. If output file already exists, bzip will complain. Silence by redirecting errors to /dev/null | |
test -f ${LASTLOGFILE} && ${BZIP} -q ${LASTLOGFILE} > /dev/null 2>&1 | |
# Store system information to file, if it does not exist (i.e. once a month) | |
test ! -f ${METAFILE} && test ${OUTDIR} && ${UNAME} -a >> ${METAFILE} && ${SYSPROF} SPSoftwareDataType SPPowerDataType >> ${METAFILE} && ${IOREG} -lrSc AppleSmartBattery >> ${METAFILE} | |
test ${DEBUG} && ${UNAME} -a && ${SYSPROF} SPSoftwareDataType SPPowerDataType && ${IOREG} -rSk Temperature | |
# ============================================================================ | |
# IOREG(8) PARSING | |
# ============================================================================ | |
# We want these fields from ioreg: | |
# ExternalConnected, CellVoltage, MaxCapacity, Voltage, CurrentCapacity, BatteryInstalled, CycleCount, DesignCapacity, Temperature, FullyCharged, InstantAmperage, Amperage, IsCharging, DesignCycleCount9C | |
IOREG_GREPSTR="\(Cycle\|ExternalConnected\|Voltage\|Installed\|Capacity\|Voltage\|Temperature\|Charged\|Amperage\)" | |
IOREG_GREPIGNSTR="LegacyBatteryInfo" | |
# Get all values with property names, sort for reproducible orders | |
IOREG_HDR=$(${IOREG} -rSk Temperature | ${GREP} ${IOREG_GREPSTR} | ${GREP} -v ${IOREG_GREPIGNSTR} | sort | ${TR} "\n=" ",:" | ${TR} -d " \"") | |
# Remove property names and only take data | |
IOREG_DATA=$(${ECHO} ${IOREG_HDR} | ${SED} -E 's/[a-zA-Z9]+://g' | ${TR} -d "()") | |
test ${DEBUG} && echo "Got IOREG_HDR: ${IOREG_HDR}" | |
test ${DEBUG} && echo "Got IOREG_DATA: ${IOREG_DATA}" | |
# ============================================================================ | |
# SYSTEM_PROFILER(8) PARSING | |
# ============================================================================ | |
# We want these fields from system_profiler: | |
# Firmware Version, Hardware Revision, Cell Revision, Charge Remaining, Fully Charged, Charging, Full Charge Capacity, Cycle Count, Condition, Amperage, Voltage, Connected, Wattage, Revision, Family | |
SYSPROF_GREPSTR="\(Version\|Revision\|Cell\|Charg\|Cycle\|Condition\|Amperage\|Voltage\|Connected\|Wattage\|Family|\)" | |
# Get all values with property names, sort for reproducible orders | |
SYSPROF_HDR=$(${SYSPROF} SPPowerDataType | ${GREP} ${SYSPROF_GREPSTR} | sort | ${TR} "\n" ", " | ${TR} -d " ") | |
# Remove property names and only take data | |
SYSPROF_DATA=$(${ECHO} ${SYSPROF_HDR} | ${SED} -E 's/[a-zA-Z9\(\)]+://g') | |
test ${DEBUG} && echo "Got SYSPROF_HDR: ${SYSPROF_HDR}" | |
test ${DEBUG} && echo "Got SYSPROF_DATA: ${SYSPROF_DATA}" | |
# ============================================================================ | |
# STORE DATA | |
# ============================================================================ | |
# Get date as ISO 8601 + timezone in HHMM offset from UTC | |
DATEINFO=$(${DATE} +%Y-%m-%dT%H:%M:%S%z) | |
# Get uptime and load info, replace , with ; as we use commas for field separators | |
LOADINFO=$(${UPTIME} | ${TR} "," ";") | |
test ${DEBUG} && echo "Got APIVER: ${APIVER}, DATEINFO: ${DATEINFO}, LOADINFO: ${LOADINFO}" | |
test ${DEBUG} && echo ${DATEINFO}, ${APIVER}, ${LOADINFO}, "IOREG", ${IOREG_DATA}, "SYSPROF", ${SYSPROF_DATA} | |
# Add header to logfile if it does not exist (i.e. once a month) | |
test ! -f ${LOGFILE} && test ${OUTDIR} && ${ECHO} "Date, APIVER, uptime, " ${IOREG_HDR} ${SYSPROF_HDR} >> ${LOGFILE} | |
# Add data to logfile | |
test ${OUTDIR} && ${ECHO} ${DATEINFO}, ${APIVER}, ${LOADINFO}, "IOREG", ${IOREG_DATA}, "SYSPROF", ${SYSPROF_DATA} >> ${LOGFILE} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python2.7 | |
# -*- coding: utf-8 -*- | |
""" | |
@file battery_plotter.py | |
@brief Plot data logged by battery_logger.sh | |
@author Tim van Werkhoven ([email protected]) | |
@date 20130111 | |
Parse logged battery health data from ioreg(8) and system_profiler(8) on OS | |
X, visualize into graphs and figures. | |
To plot, run: | |
./battery_plotter.py mac_health/20*battery_log.txt* | |
Compatible with OS X 10.7, 10.9, numpy 1.8.0 and python 2.7.6 | |
Created by Tim van Werkhoven on 20130111 | |
Copyright (c) 2013 Tim van Werkhoven ([email protected]) | |
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 | |
Unported License. To view a copy of this license, visit | |
http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative | |
Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, | |
USA. | |
""" | |
# Import libs | |
import numpy as np | |
import argparse | |
import sys, os, bz2 | |
from os.path import join as pjoin | |
import dateutil.parser | |
import datetime | |
import re | |
from IPython import embed as shell | |
from time import sleep | |
import libtim as tim | |
import libtim.util | |
import time | |
import pytz | |
import pylab as plt | |
import matplotlib as mpl | |
import matplotlib.dates | |
import matplotlib.patches as mpatches | |
from matplotlib.collections import PatchCollection | |
# Define some contants | |
AUTHOR = "Tim van Werkhoven ([email protected])" | |
DATE = "20130111" | |
# Start functions | |
def main(): | |
# Parse & check options | |
(parser, args) = parsopts() | |
# Load data | |
batt_d = load_batt_files(args.logfiles, args.metafiles) | |
if (args.listquants): | |
print "Plottable quantities: ", batt_d.dtype.names | |
return | |
for plotq in args.plotquants: | |
# Plot punch card data | |
if plotq == 'punchdate': | |
plot_punchcard(batt_d['date'], 'date', punchrad=args.punchrad, outdir=args.outdir) | |
elif (plotq[:5] == 'punch') and (plotq[5:] in batt_d.dtype.names): | |
plot_punchcard(batt_d['date'], plotq[5:], punchrad=args.punchrad, quantity=batt_d[plotq[5:]], outdir=args.outdir) | |
if (plotq[:4] == 'corr'): | |
xq, yq = plotq[4:].split("_") | |
if (xq in batt_d.dtype.names) and (yq in batt_d.dtype.names): | |
plot_correlation(batt_d[xq], xq, batt_d[yq], yq, outdir=args.outdir) | |
### END | |
def parsopts(): | |
### Parse program options and return results | |
parser = argparse.ArgumentParser(description='Plot data logged by battery_logger.sh.', epilog='Comments & bugreports to %s' % (AUTHOR), prog='battery_plotter') | |
parser.add_argument('logfiles', metavar='LOGF', nargs='+', | |
help='log files to plot') | |
parser.add_argument('--metafiles', metavar='METAF', default=[], | |
nargs='+', help='metadata files (default: None)') | |
parser.add_argument('--listquants', action="store_true", default=False, | |
help='list plottable quantities (False)') | |
parser.add_argument('--plotquants', metavar='Q', nargs='+', default=['punchconnected', 'punchdate', 'corrload1_load15', 'corrdate_uptime', 'corrtemp_load1', 'corrtemp_load5', 'corrtemp_load15', 'corrvolt_temp', 'corrtemp_volt', 'corrdate_temp', 'corrdate_maxcap', 'corrdate_ncycles', 'corrncycles_maxcap', 'corramperage_load1', 'corramperage_load5', 'corramperage_load15', 'corrdate_amperage', 'corrtime_amperage', 'corrdate_power', 'corrtime_power', 'corrtime_connected', 'corrtime_charged', 'corrdate_load1', 'corrdate_load5', 'corrdate_load15', 'corrtime_load1', 'corrtime_load5', 'corrtime_load15'], | |
help='quantities to plot, [punch<qty>, corr<qty1>_<qty2>]') | |
g1 = parser.add_argument_group("Plotting options") | |
g1.add_argument('--punchrad', dest='punchrad', default=2.0, type=float, | |
help='scaling factor for punch radius') | |
g4 = parser.add_argument_group("Miscellaneous options") | |
g4.add_argument('-v', dest='debug', action='append_const', const=1, | |
help='increase verbosity') | |
g4.add_argument('-q', dest='debug', action='append_const', const=-1, | |
help='decrease verbosity') | |
args = parser.parse_args() | |
# Check & fix some options | |
checkopts(parser, args) | |
# Return results | |
return (parser, args) | |
def checkopts(parser, args): | |
args.verb = 0 | |
if (args.debug): | |
args.verb = sum(args.debug) | |
if (args.verb > 1): | |
# Print some debug output | |
print "Running program %s" % (sys.argv[0]) | |
print args | |
print sys.argv | |
# If we have meta files, they should be the same amount as log files | |
if (len(args.metafiles) and len(args.metafiles) != len(args.logfiles)): | |
print "Error: need same log file (%d) as meta files (%d)!" % (len(args.logfiles), len(args.metafiles)) | |
parser.print_usage() | |
exit(-1) | |
# Make output directory | |
dateid = time.strftime('%Y%m%d', time.gmtime()) | |
args.outdir = os.path.relpath(dateid+"_battery_plotter") | |
try: | |
os.makedirs(args.outdir) | |
except OSError: | |
pass | |
def load_batt_files(logfiles, metafiles): | |
""" | |
Load log files and meta log files. | |
Log files have concise battery data logged regularly through `cron`, | |
meta log files have full output of ioreg(8) and system_profiler(8) to | |
verify program output. | |
@param [in] logfiles List of paths to log files (might be compressed) | |
@param [in] metafiles List of paths to meta log files | |
@return Parse data as np.recarray with colums date, time, uptime, load1, load5, load15, connected, maxcap, volt, currcap, hasbatt, ncycles, temp, charged, amperage, charging, power | |
""" | |
logdat_all = [] | |
# Loop over all pairs of log- and meta-log files | |
for lfile in sorted(logfiles): | |
# Load file manually, can be bz2 or regular file | |
try: | |
with bz2.BZ2File(lfile, "r") as fd: | |
logdat = [l.strip().split(",") for l in fd.xreadlines()] | |
except IOError: | |
with open(lfile, "r") as fd: | |
logdat = [l.strip().split(",") for l in fd.xreadlines()] | |
# Process columns of interesting data | |
logdat_fmt = [parse_log_row(row) for row in logdat[1:]] | |
logdat_all.extend(logdat_fmt) | |
# Format into recarray | |
logdat_rec = np.rec.fromrecords(logdat_all, names='date, time, uptime, load1, load5, load15, connected, maxcap, volt, currcap, hasbatt, ncycles, temp, charged, amperage, charging, power') | |
return logdat_rec | |
def parse_log_row(logrow): | |
""" | |
Parse a row of a battery log file into usable quantities. | |
Row should be something like: | |
array(['20130101-133000', ' 13:30 up 6 mins', ' 2 users', | |
' load averages: 0.42 0.41 0.22', ' ExternalConnectedYes', | |
'CellVoltage(4166', '4167', '4166', '0)', 'MaxCapacity4822', | |
'Voltage12499', 'CurrentCapacity4795', 'BatteryInstalledYes', | |
'CycleCount150', 'DesignCapacity5770', 'Temperature2923', | |
'FullyChargedYes', 'InstantAmperage0', 'Amperage0', | |
'DesignCycleCount9C1000', ' 201', '2', '164', '', '4795', 'Yes', | |
'No', '4822', '150', 'Normal', '0', '12499', '', 'Yes', '60', | |
'0x0000'], | |
where the columns denote (approximately): | |
array(['Date', ' uptime', ' ExternalConnectedYes', 'CellVoltage(4166', | |
'4167', '4166', '0)', 'MaxCapacity4822', 'Voltage12499', | |
'CurrentCapacity4795', 'BatteryInstalledYes', 'CycleCount150', | |
'DesignCapacity5770', 'Temperature2923', 'FullyChargedYes', | |
'InstantAmperage0', 'Amperage0', 'DesignCycleCount9C1000', | |
' FirmwareVersion:201', 'HardwareRevision:2', 'CellRevision:164', | |
'ChargeInformation:', 'ChargeRemaining(mAh):4795', | |
'FullyCharged:Yes', 'Charging:No', 'FullChargeCapacity(mAh):4822', | |
'CycleCount:150', 'Condition:Normal', 'Amperage(mA):0', | |
'Voltage(mV):12499', 'ACChargerInformation:', 'Connected:Yes', | |
'Wattage(W):60', 'Revision:0x0000', 'Charging:No', ''], | |
dtype='|S35') | |
@param [in] logrow A row from a log file | |
@return [date, time, uptime, load1, load5, load15, connected, maxcap, volt, currcap, hasbatt, ncycles, temp, charged, amperage, charging, power] | |
""" | |
# Check version, if we can parse the second column to a float, we have a version. If not, this is an old format. | |
try: | |
apiver = float(logrow[1]) | |
except: | |
apiver = 0.1 | |
# Date is always the first column | |
rowdate = dateutil.parser.parse(logrow[0]) | |
# Hack: if no timezone info, set to GMT+1 | |
if (not rowdate.tzinfo): | |
tz = pytz.timezone('Europe/Amsterdam') | |
rowdate = tz.normalize(tz.localize(rowdate)) | |
# Also only store time as hours | |
rowtime = rowdate.second/3600. + rowdate.minute/60. + rowdate.hour | |
# Find the column with 'load averages' (this is variable for APIVER<0.9) | |
for rowoff, el in enumerate(logrow): | |
if 'load averages' in el: break | |
else: | |
raise ValueError("Expected to find 'load averages' somewhere") | |
for upoff, el in enumerate(logrow): | |
if ' up ' in el: break | |
else: | |
raise ValueError("Expected to find ' up ' somewhere") | |
# Replace 'Yes', 'No' with 1,0 in each column, then remove text | |
logrow2 = [el.replace("Yes","1").replace("No","0") for el in logrow] | |
logrowp = logrow2[:rowoff+1] + \ | |
[re.sub("^[ a-zA-Z]+", '', el) for el in logrow2[rowoff+1:]] | |
# Parse load string | |
# rowloadstr = logrowp[rowoff].split("load averages")[-1] | |
# rowload_l = [float(l) for l in rowloadstr.split(" ")[-3:]] | |
if (apiver <= 0.9): | |
upstr = ", ".join(logrowp[upoff:rowoff+1]).replace(";",",") | |
parsed = tim.util.parse_uptime(upstr) | |
ltime, rowuptime, nuser, rowload_l = parsed | |
# Parse ioreg output (these fields are sometimes swapped?) | |
rowconn1 = int(logrowp[rowoff+1]) | |
rowmaxcap1 = int(logrowp[rowoff+6]) | |
rowvolt1 = int(logrowp[rowoff+7]) | |
rowcurrcap1 = int(logrowp[rowoff+8]) | |
rowhasbatt = int(logrowp[rowoff+9]) | |
rowcyc1 = int(logrowp[rowoff+10]) | |
rowtemp = float(logrowp[rowoff+12])/100. | |
rowcharged1 = int(logrowp[rowoff+13]) | |
rowampg1 = int(logrowp[rowoff+15]) | |
# Parse system_profiler output | |
rowcurrcap = int(logrowp[rowoff+21]) | |
rowcharged = int(logrowp[rowoff+22]) | |
rowcharging = int(logrowp[rowoff+23]) | |
rowmaxcap = int(logrowp[rowoff+24]) | |
rowcyc = int(logrowp[rowoff+25]) | |
rowampg = int(logrowp[rowoff+27]) | |
rowvolt = int(logrowp[rowoff+28]) | |
rowconn = int(logrowp[rowoff+30]) | |
# Power is only available if connected | |
rowpower = 0 | |
if (rowconn): | |
# Hack: sometimes we didn't store power data in which case it's '0x0000' | |
if logrowp[rowoff+31] != '0x0000': | |
rowpower = int(logrowp[rowoff+31]) | |
# Hack: somewhere in the log maxcap and voltage are swapped. Check with ioreg(8) | |
# output to confirm | |
if rowmaxcap == rowvolt1 and rowvolt == rowmaxcap1: | |
rowvolt1, rowmaxcap1 = rowmaxcap1, rowvolt1 | |
# Check that data match between ioreg(8) and system_profiler(8), allow | |
# for a small error due to possible lag | |
if (rowconn1 != rowconn | |
or abs(rowmaxcap1 - rowmaxcap) > 5 | |
or abs(rowvolt1 - rowvolt) > 5 | |
or abs(rowcurrcap1 - rowcurrcap) > 5 | |
or abs(rowcyc1 - rowcyc) > 1 | |
or rowcharged1 != rowcharged | |
or abs(rowampg1 - rowampg) > 5): | |
pass | |
#print rowdate, "conn:", rowconn1, rowconn, "maxcap:", rowmaxcap1, rowmaxcap, "volt:", rowvolt1, rowvolt, "currcap:", rowcurrcap1, rowcurrcap, "cyc:", rowcyc1, rowcyc, "charged:", rowcharged1, rowcharged, "ampg:", rowampg1, rowampg | |
elif (apiver == 0.91): | |
parsed = tim.util.parse_uptime(logrow[2].replace(";",",")) | |
ltime, rowuptime, nuser, rowload_l = parsed | |
# Find ioreg offset | |
for ioregoff, el in enumerate(logrow): | |
if el.strip() == 'IOREG': break | |
for syspoff, el in enumerate(logrow): | |
if el.strip() == 'SYSPROF': break | |
rowhasbatt = int(logrowp[ioregoff+2]) | |
rowcurrcap1 = int(logrowp[ioregoff+7]) | |
rowcyc1 = int(logrowp[ioregoff+8]) | |
rowdescap = int(logrowp[ioregoff+9]) | |
rowconn1 = int(logrowp[ioregoff+11]) | |
rowcharged1 = int(logrowp[ioregoff+12]) | |
rowmaxcap1 = int(logrowp[ioregoff+14]) | |
rowtemp = float(logrowp[ioregoff+15])/100. | |
rowvolt1 = int(logrowp[ioregoff+16]) | |
rowcurrcap = int(logrowp[syspoff+2]) | |
rowcharging = int(logrowp[syspoff+3]) | |
rowcyc = int(logrowp[syspoff+5]) | |
rowmaxcap = int(logrowp[syspoff+7]) | |
rowcharged = int(logrowp[syspoff+8]) | |
rowampg = int(logrowp[syspoff+10]) | |
rowcharging2 = int(logrowp[syspoff+12]) | |
rowconn = int(logrowp[syspoff+13]) | |
# Power is only available if connected, volt is in a different column | |
rowpower = 0 | |
if (rowconn): | |
rowpower = int(logrowp[syspoff+16] or 0) | |
rowvolt = int(logrowp[syspoff+15] or 0) | |
else: | |
rowvolt = int(logrowp[syspoff+14]) | |
# Check that data match, allow for a small error due to possible lag | |
if (rowconn1 != rowconn | |
or abs(rowmaxcap1 - rowmaxcap) > 5 | |
or abs(rowvolt1 - rowvolt) > 5 | |
or abs(rowcurrcap1 - rowcurrcap) > 5 | |
or abs(rowcyc1 - rowcyc) > 1 | |
or rowcharged1 != rowcharged): | |
pass | |
#print rowdate, "conn:", rowconn1, rowconn, "maxcap:", rowmaxcap1, rowmaxcap, "volt:", rowvolt1, rowvolt, "currcap:", rowcurrcap1, rowcurrcap, "cyc:", rowcyc1, rowcyc, "charged:", rowcharged1, rowcharged | |
# Format data and return | |
return [rowdate, rowtime, rowuptime] + list(rowload_l) + [rowconn, rowmaxcap, rowvolt, rowcurrcap, rowhasbatt, rowcyc, rowtemp, rowcharged, rowampg, rowcharging, rowpower] | |
# Plot functions | |
def plot_correlation(xdata, xlab, ydata, ylab, outdir='./'): | |
""" | |
Plot cross-correlation of two quantities. | |
@param [in] xdata Data for x-axis | |
@param [in] xlab X-axis label | |
@param [in] ydata Data for y-axis | |
@param [in] ylab Y-axis label | |
@param [in] plotname Title for plot and filename | |
""" | |
# Plot data | |
fig = plt.figure(300); fig.clf(); | |
ax = fig.add_subplot(111) | |
ax.set_title("Correlation plot") | |
ax.set_xlabel(xlab) | |
ax.set_ylabel(ylab) | |
ax.grid(True) | |
# Check if axis are time or dates | |
xdate = type(xdata[0]) == datetime.datetime | |
if (xdate): xdata = mpl.dates.date2num(xdata) | |
ydate = type(ydata[0]) == datetime.datetime | |
if (ydate): ydata = mpl.dates.date2num(ydata) | |
ax.plot_date(xdata, ydata, fmt='.', xdate=xdate, ydate=ydate) | |
if (xdate): fig.autofmt_xdate() | |
if (ydate): fig.autofmt_ydate() | |
plt.savefig(pjoin(outdir, "batt_log_corr_%s_vs_%s.pdf" % (xlab, ylab))) | |
def plot_punchcard(timedata, plotname, punchrad=2.0, quantity=None, outdir='./'): | |
""" | |
Plot a github-like punchcard, using matplotlib. | |
@param [in] timedata Timestamps to use | |
@param [in] plotname Title for plot and filename | |
@param [in] punchrad Scaling factor for punch radius | |
@param [in] quantity Quantity to plot. If **None**, use timedata density itself | |
""" | |
# Check if we have data | |
if (not len(timedata)): | |
raise ValueError("No time data given. Log files empty?") | |
# Init array for punchcard | |
# punchdat = np.zeros((7, 24), dtype=float) | |
# for thisday in range(1,8): | |
# dmask = np.r_[ [d.isoweekday() == thisday for d in timedata] ] | |
# thisdates = [d for d in timedata if d.isoweekday() == thisday] | |
# for thishour in range(24): | |
# tmask = np.r_[ [d.hour == thishour for d in thisdates] ] | |
# # If no quantity is given, use time | |
# if (quantity == None): | |
# punchdat[thisday-1, thishour] = \ | |
# len([1 for d in thisdates if d.hour == thishour]) | |
# else: | |
# punchdat[thisday-1, thishour] = sum(quantity[dmask][tmask]) | |
# Normalize | |
# punchdatn = punchdat/punchdat.sum() | |
# Plot data | |
# w, h = 24./2., 7./2. | |
# fig = plt.figure(290, figsize=(w+.5+.1, h+.3+.3)); fig.clf(); | |
# fig.subplots_adjust(left=.5/w, right=1-.1/w, top=1-.3/h, bottom=.3/h) | |
#ax = plt.axes([0,0,1,1]) | |
fig = plt.figure(200, figsize=(7.09, 2.13)); fig.clf() | |
ax = fig.add_subplot(111) | |
ax.set_title("Punch card for '%s'" % plotname) | |
ax.set_xlabel("") | |
ax.set_ylabel("") | |
ax.set_xlim(-1, 24) | |
ax.set_ylim(0, 8) | |
ax.grid(False) | |
# From <http://matplotlib.org/examples/api/artist_demo.html> | |
patches = [] | |
for thisday in range(1,8): | |
# Make binary mask per day of the week to select data subsets for each day | |
dmask = np.r_[ [d.isoweekday() == thisday for d in timedata] ] | |
# Select date subsets for current day | |
thisdates = [d for d in timedata if d.isoweekday() == thisday] | |
# Check if we have data for this day, else skip | |
if not len(thisdates): | |
print "No data for day %d" % (thisday) | |
continue | |
for thishour in range(24): | |
# Make binary mask for each hour of day | |
tmask = np.r_[ [d.hour == thishour for d in thisdates] ] | |
# Check if we have data for this day, else skip | |
if not len(tmask): | |
print "No data for hour %d" % (thishour) | |
continue | |
# If no quantity is given, use time | |
if (quantity == None): | |
thisq = len([1 for d in thisdates if d.hour == thishour]) | |
thisqn = thisq*1.0/len(timedata) | |
else: | |
thisqn = 4.0*np.mean(quantity[dmask][tmask])/np.sum(quantity) | |
circ = plt.Circle((thishour, thisday), radius=punchrad*thisqn**.5) | |
patches.append(circ) | |
collection = PatchCollection(patches, cmap=mpl.cm.jet, alpha=0.6) | |
ax.add_collection(collection) | |
plt.yticks( range(1,8), ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], rotation=0) | |
plt.xticks( range(0, 24, 2) ) | |
plt.show() | |
plt.savefig(pjoin(outdir, "batt_log_punchcard_%s.pdf" % (plotname))) | |
# Run main program, must be at end or the rest of the file will not be read | |
if __name__ == "__main__": | |
sys.exit(main()) | |
# EOF |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
dmask
isand
tmask
is[]
.