Household Power Use Pt. 2 – Move to ESP8266

In a previous post I wrote about displaying household power use on a small OLED display. Here, I’ll describe further steps towards making a standalone device by reading the data and controlling the display with a wifi-enabled microcontroller.

This turned out to be a substantial project. The limited power and storage of the ESP-12E required writing a micropython library to access the energy gateway and creating a memory-light copy of pretty fonts for the display.

Finally, I wanted to make the display look a little nicer, so I also added a PWM-controlled panel meter to illustrate current power use.


To start, I’ll set up a nice working environment to work in.

python3 -m venv venv
source venv/bin/activate

and install a few tools for convenience

pip3 install mpfshell
pip3 install mpy-cross


Getting the Data

Like in the previous post, I’ll be pulling data from my smart meter. The module I used last time, evilpete’s RainEagle module, was too difficult to port over to MicroPython, so instead I ended up writing a lightweight library, uEagle, which is detailed in this post.


Small Fonts

To move to a microcontroller it is necessary to minimize the memory used by the typeface. To that end I’ll use peterhinch’s font-to-py.

pip3 install freetype-py
git submodule add tools/micropython-font-to-py

This tool provides two things. First, renders a subset of a ttf font to raw bytes in a python file, allowing fast but memory-light access. Second, it provides a Writer class that uses fonts in this format to write to a display like the SSD1306.

Preparing a nice looking font for the display simply involves fetching the font and running font_to_py on it. I’m again using Roboto Regular, but any ttf should work. For flexibility I make both half-height (15px) and full-height (32px) variants.

python3 tools/micropython-font-to-py/ -x -f RobotoMono-Regular.ttf 15
python3 tools/micropython-font-to-py/ -x RobotoMono-Regular.ttf 32

All that’s left is to get the and the fonts onto the board. As peterhinch notes, is quite large and requires freezing/compilation to run on the ESP8266.

python3 -m mpy_cross
mpfshell -n -o ttyUSB0 -c put font_full.mpy

python3 -m mpy_cross
mpfshell -n -o ttyUSB0 -c put font_half.mpy

cd tools/micropython-font-to-py/writer
python3 -m mpy_cross
mpfshell -n -o ttyUSB0 -c put writer.mpy

Writing to the Display

With nice fonts on the microcontroller, the next step is to get a them displayed on the SSD1306.

import machine
import ssd1306
import writer
import font_half

#Setting up the OLED display
scl_pin = machine.Pin(14) #"D5" on the NodeMCU
sda_pin = machine.Pin(12) #"D6" on the NodeMCU
i2c = machine.I2C(scl=scl_pin, sda=sda_pin)

#Displays are either 128x32 or 128x64
ssd = ssd1306.SSD1306_I2C(128, 32, i2c)

wri = writer.Writer(ssd, font_half)

#Writing a test message
wri.set_textpos(ssd, 0,0)

Micropython’s SSD1306 drivers have quite a few drawing routines built in, so unlike in the last post, there’s no need to create a PIL image/artist. (Unfortunately there’s still no support for smooth scrolling.)

Adding an Analog Meter

In addition to the digital display from my previous post, I’m going to add an analog display – a 200μA panel meter I found at an antique shop. To do so I’m going to use one of the ESP8266’s pulse-width modulation (PWM) pins and an ≈16kΩ resistor to map the board’s 3.3V full-cycle output down to the gauge’s 200mA maximum. (The ammeter’s response is sufficiently slow that there’s no need to low-pass the PWM signal.)

The relevant code to monitor a signal f(x), with range (-1,1), might be

import machine

pwm = machine.PWM(machine.Pin(4), freq=1000)

from math import sin
from utime import sleep_ms
for t in range(0,100):
    theta = t * 2 * math.pi / 100
    sig_value = sin(theta)
    pwm.duty(int(1023 * (sig_value + 1)/2))

Putting it all together

To build the final product I wired up the ESP-12E, panel meter and display as seen here:

The code depends on a few external resources which need to be uploaded to the board

  • Network credentials, stored in network_creds.json
  • EAGLE credentials, stored in eagle_creds.json
  • font_half.mpy and writer.mpy, as described above
  • slimDNS.mpy, nickovs’ mDNS server (optional – one can also specify the EAGLE’s IP address explicitly)

With these compiled and uploaded to the board, all that remains is and does two things – it connects to the wireless network and synchronizes the board’s clock

import gc
import utime
import network

#Get on wifi using network_creds.json
    import ujson
    creds = ujson.load(open('network_creds.json','r'))
    sta_if = network.WLAN(network.STA_IF)
    if not (sta_if.isconnected() and sta_if.config('essid') == creds['essid']):
        sta_if.connect(creds['essid'], creds['password'])
        for i in range(10):
            if sta_if.isconnected():

    print('Couldn\'t connect to network')


#Set network time
import ntptime
for _ in range(4):


The main script sets up the display, PWM pin, and connects to the EAGLE before entering a while True loop that repeatedly polls the EAGLE for new data and updates the displays. It also keeps a running maximum demand value and adjusts the panel meter’s scale.

#Set up the power meter
import machine
import ssd1306
import writer
import font_half #font_full
from uEagle import Eagle
from micropython import const

from utime import ticks_ms, ticks_diff, sleep, sleep_ms

SCL_PIN = const(14)
SDA_PIN = const(12)
PWM_PIN = const(4)
SSD_WIDTH = const(128)
SSD_HEIGHT = const(32)


for _ in range(4):
        import ntptime

#Setting up the OLED display
i2c = machine.I2C(scl=machine.Pin(SCL_PIN), sda=machine.Pin(SDA_PIN))
ssd = ssd1306.SSD1306_I2C(SSD_WIDTH, SSD_HEIGHT, i2c)
wri = writer.Writer(ssd, font_half)

#Setting up analog PWM meter
pwm = machine.PWM(machine.Pin(PWM_PIN))

#Setting up the EAGLE
import ujson
    eagle_creds = ujson.load(open('eagle_creds.json','r'))
    CLOUD_ID = eagle_creds['cloud_id']
    INSTALL_CODE = eagle_creds['install_code']
    ADDRESS = eagle_creds.get('address', None)
    raise Exception('Couldn\'t read credentials from eagle_creds.json.')

if ADDRESS is None:
    import network
    from slimDNS import SlimDNSServer
    sta_if = network.WLAN(network.STA_IF)
    ap_if = network.WLAN(network.AP_IF)
    my_addr = sta_if.ifconfig()[0]
    ap_was_active =
    server = SlimDNSServer(my_addr)
    ADDRESS = server.resolve_mdns_address('eagle-{}.local'.format(CLOUD_ID))
    ADDRESS = '{}.{}.{}.{}'.format(*ADDRESS)



eagle = Eagle(CLOUD_ID, INSTALL_CODE, address=ADDRESS)

def get_demand():
    demand_data = eagle.get_instantaneous_demand()
    demand = demand_data['Demand']
    timestamp = demand_data['TimeStamp']
    return timestamp, demand

def write_to_ssd(s):
    _ = wri.set_textpos(ssd, 0,0)

#max_demand = float(eagle.get_demand_peaks()['PeakDelivered'])
max_demand = 0.

t_data = 0
t_disp = 0
while True:
    t0 = ticks_ms()

    if ticks_diff(t0, t_data) > 1000 * DATA_REFRESH_TIME:
        t_data = t0
            timestamp, demand = get_demand()
        except OSError:
            pass #Probably trouble connecting to device
        max_demand = max(demand, max_demand)

    if ticks_diff(t0, t_disp) > 1000 * DISPLAY_REFRESH_TIME:
        t_disp = t0
        write_to_ssd('{} kW\nMax: {} kW'.format(demand, max_demand))
        pwm.duty(int(demand * 1000 / max_demand))


    t1 = ticks_ms()
    wait_data = int(1000 * DATA_REFRESH_TIME - ticks_diff(t1, t_data))
    wait_disp = int(1000 * DISPLAY_REFRESH_TIME - ticks_diff(t1, t_disp))
    sleep_ms(min(wait_data, wait_disp))

And here it is in action:

Future Work

The code above works, but is quite unstable. It doesn’t handle errors from uEagle at all, and since most of the code lives in, a single crash ends the main loop.

In the final part of this project I’ll focus on making the display stable enough to serve as a practical household display by using MicroPython’s uasyncio library.

I’ll also try to incorporate some of the features from part 1, including a clock and scrolling line graph, and add the ability to switch between display modes with a button.


Joseph Albert

Leave a Reply

Your email address will not be published. Required fields are marked *