iot fastapi react mqtt websockets python

SolarRally: The IoT Stack Behind an EV Charger Dashboard

How I built a real-time EV solar charging monitor using FastAPI, WebSockets, MQTT, and a React dashboard — and what I'd do differently.

SolarRally started as a question: what does it actually take to surface live telemetry from a solar-powered EV charger into a browser dashboard? The answer turned out to be a four-layer stack where every layer has its own failure modes.

Architecture Overview

[EV Charger / Sensor Node]
        │  MQTT publish

[MQTT Broker — Mosquitto]
        │  subscribe

[FastAPI Backend — Python]
        │  WebSocket broadcast

[React Dashboard — Browser]

Simple on paper. Each arrow hides complexity.

MQTT: The Reliable Piece

Mosquitto was the one part that never gave me trouble. Charger state, solar input wattage, and battery SOC published to topics like charger/state, solar/power, and battery/soc at 1-second intervals.

The FastAPI backend subscribed using paho-mqtt:

import paho.mqtt.client as mqtt

def on_message(client, userdata, msg):
    topic = msg.topic
    payload = json.loads(msg.payload)
    asyncio.run_coroutine_threadsafe(
        broadcast(topic, payload), loop
    )

The asyncio.run_coroutine_threadsafe pattern is the key — paho runs its network loop in a thread, but the WebSocket manager lives in the asyncio event loop. Bridging them without deadlocks took a few iterations.

WebSockets: Where Things Got Interesting

FastAPI’s WebSocket support is clean, but managing multiple clients requires a connection manager. Mine handled connect, disconnect, and broadcast:

class ConnectionManager:
    def __init__(self):
        self.active: list[WebSocket] = []

    async def connect(self, ws: WebSocket):
        await ws.accept()
        self.active.append(ws)

    async def disconnect(self, ws: WebSocket):
        self.active.remove(ws)

    async def broadcast(self, data: dict):
        dead = []
        for ws in self.active:
            try:
                await ws.send_json(data)
            except:
                dead.append(ws)
        for ws in dead:
            self.active.remove(ws)

The stale connection cleanup in broadcast was an afterthought that became essential — browser tabs close without clean WebSocket termination constantly.

React Dashboard

The frontend consumed the WebSocket stream and updated a live chart using recharts. The pattern was straightforward: useEffect opens the socket, useState holds the sliding window of data points, and LineChart re-renders on each update.

One gotcha: recharts re-renders on every data push, and at 1Hz with a 60-point window, this was fine. At 10Hz it would destroy performance. The fix is useMemo on the data slice and shouldComponentUpdate equivalents — worth knowing before you scale.

What I’d Do Differently

1. TimescaleDB instead of PostgreSQL. Raw power data is time-series by nature. TimescaleDB’s automatic partitioning and time_bucket aggregation would have saved me from writing a lot of manual windowing SQL.

2. Server-Sent Events for the dashboard. WebSockets felt like overkill for a unidirectional data stream. SSE is simpler, survives load balancers without sticky sessions, and browsers handle reconnection automatically.

3. Schema validation at the MQTT boundary. Late in the project I added Pydantic models to validate MQTT payloads. It caught two cases where the charger firmware sent malformed JSON during a firmware update. Should have been there from day one.


SolarRally is on GitHub — the README has setup instructions for running it locally with a simulated MQTT publisher so you don’t need actual hardware.