My parents installed solar panels on the roof of their house. This is obviously a win for renewable energy, but it's also a win for project sources! This hackathon my dad and I built an old-school display to show how much power is being generated.

Unlike my previous projects, I actually came up with a semi-legitimate project plan well before starting anything:

  • Build a data ingestion pipeline
    1. Set up InfluxDB for storing the data
    2. Write a script to ingest the data
    3. Create a Telegraf dashboard to show the historical data
  • Build an analog meter using an ESP32
    1. Write a script to read from influx
    2. Connect the ESP32 to wifi
    3. Wire the board
    4. Build a housing for the whole thing

The Research

The plan above is really the idealized version. Before we could start with anything on that list we needed to actually find a nice dial and figure out how to use it! I knew that, generally, we needed a voltmeter. Something we can hook up to a GPIO pin, output the desired voltage, and see the corresponding needle movement. I quickly learned the magic words "analog panel meter." That's the generic term for indicators like that. There are a lot of mediocre panel meters out there, but the goal of this project was to be a nice desk item. A cheap plastic looking meter won't suffice! After a lot of searching and deliberation I found what I thought was the perfect meter

PERCENT OF NORMAL: perfect

Unfortunately when it arrived I had NO idea how to actually drive it. I hooked it up to a variable power supply and fiddled with the dials and almost immediately maxed out the dial (whoops!). The packaging was a total mystery:

My friend Ben discovered that the important scratchings on the box are M 1329 and 0-200 MIC. 1329 is the model number for Simpson's Wide-Vue 4.5" DC Microammeter panel meter. 0-200 MIC indicates the range of the meter, 0-200 μA.

Initially I was very concerned. It seemed like the challenge had changed from "get a GPIO pin to output a voltage" to "get a GPIO pin to output a specific current." The former is straightforward - use PWM to approximate the desired voltage. The latter, on the other hand, seemed much more complicated. I started researching voltage-driven current sources and almost immediately hit a wall. Googling the problem shows many sample circuits, but none of them were intelligible to me. I had no idea where to even start making changes to achieve the desired current.

uhhh... what do I do with this?! https://www.tina.com/blog/voltage-controlled-current-source-circuit/

After way too long, I remembered back to high school physics. Ohms Law. V = IR. The current is directly proportional the voltage of the circuit. We know the max voltage of the ESP32 is 3.3V. We know we need a max current of 200μA. That means we just need a resistor of around 3.3V/200μA = 16500Ω to scale the current down to the appropriate range. 16500Ω isn't a common resistor rating, but it's simple enough to wire together 10k, a 4.7k, and a 2.2k resistors in series to get a CloseEnough™ one. Any compensation past that could be done through software or a potentiometer.

Neither of me nor my dad had used an ESP32 before, so we needed to research how to actually write code for it. In doing so we found the MicroPython project that allows you to write python code instead of C. MicroPython looked very promising as it has full support for the board, as well as many common sensors and protocols.

The Software

With the dial figured out we felt confident enough to start working through the steps in the plan. The first step was to set up some infrastructure. We needed an InfluxDB database to store the data and a Telegraf instance for visualizing the data.

Rather than spend time configuring each service we turned to docker. The official images for influx and telegraf (unsurprisingly) worked perfectly with each other. To help make this setup repeatable we whipped up a docker-compose.yml and started it up.

version: '3'
services:
  influxdb:
    image: "influxdb:latest"
    container_name: influxdb
    volumes:
      # Mount for influxdb data directory
      - ./influxdb/data:/var/lib/influxdb
      # Mount for influxdb configuration
      - ./influxdb/config/:/etc/influxdb/
    ports:
      # The API for InfluxDB is served on port 8086
      - "8086:8086"
      - "8082:8082"
      # UDP Port
      - "8089:8089"

  chronograf:
    image: "chronograf:latest"
    container_name: chronograf
    volumes:
      - ./chronograf:/var/lib/chronograf
    ports:
      - "8888:8888"
    depends_on:
      - influxdb

Now that we had somewhere to put the data it was time to start collecting it. We have an Enphase Envoy system. The system has a web interface that shows the current production of the system. Firing up the Chrome devtools shows that it's requesting several json files. Perfect! We can script against that! The most interesting request is to http://envoy-ip/production.json, which appears to contain all the data we need.

