Skip to content

Instantly share code, notes, and snippets.

@tvwerkhoven
Last active April 6, 2022 02:56
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tvwerkhoven/4369140 to your computer and use it in GitHub Desktop.
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
#!/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 <timvanwerkhoven@gmail.com>
# 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}
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
"""
@file battery_plotter.py
@brief Plot data logged by battery_logger.sh
@author Tim van Werkhoven (t.i.m.vanwerkhoven@gmail.com)
@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 (t.i.m.vanwerkhoven@xs4all.nl)
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 (t.i.m.vanwerkhoven@gmail.com)"
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
@parkr
Copy link

parkr commented Aug 2, 2014

Running into a problem with this:

~/Documents/battery$ ./battery_plotter.py 201408_6003088e3bcc_battery_log.txt -v
Traceback (most recent call last):
  File "./battery_plotter.py", line 443, in <module>
    sys.exit(main())
  File "./battery_plotter.py", line 70, in main
    plot_punchcard(batt_d['date'], plotq[5:], punchrad=args.punchrad, quantity=batt_d[plotq[5:]], outdir=args.outdir)
  File "./battery_plotter.py", line 428, in plot_punchcard
    thisqn = 4.0*np.mean(quantity[dmask][tmask])/np.sum(quantity)
IndexError: arrays used as indices must be of integer (or boolean) type

@parkr
Copy link

parkr commented Aug 2, 2014

dmask is

[False False False False False False False False False False False False
 False False False False False False False False False False False]

and tmask is [].

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment