Nathan Grigg

More Charging Plots

Since I last analyzed my electricity usage two years ago, several things have changed:

  1. I had a 240V charger installed, which speeds up charge time and draws more power.
  2. My electric company changed the XML data slightly, with an entry every 15 minutes instead of 60, and with entries for power returned to the grid. For me, the latter are always zero because my house does not generate electricity.
  3. We replaced our family-hauling gas minivan with an electric vehicle, so now there are two cars to charge. This happened at the end of the year, so you don’t really see it in the data yet.

To extract the data in Python, using the built-in xml.etree module to convert to a Pandas series, I reused most of the code from last time:

from xml.etree import ElementTree
import datetime
import pandas as pd

ns = {'atom': 'http://www.w3.org/2005/Atom', 'espi': 'http://naesb.org/espi'}
root = ElementTree.parse('/path/to/file.xml').getroot()
prefix = "./atom:entry/atom:content/espi:IntervalBlock[@rel='Delivered']/espi:IntervalReading"
times = [datetime.datetime.fromtimestamp(int(x.text))
         for x in root.findall(prefix + "/espi:timePeriod/espi:start", ns)]
values = [float(x.text)
          for x in root.findall(prefix + "/espi:value", ns)]
ts = pd.Series(values, index=times).sort_index()

The main difference is the addition of [@rel='Delivered'] to filter to only power delivered to me and not the other way around. I also added the sort_index command, because for some reason the dates are not entirely in order in the XML.

At this point, I wanted to determine when I was charging a car. Charge loads are probably pretty easy to isolate, because they last for a long time and are relatively constant. If I were trying to be robust, I would probably figure out what expected power draw for a given time of year and time of day, and then find out periods where the draw is significantly higher than that. Using something simple like the median of the surrounding 14 days of a given time would probably work, since I charge less than half of the days.

But in my case, the 7.5 kW of power that our electric Mini draws is more than our entire house uses over any 30-minute period. There are five 15-minute periods that reach that level, but these are relatively easy to filter out.

I wrote this code to compute the charge state. I wanted to separate it into cycles of “off”, “start”, “on”, and “stop”. My thinking was that these “start” and “stop” periods are probably times where I was charging the car for some but not all of the 15-minute period. I used a threshold of 1800 Wh, which is 7.2 kW over a 15-minute period.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
THRESHOLD = 1800
charge_state = pd.Series('off', index=ts.index)
charging = False
for i in range(len(ts)):
    if charging:
        if ts.iloc[i] >= THRESHOLD:
            charge_state.iloc[i] = 'on'
        else:
            charge_state.iloc[i] = 'stop'
            charging = False
    # Look at the two entries after this one to see if
    # they are both above the threshold.
    elif all(ts.iloc[i+1:i+3] >= THRESHOLD) and i+1 != len(ts):
        charge_state.iloc[i] = 'start'
        charging = True

Line 2 creates a new series with the same index as our time series. We then look at the entries one by one and determine when to transition to “start” (Line 13, if we are not already charging and we see two upcoming entries above the threshold), when to stay “on” (Line 6, as long as we stay above the threshold), when to transition to “stop” (Line 8, as soon as we first go below the threshold). Note that Pandas uses iloc to look up an entry by integer offset, rather than by time.

With this charge_state series, it is easy to play around with the data. For example, to count how many charge sessions:

sum(charge_state == 'start')

To look at the entries where usage is high but you aren’t charging. This means “filter ts to points where it is above the threshold but charge_state is off.”

ts[(ts > THRESHOLD) & (charge_state == 'off')]

Finally, a good visualization is always helpful to understand the data. I don’t usually use much ChatGPT while programming, because at work I am usually dealing with complicated code that I don’t want to mess up. But it is impossible to remember how to do anything in Matplotlib, and I confess that I asked ChatGPT for a lot of help, and it did a pretty good job most of the time.

Here my goal is to draw dots at start and stop time for each charge, and connect them with a line. I really just have three arrays here:

  1. start_time is the full date and time when I started charging. This is used as the x axis.
  2. start_hour is the time of day when I started charging.
  3. charge_hours is the number of hours that I charged.

Note that since charging often happens overnight, I’m using the end time as start_hour + charge_hours, which might be greater than 24, but I think that makes a better visualization than wrapping around to the bottom.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import pyplot as plt

start_time = ts[charge_state == 'start'].index
start_hour = start_time.hour + start_time.minute / 60
charge_hours = (
    ts[charge_state == 'stop'].index -
    ts[charge_state == 'start'].index).total_seconds() / 3600

fig, ax = plt.subplots(figsize=(15, 8))
# The bars
bars = ax.bar(start_time, charge_hours, bottom=start_hour, color='b')
# Draw the number on top of the bar
ax.bar_label(bars, labels=[f'{h:.0f}' for h in charge_hours], padding=4)
# Bottom dot
ax.scatter(start_time, start_hour, marker='.', color='b')
# Top dot
ax.scatter(start_time, start_hour+charge_hours, marker='.', color='b')
# Add a grid and make it denser than it wants to be.
plt.grid(True)
plt.yticks(np.arange(0, 29, 2));

And here is the final result. The Mini only has a 32 kWh battery, so can always fill in four hours or so. The longer lines from December are for the new car, which has triple the battery size, but also can max out the 50-amp circuit that my charger is on by pulling 9.5 kW. (If you do the math, that is only 40 amps, because code require that a continuous load uses only 80% of the rated amperage.)

The Mini used to be scheduled to start at 11 p.m., because it draws 30 amps on our 100 amp service, and I was afraid that if I charged it while everyone was still awake it might trip the breaker. In November, I decided to stop babying our house, and scheduled the charge to start at 9:15 instead. Cheap electricity starts at 9:00.

Also, a quirk of the Mini app is that if you plug it in on a Saturday night (the way my schedule is set), it won’t start charging until Sunday morning at 3:00. That is a long story for another time.


The back side of a clock from inside Musée d’Orsay. In the distance, the Louvre.


Welcoming courtyard


Ancient Roman Arena, Arles, France


The Camargues, France


Cloître Saint-Trophime, Arles, France


View of the river from Pont du Gard, France


Pont Neuf and Square du Vert-Galant, Paris


Boats along the Seine


New growth