Home Assistant : Solar Self-consumption Monitoring
Background
The project
Not bad for a first attempt. But, still, we cannot see the most important information: should I plug in the dishwasher? Instead, there is information that I do not need like the temperature of some irrelevant rooms and the agenda, I do not need that.
However, when looking at this dashboard, I immediately thought about 3-color ePaper. Yes! This is what I need, a big enough ePaper and some cool frame to display that information to anyone passing by the frame. Go! Let's quickly order a 7.5 inch 3-color ePaper display while continuing this project.
After a few iterations, I have found the ideal dashboard, here it is :
Home Assistant
MicroPython
Hard resetting via RTS pin...
The software architecture
/ (ESP32 flash root)
├── main.py ← orchestrates the full cycle├── display_en.py ← UC8179 driver + dashboard layout in EN
├── battery.py ← MAX17048 fuel gauge over I²C
Here is the mpremote command to push all files at once on the ESP32 :
# Push all files to the ESP32 flash root
$ python3 -m mpremote cp \
main.py \
epaper_ha_client.py \
display.py \
display_en.py \
battery.py \
config.json \
barlow_bold_56.py \
barlow_bold_40.py \
barlow_bold_28.py \
archivo_bold_13.py \
:
# Verify
$ python3 -m mpremote lsdisplay_en.py to display.py before copying (or delete the French version on the device after the copy).The cycle (main.py)
Every 10 minutes the ESP32 wakes from deep sleep and runs the same sequence :
- Pre-allocate the framebuffer —
bytearray(48000)as the very first thing, before any other import - Connect WiFi — read
config.json, attempt 3 retries - Fetch HA data — REST calls to read ~14 sensors and the production history
- Release WiFi — give back ~40 KB of RAM to the system
- Read battery — query the MAX17048 fuel gauge via I²C
- Render the dashboard — compose the 2 planes (black + red), send to ePaper
- Deep sleep for the next 10 minutes
Home Assistant integration
Home Assistant exposes a clean REST API that lets external clients read the state of any sensor, fetch history, and even trigger actions. For this project I only need read access — the ESP32 never writes anything back to HA.
Getting an authentication token
In HA, go to your user profile → Long-lived access tokens → Create token. Give it a name like epaper-dashboard, copy the token (a long string starting with eyJ...), and store it. HA only shows it once — if you lose it, you'll have to create a new one.
⚠️ Treat the token as a password. It grants full read access to your entire HA instance. Never commit it to git.
The token, the WiFi credentials, and the HA URL all go into config.json on the ESP32 flash:
{
"wifi_ssid": "YourNetwork",
"wifi_pass": "YourPassword",
"ha_url": "http://192.168.x.y:8123",
"ha_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6........"
}$ python3 -m mpremote cp config.json :
Reading a sensor
The REST endpoint to read one sensor is:
GET /api/states/<entity_id>
Authorization: Bearer <ha_token>For example, GET /api/states/sensor.pv_production_today returns a JSON document like:In MicroPython, fetching that becomes:{"entity_id": "sensor.pv_production_today", "state": "14.23", "attributes": { "unit_of_measurement": "kWh", ... }, "last_changed": "2026-06-06T14:32:11.000Z"}
import urequests
HEADERS = {"Authorization": "Bearer " + TOKEN}
def get_state(entity_id): r = urequests.get(HA_URL + "/api/states/" + entity_id, headers=HEADERS) state = r.json()["state"] r.close() # always close to free the socket! return state
pv_today = float(get_state("sensor.pv_production_today"))
epaper_ha_client.py:ENT = { "pv_today": "sensor.pv_production_today", "pv_now": "sensor.pv_production_now", "inj_today": "sensor.grid_export_today", "tarif": "sensor.current_electricity_tariff", "t_ext": "sensor.outdoor_temperature", "lever": "sensor.sunrise", "coucher": "sensor.sunset", # ... 7 more }
This way, swapping a sensor (different brand, different naming) means editing one line.
The histogram — fetching production over time
A bare list of "today's totals" isn't very informative. What I really want is the shape of today's production : was the morning sunny? Did a cloud pass at 2 PM? Where am I on the daily curve?
HA exposes a history endpoint that returns all data points logged for a sensor within a time window:
GET /api/history/period/<iso_timestamp>?filter_entity_id=<entity>
&minimal_response &significant_changes_only &no_attributes
The 3 query parameters at the end matter:
minimal_response— strips redundant fields, smaller JSONsignificant_changes_only— only points where the value actually changedno_attributes— we only needstateandlast_changed
For a 12-hour window I typically get 30-50 data points. The challenge is that these points are irregularly spaced (HA only logs when the value changes by more than a threshold). To draw a clean histogram of 24 half-hour buckets, I need to interpolate.
The trick exploits the fact that the production sensor is cumulative (kWh since midnight) and monotonic (only goes up, except at midnight reset) :
- For each 30-minute boundary in the last 12 hours, find the two data points surrounding it
- Linearly interpolate between them to estimate the cumulative value at that exact moment
- The production during a 30-minute slot is the difference between two consecutive interpolated boundaries
def interpolate_at(timestamp, points):
"""Linear interpolation between the 2 surrounding points."""
for i in range(len(points) - 1):
t1, v1 = points[i]
t2, v2 = points[i + 1]
if t1 <= timestamp <= t2:
return v1 + (v2 - v1) * (timestamp - t1) / (t2 - t1)
return points[-1][1] # fallback to last known
A surprise time-zone gotcha
The sunrise and sunset times displayed in my dashboard kept showing up 2 hours off during summer. The culprit was the HA template that computes them:
# WRONG — outputs UTCtemplate:
- sensor: - name: "Sunrise" state: "{{ state_attr('sun.sun','next_rising')[11:16] }}"
# RIGHT — outputs local time, DST-awaretemplate:- sensor:- name: "Sunrise"state: >{% set t = as_datetime(state_attr('sun.sun','next_rising')) | as_local %}{{ t.strftime('%H:%M') }}No ESP32 code change needed — at the next refresh cycle, the correct values appear automatically.
Bill of Materials
Total cost: roughly 70-90 € depending on your suppliers and what you already have lying around.
Electronics
- Tricolor ePaper display + driver board — Waveshare 7.5" V2 (black/white/red), 800×480 px, shipped together with the ESP32 driver board. ~65 € on Amazon FR. Make sure you select the V2 version when ordering — older revisions use a different controller chip and the code in this project won't work with them.
- LiPo battery — HXJNLDC 7565121, 1S 3.7V, 8200 mAh, with built-in PCM (protection circuit module) and pre-soldered wires. ~12 € on Amazon FR. The size (75×65×12 mm) fits nicely behind a 17×22 cm photo frame.
- LiPo charger + 5V boost — Seeed Lipo Rider Plus. ~15 € on Kiwi Electronics. Charges the LiPo via USB-C and boosts the 3.7V cell voltage to a stable 5V for the ESP32 — two functions in one tiny board. The 2.4A boost output has plenty of headroom for the ePaper refresh peak.
- Battery fuel gauge — Adafruit MAX17048 (part 5580). ~12 € on Kiwi Electronics. I²C battery monitor that gives accurate
%, voltage, and charge rate. Optional but highly recommended — without it, you have no way to know if your LiPo is at 80% or about to die. 💡 The chip is ESD-sensitive — order two if you can afford to. A spare will save you a 3-day shipping wait when you (inevitably) zap one with a careless wiring move. - JST-PH adapter cable (optional) — 2-pin JST-PH male-to-male, ~10 cm. Around 2 €. Useful if your LiPo's polarity doesn't match the charger's expected polarity (you'll see this in the hardware section).
- Dupont jumper wires — Female-to-female, ~10 cm. A few cents each. You need 4 wires for the I²C bus between the fuel gauge and the ESP32 driver board.
- USB-A to USB-C cable — Short (~15 cm), data-capable. Around 3 €. Connects the boost output to the ESP32 input. A short cable keeps things tidy inside the frame.
- USB-C charger — Any 5V/2A wall adapter. ~5-10 €. Only used occasionally to recharge the LiPo.
Enclosure
- Photo frame — Wooden or MDF, with at least 17×22 cm interior, ≥ 25 mm depth, and a removable back. 2-15 € from any furniture store or supermarket. I used a basic 2 € wooden frame and it works beautifully.
- Passe-partout (mat board) — Off-white or cream, cut to expose the active area of the panel (163×98 mm). Around 2-5 €. Adds a clean visual finish — hides the panel borders and gives a professional look.
- Double-sided foam tape — 3M-style, ~1 mm thick. Around 3 €. To stick the LiPo and small boards inside the frame.
Tools (one-time)
- Soldering iron + solder — Any basic 30W iron will do. Needed to fix the polarity adapter cable and (optionally) to solder pin headers on the Lipo Rider Plus.
- Multimeter — A basic 10 € meter from Amazon is sufficient. Essential for voltage and polarity checks before plugging the LiPo into anything — saves you from frying a chip the hard way.
- Heat shrink tubing — A small assortment kit (~5 €) lasts years. Used to insulate exposed solder joints.
- USB cable to the ESP32 — Most projects use USB-C nowadays. You'll need it to flash the firmware and push your code from your computer.
Software (free)
- MicroPython firmware for the ESP32 — download from micropython.org.
esptoolto flash the firmware —pip install esptool.mpremoteto copy files and run code from your computer —pip install mpremote.- A Home Assistant instance, with a long-lived access token.
What I'd avoid
A few things I tried first and would recommend skipping:
- A USB power bank to power the ESP32 in deep sleep — they all auto-shutoff when the current draw drops below ~50 mA, so the ESP32 dies at the first deep sleep. Use a dedicated LiPo + boost setup instead.
- A bare TP4056 charger module without a boost — it charges the LiPo but doesn't output 5V to power the ESP32. You'd need a separate boost converter and lose efficiency on the wiring. The Lipo Rider Plus combines both functions and is much cleaner.
- A 7.5" ePaper without the "V2" tag — older revisions use a different driver chip and the code in this project won't work out of the box.
The Connections
The diagram is split into two halves:
- Top — signal and data connections: the ePaper panel talks to the ESP32 via SPI over a flat FFC ribbon (6 signals). The MAX17048 fuel gauge talks to the ESP32 via I²C (4 wires : VIN, GND, SDA, SCL).
- Bottom — power chain: the LiPo flows through a polarity adapter, then the MAX17048 (which measures it via JST passthrough), then a JST inverter cable, and finally into the Lipo Rider Plus, whose boosted 5V output powers the ESP32 via a short USB-A to USB-C cable.
Final Product
The time has finally come to show the final product with all the components fitted together!
What I learned
Looking back at this project from idea to finished frame, a few takeaways stand out:
- Tricolor ePaper is gorgeous in person — the red ink really pops on the off-white background, and the matte finish feels like printed paper rather than a screen. Worth every euro.
- MicroPython on ESP32 is more capable than I expected — the 165 KB heap feels tight at first, but with discipline (pre-allocate buffers, lazy imports, release WiFi early) you can do surprisingly rich applications.
- The real magic is in Home Assistant — the dashboard is "just" a remote display, but because HA exposes everything via a clean REST API, anyone can adapt this project to whatever sensors they care about. Solar today, washing machine state next, fridge temperature after that.
- A 2 € photo frame beats a 3D-printed enclosure — I almost went down the rabbit hole of designing a custom case in OpenSCAD. Glad I didn't. A wooden frame with a passe-partout looks classier and takes 5 minutes instead of 5 hours.







Comments
Post a Comment
Thank you for your message, it has been sent to the moderator for review...