{
  "production": [
    {
      "type": "inverters",
      "activeCount": 18,
      "readingTime": 1553619770,
      "wNow": 4309,
      "whLifetime": 13136601
    },
    {
      "type": "eim",
      "activeCount": 1,
      "measurementType": "production",
      "readingTime": 1553620169,
      "wNow": 4395.389,
      "whLifetime": 12981605.665,
      "varhLeadLifetime": 0.314,
      "varhLagLifetime": 6155135.453,
      "vahLifetime": 17257638.644,
      "rmsCurrent": 34.879,
      "rmsVoltage": 252.589,
      "reactPwr": 361.391,
      "apprntPwr": 4404.239,
      "pwrFactor": 1,
      "whToday": 10581.665,
      "whLastSevenDays": 143242.665,
      "vahToday": 13025.644,
      "varhLeadToday": 0.314,
      "varhLagToday": 3859.453
    }
  ],
  "consumption": [
    {
      "type": "eim",
      "activeCount": 0,
      "measurementType": "total-consumption",
      "readingTime": 1553620169,
      "wNow": 4395.389,
      "whLifetime": 12981602.665,
      "varhLeadLifetime": 0.314,
      "varhLagLifetime": 6155135.453,
      "vahLifetime": 0,
      "rmsCurrent": 35.162,
      "rmsVoltage": 252.567,
      "reactPwr": -360.487,
      "apprntPwr": 8880.727,
      "pwrFactor": 0.49,
      "whToday": 12981602.665,
      "whLastSevenDays": 12981602.665,
      "vahToday": 0,
      "varhLeadToday": 0.314,
      "varhLagToday": 6155135.453
    },
    {
      "type": "eim",
      "activeCount": 0,
      "measurementType": "net-consumption",
      "readingTime": 1553620169,
      "wNow": 0,
      "whLifetime": 0,
      "varhLeadLifetime": 0,
      "varhLagLifetime": 0,
      "vahLifetime": 0,
      "rmsCurrent": 0.283,
      "rmsVoltage": 252.545,
      "reactPwr": 0.904,
      "apprntPwr": 35.751,
      "pwrFactor": 0,
      "whToday": 0,
      "whLastSevenDays": 0,
      "vahToday": 0,
      "varhLeadToday": 0,
      "varhLagToday": 0
    }
  ],
  "storage": [
    {
      "type": "acb",
      "activeCount": 0,
      "readingTime": 0,
      "wNow": 0,
      "whNow": 0,
      "state": "idle"
    }
  ]
}

At first glance, the first entry in the production array (the inverters section) seemed to have the exact stats we were looking for. Closer inspection, however, revealed that those values aren't actually the ones shown in the UI. Moreover, they seemed to only update on a 15 minute interval. The second entry (the eim section... whatever that means) has similar metrics, but also goes into greater detail. More importantly, these values update with every API call. This makes them much more suited for a semi-real-time display.

Scraping this API using python is very straightforward. We set up a simple loop that used the requests API to download the json every 15 seconds. For each result we do two things: save it to influxdb for the sweet graphs and publish it to a MQTT topic.

MQTT is a popular protocol for IoT device communication. It provides a message passing mechanism that allows for any number of publishers and subscribers. This means it's really easy to build a decoupled system where the data provider and the data consumer have no knowledge of each other. This is perfect for the meter project. We don't want the uptime of one part to affect the uptime of the other. Obviously the meter isn't particularly useful if the data loader isn't running (and vice-versa), but this decoupling means that as soon as the broken service is restored everything can start working again. No manual action should be necessary to reconnect the working service to the recovered one.

The completed loader program is on github. To help testing, there's an MQTT debug client that allows us to send and receive messages on our configured topics. There's also a dockerfile so we can build and run the program using our main docker-compose.yml file

The Firmware

The data loader was in good shape so we set to work on the firmware for the ESP32. We connected the device to my laptop and after a little fiddling were able to get the right drivers working so that we could open a connection. Following the setup instructions we were able to flash the latest version of MicroPython and get a minimal python REPL (Read-Execute-Print Loop). Once we were connected to the WIFI we switched to the WebREPL. It provides the same functionality but with a ...better? interface.

The MicroPython WebREPL
The WebREPL in all its 1990s web glory

Using the WebREPL we experimented with driving the built in LED and dial. The PWM function works great for both! PWM lets us approximate analog outputs in a digital system by trading the complex problem of "output 50% power on a pin" with the simple problem of "output full power 50% of the time and no power 50% of the time."

import machine

# Define the pin
meter_pin = machine.Pin(meter_pin, machine.Pin.OUT)

# Set up the pin for analog output
meter_pwm = machine.PWM(meter_pin, freq=1000)

meter_pwm.duty(1000) # Full power
meter_pwm.duty(500)  # Half power
meter_pwm.duty(0)    # No power

We also had a DHT11 to collect humidity and temperature data. Unfortunately this is only reading the air around the sensor itself so it's not useful for correlating temperature to solar production. It is, however, a cool data source for making more graphs! MicroPython has built-in support for this type of sensor so after wiring it in we were able to get readings in the REPL in just a few lines

import dht
dht_pin = machine.Pin(dht_pin, machine.Pin.IN)
dht = dht.DHT11(dht_pin)

# Take a reading (updates state within DHT, nothing returned)
dht.measure()

# Get the values from the last reading
dht.temperature()
dht.humidity()

With these building blocks in place, we just needed to wrap everything up into a nice loop and flash it to the board. The full code for displaying the current solar production and writing back the the temperature data can be found on github.

Our debug program in action: setting the value of the meter via MQTT messages

The Hardware

The wiring for the project ended up being pretty straightforward. We use two IO pins for the dial and the temperature sensor, and then connect the common power and ground lines.

The working circuitry, ready to be soldered forever

Soldering everything together on a prototyping board went smoothly. The most tedious part was connecting each pin on the ESP32. We probably could have skipped this since we only made connections to a few of the pins, but it did give us a good practice opportunity.

The final board, ready for housing

The Polish

The last stage of the project has been the hardest. We have a fully functioning meter, but it still needs a nice house before we can proudly display it. For simplicity's sake we decided to build an angled bracket with a base and a slanted front to mount the panel onto.

Finding software tools is easy. Finding woodworking tools, not so much. The main challenge of assembling a stand for the meter is to cut out a circular hole for the back of the dial to fit through. We couldn't find a hole saw of an appropriate diameter that wasn't a part of an expensive kit. Determined, we fired up the drill press and cut dozens of holes in a circular pattern instead! It wasn't pretty, but it was effective!

At this point the only thing we have left to do is finish staining the piece and then attach the front side to the base. Once that is finished I will update the post to share the final images!