Build log · solar array + EV charging

Sun → car,
no clouds involved

How a Fronius GEN24 inverter, a Tesla Wall Connector Gen 3 and a Model 3 were tied together with a Raspberry Pi 4, so the car charges on its own surplus — and pulls hardest exactly when the grid is drowning in sunshine.

Array
7 kW, three-phase
Controller
Raspberry Pi 4 B
Software
evcc · Tesla BLE · InfluxDB · Grafana
Cloud services
none

The whole system, on a single scale

Everything that follows serves this diagram. The car charges from surplus whenever surplus exists; when voltage at the feed-in point climbs — the sign that the whole neighbourhood is exporting and the inverter is about to derate — charging switches to a guaranteed mode and soaks up everything it can.

225 V
237 V
240 V
253 V
01

Architecture

Four devices, one local network, nothing external in the command path.

Fronius GEN24 + Smart Meter at the feed-in point Wall Connector Gen 3, three-phase status + power Raspberry Pi 4 evcc TeslaBleHttpProxy voltage-watch ~4 W, starts on boot Model 3 start / stop amperage control Modbus TCP 502 Solar API HTTP /api/1/vitals Bluetooth LE 3~ power red line = power · black line = data
Data and power flow

The Pi reads production and grid exchange from the inverter, reads socket status from the Wall Connector, and commands the car directly over Bluetooth. A Gen 3 Wall Connector cannot adjust its own current, so the fine control happens on the car — which is why the Bluetooth bridge is the centrepiece rather than a nicety.

Why Bluetooth rather than the Tesla APIIt works without internet, without a developer account, without request limits, and it can go below the usual 5 A floor — useful on cloudy days.
02

What you need

The full list, before touching anything.

Distance mattersThe Pi has to sit within Bluetooth range of the car — realistically 5–15 m, through no more than one wall. Next to the router is ideal if the router is already near the car.
A separate network for the IoT gearThe ASUS TUF-BE6500 earns its place here: alongside Wi-Fi 7 and 2.5 GbE, it can broadcast a fully isolated IoT network, so the Wall Connector, the GEN24 inverter and the Raspberry Pi live on their own segment, walled off from the main LAN — a clean security boundary for devices that have no business reaching the rest of the house. Keep all three on that IoT SSID (2.4 GHz) so they can still talk to one another, give each a DHCP reservation there, and — if the IoT network blocks cross-segment traffic by default — allow access from your main devices so you can still open the evcc and Grafana pages.
03

Writing the SD card

Raspberry Pi Imager 2.x, with the step-by-step wizard.

  1. Device: Raspberry Pi 4
  2. Operating system: Raspberry Pi OS Lite (64-bit), under "Raspberry Pi OS (other)"
  3. Storage: the SD card, then enter the customisation wizard
  4. Hostname: evcc
  5. Location: Bucharest — the radio's country code is derived from this; without it the Wi-Fi adapter stays disabled
  6. User account: username and password, written down
  7. Wi-Fi: the 2.4 GHz network, password typed carefully
  8. SSH: enabled, with password authentication
  9. Raspberry Pi Connect: not needed; everything happens on the local network
If the board doesn't appear on the network after 3 minutesNine times out of ten it's a mistyped Wi-Fi password or a missing location setting. Re-flashing takes five minutes and beats troubleshooting.
04

First contact over SSH

Windows already ships an SSH client; nothing to install.

Open PowerShell and connect. The part before the @ is the username, which is why it is never asked for separately. Nothing appears on screen while typing the password — that's normal.

PowerShell, on the PC
ssh pi@evcc.local

If the name doesn't resolve, find the IP in the router's client list (the host shows up as evcc) and use ssh pi@192.168.50.x. Once in, update the system:

on the Raspberry Pi
sudo apt update && sudo apt full-upgrade -y
sudo reboot
05

Installing evcc

The brain of the system, from an official apt repository.

on the Raspberry Pi
curl -1sLf 'https://dl.evcc.io/public/evcc/stable/setup.deb.sh' | sudo -E bash
sudo apt update
sudo apt install -y evcc
sudo systemctl enable --now evcc

Check the service, then open the interface at http://evcc.local:7070. It will report configuration errors until the following steps are done — that's expected.

check
systemctl status evcc
06

Fronius GEN24

Two interfaces to switch on: Modbus for evcc, Solar API for voltage readings.

If you don't have the Customer password

No installer needed. The registered system owner in Solar.web can reset it:

  1. Tap the optical sensor on the front of the inverter once — the right-hand LED flashes blue and the WLAN access point opens
  2. Open the inverter's web interface in a browser (from the home network, at its usual IP)
  3. Click "Forgot password?" for the Customer role — a PIN is generated
  4. Go to Fronius Solar.web → Settings → Components → Actions on the inverter → Reset Customer Password
  5. Enter the PIN, get the recovery key, set the new password
Owner onlyThe function only appears to the account registered as owner of the system in Solar.web. The Technician password can be reset solely through the installer or Fronius support.

Modbus TCP — the data source for evcc

In the inverter's interface: Communication → Modbus

SettingValue
Slave as Modbus TCPenabled
Port502
SunSpec model typeint + SF
Meter address200

Save. The inverter may briefly report a stopped state — it restarts its communication side and comes back on its own within a minute or two. Verify from the Pi that the port answers:

on the Raspberry Pi
nc -zv 192.168.50.99 502

Connection refused means Modbus isn't on, or the address belongs to another device. succeeded means you can move on.

Solar API — the voltage source for step 12

Communication → Solar API → enabled. It coexists with Modbus without issue. Test:

on the Raspberry Pi
curl -s "http://192.168.50.99/solar_api/v1/GetMeterRealtimeData.cgi?Scope=Device&DeviceId=0" \
  | jq '.Body.Data.Voltage_AC_Phase_1'

A number around 230 means it works. null means Solar API isn't enabled.

07

Wall Connector

All it has to do is join the home network; the rest is read locally.

Easiest route, from the Tesla app: profile → Add product → Wall Connector → scan the QR code from the quickstart card or the label on the side of the unit. The app connects over Bluetooth, so stand near the charger, then join it to Wi-Fi.

Fallback, if the app doesn't offer it: switch the charger's breaker off and back on, connect a phone to the TeslaWallConnector_XXXXXX network (password on the same card) and open http://192.168.92.1. The access point is only live for about 15 minutes after power-up.

2.4 GHz onlyBoth the Wall Connector and the GEN24 inverter refuse 5 GHz networks. If the router uses separate names per band, pick the 2.4 GHz one.

Once connected, note the IP from the router's list, give it a DHCP reservation, and check the local interface in a browser:

in a browser
http://WALL_CONNECTOR_IP/api/1/vitals

The response is JSON starting with "contactor_closed":false,"vehicle_connected":false — exactly right when nothing is plugged in.

08

The Bluetooth bridge

Docker, TeslaBleHttpProxy, and a key with limited rights.

Docker

on the Raspberry Pi
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER

Log out with exit and reconnect so the group membership takes effect.

The bridge

on the Raspberry Pi
mkdir -p ~/TeslaBleHttpProxy/key
nano ~/TeslaBleHttpProxy/docker-compose.yml
~/TeslaBleHttpProxy/docker-compose.yml
services:
  tesla-ble-http-proxy:
    image: wimaha/tesla-ble-http-proxy
    container_name: tesla-ble-http-proxy
    volumes:
      - ~/TeslaBleHttpProxy/key:/key
      - /var/run/dbus:/var/run/dbus
    restart: always
    privileged: true
    network_mode: host
    cap_add:
      - NET_ADMIN
      - SYS_ADMIN
start
cd ~/TeslaBleHttpProxy
docker compose up -d

Generating the key and pairing with the car

Open http://evcc.local:8080/dashboard and click Generate for the Charging Manager role. That role can read data and wake the car, start and stop charging, and set the amperage — it cannot unlock or drive. If the Pi were ever stolen, the key opens nothing.

Pairing, in exactly this order:

  1. Sit in the car and keep the screen on — that way it can't fall asleep mid-procedure
  2. From your phone, in the bridge dashboard under Setup Vehicle, enter the VIN (17 characters) and send the request
  3. Wait for the green message confirming the request was sent
  4. Within 30 seconds, hold the key card flat against the centre console, on the wireless charging area, for 2–3 seconds
  5. The car's screen asks whether to add a new key — confirm it
  6. Verify under Controls → Locks: a new key should appear in the list
Tapping the card isn't enoughThe on-screen confirmation is the step that completes pairing. Nothing appears on screen until the card is tapped — don't wait for a prompt beforehand.

Final test of the chain, with the real VIN in the URL:

in a browser
http://evcc.local:8080/api/1/vehicles/REAL_VIN/vehicle_data?endpoints=charge_state&wakeup=true

JSON with the battery state means the bridge both reads and commands the car. The first request takes a few seconds while the Bluetooth connection is established.

09

Bluetooth that survives reboots

The one part of the chain that can vanish silently after a power cut.

Linux can block the radio through rfkill. The symptom is unmistakable: hci0: can't up device: operation not possible due to RF-kill. Immediate fix:

on the Raspberry Pi
sudo rfkill unblock bluetooth
sudo systemctl restart bluetooth
rfkill list

To guarantee the state at every boot, a tiny service that runs once and exits:

/etc/systemd/system/bt-unblock.service
[Unit]
Description=Unblock Bluetooth at boot
Before=bluetooth.service

[Service]
Type=oneshot
ExecStart=/usr/sbin/rfkill unblock bluetooth

[Install]
WantedBy=multi-user.target
enable
sudo systemctl daemon-reload
sudo systemctl enable bt-unblock
Wi-Fi and Bluetooth share one antennaOn a Raspberry Pi 4, heavy Wi-Fi traffic can make the link to the car unstable (context deadline exceeded errors). If those show up often, move the Pi onto Ethernet and run sudo rfkill block wifi — the radio then belongs to the car alone.
10

The configuration file

One file ties the inverter, the charger and the car together.

sudo nano /etc/evcc.yaml
network:
  schema: http
  host: evcc.local
  port: 7070

interval: 30s

meters:
  - name: grid
    type: template
    template: fronius-gen24
    usage: grid
    host: 192.168.50.99
  - name: pv
    type: template
    template: fronius-gen24
    usage: pv
    host: 192.168.50.99

chargers:
  - name: wallconnector
    type: template
    template: twc3
    host: WALL_CONNECTOR_IP

vehicles:
  - name: model3
    type: template
    template: tesla-ble
    title: Model 3
    vin: REAL_VIN
    url: localhost
    port: 8080
    capacity: 60

site:
  title: Home
  meters:
    grid: grid
    pv: pv

loadpoints:
  - title: Garage
    charger: wallconnector
    vehicle: model3
    mode: pv
    phases: 3
    mincurrent: 2
    maxcurrent: 16
    enable:
      delay: 1m
      threshold: 0
    disable:
      delay: 15m               # ride through brief cloud gaps so the car sleeps less
      threshold: 0

Two values get replaced with your own: the Wall Connector's IP and the VIN. Indentation stays exactly as shown — YAML counts spaces.

Validate and start

on the Raspberry Pi
evcc --config /etc/evcc.yaml --ignore-db checkconfig
sudo systemctl restart evcc
Why --ignore-dbThe database belongs to the system service. Without this flag the command stops with a message about the database instead of checking the configuration.

Why mincurrent: 2: on three-phase, the standard 6 A minimum means a start threshold of roughly 4.1 kW of surplus — rarely reached on a 7 kW array once the house has taken its share. Over Bluetooth the car can go far below that, which moves the threshold to about 1.4 kW and makes mediocre days useful too.

Why the long disable.delay (and interval: 30s)In PV mode a passing cloud can drop the surplus for a minute or two. Without a delay evcc pauses, the Tesla falls asleep, and waking it again over Bluetooth to resume is the least reliable step in the whole chain — it shows up as charger out of sync (step 13). A 15-minute disable delay lets charging ride through those dips so the car stays awake instead of napping and failing to restart. interval: 30s is set for the same family of reasons: Bluetooth commands are slow, and a tighter loop cancels them mid-flight (context canceled).
11

Checks before the first charge

Five minutes now, instead of a month of odd charging sessions.

service status
systemctl status evcc --no-pager -l | head -5
docker ps
rfkill list
sudo journalctl -u evcc -n 50 --no-pager
CheckCorrect result
PV production in the interfacematches what Solar.web shows
Meter sign — switch on a big load (kettle, oven)grid import increases. If it drops, the sign is inverted and the system would charge at exactly the wrong times
The "Garage" loadpointvisible, no vehicle connected
The Bluetooth bridge containerUp
Bluetooth in rfkill listSoft blocked: no
After sudo rebootall of the above come back unaided

The reboot test is the most important of them all: it proves the system survives a power cut without intervention.

The first charge

Set the daily charge limit in the car (say 80%) — evcc decides when and how fast, the car decides how far. Plug in, and in PV mode the system waits for the surplus it needs and starts on its own. For immediate confirmation regardless of the weather, switch the loadpoint to Fast temporarily: charging should start within about 30 seconds, which validates the entire command chain in one move. Then back to PV.

12

The voltage trigger

The part no off-the-shelf system gives you.

On a very sunny day the whole street is exporting and the voltage at the feed-in point rises. Above roughly 253 V the inverter is obliged to curtail production — energy lost for good. Consuming locally in exactly that window keeps voltage in check and rescues production that would otherwise be cut.

evcc has no voltage trigger, so we add one: a service that reads phase voltages every 30 seconds and changes the loadpoint mode when thresholds are crossed.

Edge-triggered, not continuously enforcedThe script issues a single command at the moment a threshold is crossed, then stays quiet. That way you can change the mode from the interface any time without being overwritten 30 seconds later.

There are really two rules layered together. A daily window — 09:00 to 19:15 — decides whether to charge at all: outside it the loadpoint is forced off, so evcc stops commanding the car and lets it sleep. Inside it, the voltage tiers decide how: plain PV on surplus, and Min+PV (boost) while the feed-in voltage is high.

dependencies
sudo apt install -y jq bc
sudo nano /usr/local/bin/voltage-watch.sh
/usr/local/bin/voltage-watch.sh
#!/bin/bash
# Two-tier solar charging with a daily charging window, edge-triggered.
#
# Charging window: 09:00-19:15 (Pi local time). Outside it the loadpoint is
#   forced OFF, so evcc stops commanding the car and lets it sleep instead of
#   fighting the car's own schedule.
# Inside the window:
#   normal      -> PV mode   (surplus charging, low threshold)
#   V >= V_ON   -> Min+PV     (uninterrupted charging while voltage is high)
# Between transitions it commands nothing, so manual choices stick.

INVERTER="192.168.50.99"
EVCC="http://localhost:7070"
LP=1

V_ON=250.0
V_OFF=235.0
ON_DELAY=2         # 2 x 30s = 1 minute above the threshold
OFF_DELAY=10       # 10 x 30s = 5 minutes below it
INTERVAL=30

DAY_START=900      # 09:00 as HHMM, Pi local time - window opens
DAY_END=1915       # 19:15 as HHMM, Pi local time - window closes

above=0; below=0
zone="normal"      # voltage belief: normal|boost
period=""          # window belief: day|night ("" = first pass)

# current loadpoint mode from evcc (empty string on error)
lp_mode() { curl -s --max-time 5 "$EVCC/api/state" | jq -r ".loadpoints[$((LP-1))].mode // empty"; }
# set loadpoint mode
set_mode() { curl -s -X POST "$EVCC/api/loadpoints/$LP/mode/$1" >/dev/null; }

while true; do
  # which side of the daily window are we on?
  now=$((10#$(date +%H%M)))
  if [ "$now" -ge "$DAY_START" ] && [ "$now" -lt "$DAY_END" ]; then cur="day"; else cur="night"; fi

  # one authoritative read of the mode per cycle; skip the cycle if evcc is down,
  # so a transient outage can't make us miss a window edge
  mode=$(lp_mode)
  if [ -z "$mode" ]; then
    echo "$(date '+%F %T') evcc unreachable, retrying"
    sleep "$INTERVAL"; continue
  fi

  # --- daily window (edge-triggered) ---
  if [ "$cur" != "$period" ]; then
    if [ "$cur" = "night" ]; then
      # outside 09:00-19:15: stop charging, let the car sleep
      if [ "$mode" != "off" ]; then
        set_mode off; mode="off"
        echo "$(date '+%F %T') outside window (09:00-19:15) -> OFF"
      fi
      above=0; below=0; zone="normal"    # reset the boost latch for tomorrow
    else
      # window opens in the morning: resume surplus charging
      if [ "$mode" = "off" ]; then
        set_mode pv; mode="pv"
        echo "$(date '+%F %T') window open (09:00-19:15) -> PV"
      fi
    fi
    period="$cur"
  fi

  # at night, don't poll voltage or touch modes again
  if [ "$cur" = "night" ]; then
    sleep "$INTERVAL"; continue
  fi

  # --- voltage tiers (only inside the window) ---
  json=$(curl -s --max-time 10 "http://$INVERTER/solar_api/v1/GetMeterRealtimeData.cgi?Scope=Device&DeviceId=0")
  v1=$(echo "$json" | jq -r '.Body.Data.Voltage_AC_Phase_1 // 0')
  v2=$(echo "$json" | jq -r '.Body.Data.Voltage_AC_Phase_2 // 0')
  v3=$(echo "$json" | jq -r '.Body.Data.Voltage_AC_Phase_3 // 0')
  vmax=$(echo "$v1 $v2 $v3" | tr ' ' '\n' | sort -g | tail -1)

  if [ "$vmax" = "0" ]; then
    echo "$(date '+%F %T') meter read failed, skipping"
    sleep "$INTERVAL"; continue
  fi

  if (( $(echo "$vmax >= $V_ON" | bc -l) )); then
    above=$((above+1)); below=0
  elif (( $(echo "$vmax < $V_OFF" | bc -l) )); then
    below=$((below+1)); above=0
  else
    above=0; below=0
  fi

  # rising edge: normal -> boost
  if [ "$zone" = "normal" ] && [ $above -ge $ON_DELAY ]; then
    zone="boost"
    if [ "$mode" = "pv" ]; then
      set_mode minpv
      echo "$(date '+%F %T') Vmax=${vmax}V -> BOOST (Min+PV)"
    else
      echo "$(date '+%F %T') Vmax=${vmax}V -> boost zone, but mode=$mode (manual), leaving alone"
    fi
  fi

  # falling edge: boost -> normal
  if [ "$zone" = "boost" ] && [ $below -ge $OFF_DELAY ]; then
    zone="normal"
    if [ "$mode" = "minpv" ]; then
      set_mode pv
      echo "$(date '+%F %T') Vmax=${vmax}V -> normal (PV)"
    else
      echo "$(date '+%F %T') Vmax=${vmax}V -> normal zone, but mode=$mode (manual), leaving alone"
    fi
  fi

  sleep "$INTERVAL"
done
make it executable
sudo chmod +x /usr/local/bin/voltage-watch.sh
/etc/systemd/system/voltage-watch.service
[Unit]
Description=Voltage-triggered EV charging watchdog
After=network-online.target evcc.service

[Service]
ExecStart=/usr/local/bin/voltage-watch.sh
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target
start and watch live
sudo systemctl daemon-reload
sudo systemctl enable --now voltage-watch
journalctl -u voltage-watch -f
The window runs on the Pi’s clockThe 09:00 and 19:15 boundaries are read from the Pi’s local time, so the timezone must be set — sudo timedatectl set-timezone Europe/Bucharest. A fresh Raspberry Pi OS defaults to UTC, which would shift the cutoff by hours; verify with timedatectl. Daylight saving then follows on its own.
One scheduler, not twoNow that evcc owns when, turn Scheduled Charging off in the Tesla app and set only the charge limit in the car. If the car keeps its own schedule evcc can’t see it, so it keeps commanding a charge the car silently refuses — which leaves the dashboard stuck on “charging” at 0 W and the car awake all night.

What a sunny day looks like

MomentWhat happens
09:00 — window opensPV mode: charges at 2–6 A whenever surplus passes ~1.4 kW, pauses under cloud
Midday, voltage above 250 V for a minuteSwitches to Min+PV: charging no longer pauses and absorbs the full surplus
Afternoon, below 235 V for five minutesReturns to normal PV mode
19:15 — window closesLoadpoint forced off; the car stops being asked and can sleep
Any time you pick a mode manuallyThe script stands down and notes it in the log

Calibration

13

Errors hit, and fixes

All of these came up during the real build, in this order.

MessageCause and fix
Board doesn't appear on the network after flashingWrong Wi-Fi password, or no location set in Imager, leaving the radio disabled. Re-flash with the location set.
hci0: can't up device: operation not possible due to RF-killBluetooth soft-blocked. sudo rfkill unblock bluetooth, then the service from step 09 to make it permanent.
Vehicle is not in range: failed to scan for YOUR_VINThe placeholder text was left in the URL. Use the real 17-character VIN.
your public key has not been paired with the vehicleThe key request was sent but never confirmed on the car's screen. Redo the pairing, holding the card against the console for 2–3 seconds.
dial tcp connection refused on the PV meterModbus TCP isn't listening on the inverter. Enable it under Communication → Modbus; verify with nc -zv IP 502.
For Communication please log in as customerThe inverter's local password. Reset it via PIN + Solar.web, as in step 06.
Inverter stopped, right after saving settingsNormal restart of the communication side, or simply low light. It returns within a minute or two; Modbus works even when the inverter isn't producing.
Found systemd service database ...A notice, not an error. Run the check with --ignore-db.
Unit voltage-watch.service not foundThe service file hadn't been created yet. Create it, then daemon-reload.
Mode jumps back to Min+PV after you change itThe first version of the script enforced the state continuously. The version in step 12 only commands on threshold crossings.
charger out of sync: expected enabled, got disabled (and the reverse)evcc can’t switch a Tesla Wall Connector directly — every start/stop is a Bluetooth command to the car, while the enabled/disabled reading comes from the charger. When a command doesn’t land — usually a start after the car has slept — the two drift apart and won’t resync until a physical replug (or the app) wakes the car. Minimise it by pausing less: the disable.delay from step 10, a current BLE proxy, and minpv on marginal mornings. A few transient sync warnings around a genuine state change are normal.
charger logic error: current mismatch … make sure your interval is at least 30sevcc is changing the charge current faster than the car can apply and confirm it over Bluetooth, so the reported current is always a step behind the commanded one — and the loop over-corrects, making the current hunt and the grid flap. Set interval: 30s or more in /etc/evcc.yaml and restart; 45s is comfortable for BLE.
“Charged a bit, then went to sleep”The classic morning version: evcc starts on a burst of surplus, a cloud drops it below the threshold, evcc pauses, the car sleeps — and when the sun returns evcc’s start command can’t wake it, so it sits desynced until you replug. The honest tiles from step 15 show this correctly as Idle at 0 W rather than a frozen “charging”; the real fix is upstream — keep it from pausing at all with a generous disable.delay.
14

Day-to-day operation

What to do once everything works.

useful commands
journalctl -u voltage-watch -f          # follow the voltage thresholds
journalctl -u evcc -n 100 --no-pager    # recent evcc errors
docker logs -f tesla-ble-http-proxy     # traffic to the car
sudo apt update && sudo apt upgrade -y  # updates, evcc included
15

Long-term data: InfluxDB & Grafana

Where evcc's built-in statistics stop, a time-series database and a dashboard begin.

The evcc interface keeps per-session records, but not the fine grain: every phase voltage, every watt, second by second, kept for as long as you like. InfluxDB stores that stream; Grafana draws it. Both run in Docker on the same Pi, next to the Bluetooth bridge from step 08 — no extra hardware, no cloud account.

This is where step 12 becomes visible after the factWith voltage in the database you can see, hour by hour, how close the feed-in point came to the 253 V ceiling and whether the boost tier fired when it should have — the whole rationale of the voltage trigger, on a chart instead of in a log.

InfluxDB + Grafana, one compose file

Docker is already installed from step 08. Put both services in a second stack. The InfluxDB block self-initialises on first start — organisation, bucket, admin user and a fixed token — so there's no setup wizard to click through:

on the Raspberry Pi
mkdir -p ~/monitoring
nano ~/monitoring/docker-compose.yml
~/monitoring/docker-compose.yml
services:
  influxdb:
    image: influxdb:2.7
    container_name: influxdb
    restart: unless-stopped
    ports:
      - "8086:8086"
    volumes:
      - influxdb-data:/var/lib/influxdb2
      - influxdb-config:/etc/influxdb2
    environment:
      - DOCKER_INFLUXDB_INIT_MODE=setup
      - DOCKER_INFLUXDB_INIT_USERNAME=admin
      - DOCKER_INFLUXDB_INIT_PASSWORD=changeme-strong-pw
      - DOCKER_INFLUXDB_INIT_ORG=home
      - DOCKER_INFLUXDB_INIT_BUCKET=evcc
      - DOCKER_INFLUXDB_INIT_RETENTION=0
      - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-evcc-token

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - grafana-data:/var/lib/grafana
    depends_on:
      - influxdb

volumes:
  influxdb-data:
  influxdb-config:
  grafana-data:
start
cd ~/monitoring
docker compose up -d
Change two strings before you startReplace changeme-strong-pw and my-super-secret-evcc-token with your own. The token is the only credential evcc uses to write, so treat it like a password. RETENTION=0 keeps data forever — evcc's stream is tiny, but on a Pi that means SD-card writes for years, so a good card or a USB-SSD is the safer home for influxdb-data.

Telling evcc to write to InfluxDB

Add one top-level block to the configuration file — not nested under site or loadpoints:

append to /etc/evcc.yaml
influx:
  url: http://localhost:8086
  database: evcc                 # v2 quirk: this is the BUCKET name
  org: home
  token: my-super-secret-evcc-token
The one thing everyone gets wrongOn InfluxDB v2 the bucket is set through the database: key, not bucket:. The org and token must match the compose file exactly — a mismatch shows up as unauthorized access in the evcc log and nothing else. Restart with sudo systemctl restart evcc; the interface then lists InfluxDB as configured.

Grafana and the dashboard

Open http://evcc.local:3000 (first login admin / admin). Add the data source, then import the dashboard:

  1. Connections → Data sources → Add → InfluxDB. Query language Flux, URL http://influxdb:8086 (the container name resolves inside Docker's network), then the organisation home, the token, and default bucket evcc. Save & test.
  2. Dashboards → New → Import → Upload JSON file, pick evcc-grafana-dashboard.json, and select the InfluxDB source when asked.

The dashboard leads with the always-on House Voltage (Smart Meter) section, then a Charging — status, reason & live values block, then charge / PV / home power and per-phase current, and finally vehicle and battery state of charge with charged energy per session. An earlier build also carried a separate charger-voltage section, but once the always-on house voltage existed that became redundant — the charger's voltage only appears while the car is drawing current — so it was dropped; the live charging voltage still shows as a tile inside the Charging block.

Grafana 13 changed the dashboard schemaOn Grafana 13.x the model is the new elements + layout form, where annotations is an array rather than { list: [...] }. An older-schema file still imports fine through Upload JSON file (Grafana migrates it on the way in), but pasting one into Dashboard settings → JSON Model on 13.x throws “expected array” — paste a new-schema export there instead.

The full dashboard JSON

The complete dashboard, in the Grafana 13.x schema, wired to a DS_INFLUXDB datasource variable. Paste it into Dashboard settings → JSON Model, or drop it in a file and use Upload JSON file — either way, pick your InfluxDB source afterwards.

Show the full dashboard JSON (Grafana 13.x) — House Voltage · Charging status/battery/reason · Power · SoC
evcc — Grid Voltage & Energy · dashboard model
{
  "annotations": [
    {
      "kind": "AnnotationQuery",
      "spec": {
        "builtIn": true,
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "query": {
          "datasource": {
            "name": "-- Grafana --"
          },
          "group": "grafana",
          "kind": "DataQuery",
          "spec": {},
          "version": "v0"
        }
      }
    }
  ],
  "cursorSync": "Crosshair",
  "editable": true,
  "elements": {
    "panel-12": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: -2m)\n  |> filter(fn: (r) => r._measurement == \"chargePower\" and r._field == \"value\")\n  |> last()\n  |> map(fn: (r) => ({ r with _value: if r._value > 100.0 then 1.0 else 0.0 }))"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 12,
        "links": [],
        "title": "Charging now?",
        "vizConfig": {
          "group": "stat",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "color": {
                  "mode": "thresholds"
                },
                "mappings": [
                  {
                    "options": {
                      "0": {
                        "index": 0,
                        "text": "Idle"
                      },
                      "1": {
                        "index": 1,
                        "text": "CHARGING"
                      }
                    },
                    "type": "value"
                  }
                ],
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "#5A6370",
                      "value": 0
                    },
                    {
                      "color": "green",
                      "value": 1
                    }
                  ]
                }
              },
              "overrides": []
            },
            "options": {
              "colorMode": "background",
              "graphMode": "none",
              "justifyMode": "auto",
              "orientation": "auto",
              "percentChangeColorMode": "standard",
              "reduceOptions": {
                "calcs": [
                  "lastNotNull"
                ],
                "fields": "",
                "values": false
              },
              "showPercentChange": false,
              "textMode": "value",
              "wideLayout": true
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-13": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: -5m)\n  |> filter(fn: (r) => r._measurement == \"chargeState\" and r._field == \"mode\")\n  |> last()"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 13,
        "links": [],
        "title": "Reason (why)",
        "vizConfig": {
          "group": "stat",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "color": {
                  "mode": "thresholds"
                },
                "mappings": [
                  {
                    "options": {
                      "0": {
                        "index": 0,
                        "text": "Not charging"
                      },
                      "1": {
                        "index": 1,
                        "text": "PV \u00b7 surplus"
                      },
                      "2": {
                        "index": 2,
                        "text": "Boost \u00b7 high V"
                      },
                      "3": {
                        "index": 3,
                        "text": "Fast \u00b7 manual"
                      }
                    },
                    "type": "value"
                  },
                  {
                    "options": {
                      "-1": {
                        "index": 4,
                        "text": "\u2014"
                      }
                    },
                    "type": "value"
                  }
                ],
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "#5A6370",
                      "value": 0
                    },
                    {
                      "color": "green",
                      "value": 1
                    },
                    {
                      "color": "orange",
                      "value": 2
                    },
                    {
                      "color": "blue",
                      "value": 3
                    }
                  ]
                }
              },
              "overrides": []
            },
            "options": {
              "colorMode": "background",
              "graphMode": "none",
              "justifyMode": "auto",
              "orientation": "auto",
              "percentChangeColorMode": "standard",
              "reduceOptions": {
                "calcs": [
                  "lastNotNull"
                ],
                "fields": "",
                "values": false
              },
              "showPercentChange": false,
              "textMode": "value",
              "wideLayout": true
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-14": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"chargePower\")\n  |> filter(fn: (r) => r._field == \"value\")\n  |> last()"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 14,
        "links": [],
        "title": "Power",
        "vizConfig": {
          "group": "stat",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "decimals": 2,
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "green",
                      "value": 0
                    }
                  ]
                },
                "unit": "watt"
              },
              "overrides": []
            },
            "options": {
              "colorMode": "value",
              "graphMode": "none",
              "justifyMode": "auto",
              "orientation": "auto",
              "percentChangeColorMode": "standard",
              "reduceOptions": {
                "calcs": [
                  "lastNotNull"
                ],
                "fields": "",
                "values": false
              },
              "showPercentChange": false,
              "textMode": "value",
              "wideLayout": true
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-15": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"chargeCurrents\")\n  |> filter(fn: (r) => r._field == \"l1\" or r._field == \"l2\" or r._field == \"l3\")\n  |> last()"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 15,
        "links": [],
        "title": "Current \u2014 per phase",
        "vizConfig": {
          "group": "stat",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "decimals": 1,
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "green",
                      "value": 0
                    }
                  ]
                },
                "unit": "amp"
              },
              "overrides": []
            },
            "options": {
              "colorMode": "value",
              "graphMode": "none",
              "justifyMode": "auto",
              "orientation": "auto",
              "percentChangeColorMode": "standard",
              "reduceOptions": {
                "calcs": [
                  "lastNotNull"
                ],
                "fields": "",
                "values": false
              },
              "showPercentChange": false,
              "textMode": "auto",
              "wideLayout": true
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-16": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"chargeVoltages\")\n  |> filter(fn: (r) => r._field == \"l1\" or r._field == \"l2\" or r._field == \"l3\")\n  |> filter(fn: (r) => r._value > 0)\n  |> last()"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 16,
        "links": [],
        "title": "Voltage \u2014 per phase",
        "vizConfig": {
          "group": "stat",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "decimals": 1,
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "green",
                      "value": 0
                    }
                  ]
                },
                "unit": "volt"
              },
              "overrides": []
            },
            "options": {
              "colorMode": "value",
              "graphMode": "none",
              "justifyMode": "auto",
              "orientation": "auto",
              "percentChangeColorMode": "standard",
              "reduceOptions": {
                "calcs": [
                  "lastNotNull"
                ],
                "fields": "",
                "values": false
              },
              "showPercentChange": false,
              "textMode": "auto",
              "wideLayout": true
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-17": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"chargeState\")\n  |> filter(fn: (r) => r._field == \"reason\")\n  |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 17,
        "links": [],
        "title": "When & why the Tesla charged",
        "vizConfig": {
          "group": "state-timeline",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "color": {
                  "mode": "thresholds"
                },
                "custom": {
                  "axisPlacement": "auto",
                  "fillOpacity": 85,
                  "hideFrom": {
                    "legend": false,
                    "tooltip": false,
                    "viz": false
                  },
                  "insertNulls": false,
                  "lineWidth": 0,
                  "spanNulls": false
                },
                "mappings": [
                  {
                    "options": {
                      "0": {
                        "index": 0,
                        "text": "Not charging"
                      },
                      "1": {
                        "index": 1,
                        "text": "PV \u00b7 surplus"
                      },
                      "2": {
                        "index": 2,
                        "text": "Boost \u00b7 high V"
                      },
                      "3": {
                        "index": 3,
                        "text": "Fast \u00b7 manual"
                      }
                    },
                    "type": "value"
                  },
                  {
                    "options": {
                      "-1": {
                        "index": 4,
                        "text": "\u2014"
                      }
                    },
                    "type": "value"
                  }
                ],
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "#5A6370",
                      "value": 0
                    },
                    {
                      "color": "green",
                      "value": 1
                    },
                    {
                      "color": "orange",
                      "value": 2
                    },
                    {
                      "color": "blue",
                      "value": 3
                    }
                  ]
                }
              },
              "overrides": []
            },
            "options": {
              "alignValue": "center",
              "annotations": {
                "clustering": -1,
                "multiLane": false
              },
              "legend": {
                "displayMode": "list",
                "overflow": "ellipsis",
                "placement": "bottom",
                "showLegend": true
              },
              "mergeValues": true,
              "rowHeight": 0.9,
              "showValue": "auto",
              "tooltip": {
                "hideZeros": false,
                "mode": "single",
                "sort": "none"
              }
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-19": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"chargePower\" or r._measurement == \"pvPower\" or r._measurement == \"homePower\")\n  |> filter(fn: (r) => r._field == \"value\")\n  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 19,
        "links": [],
        "title": "Power \u2014 charge / PV / home",
        "vizConfig": {
          "group": "timeseries",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "color": {
                  "mode": "palette-classic"
                },
                "custom": {
                  "axisBorderShow": false,
                  "axisCenteredZero": false,
                  "axisColorMode": "text",
                  "axisLabel": "",
                  "axisPlacement": "auto",
                  "barAlignment": 0,
                  "barWidthFactor": 0.6,
                  "drawStyle": "line",
                  "fillOpacity": 10,
                  "gradientMode": "none",
                  "hideFrom": {
                    "legend": false,
                    "tooltip": false,
                    "viz": false
                  },
                  "insertNulls": false,
                  "lineInterpolation": "linear",
                  "lineWidth": 2,
                  "pointSize": 5,
                  "scaleDistribution": {
                    "type": "linear"
                  },
                  "showPoints": "never",
                  "showValues": false,
                  "spanNulls": true,
                  "stacking": {
                    "group": "A",
                    "mode": "none"
                  },
                  "thresholdsStyle": {
                    "mode": "off"
                  }
                },
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "green",
                      "value": 0
                    },
                    {
                      "color": "red",
                      "value": 80
                    }
                  ]
                },
                "unit": "watt"
              },
              "overrides": []
            },
            "options": {
              "annotations": {
                "clustering": -1,
                "multiLane": false
              },
              "legend": {
                "calcs": [
                  "mean",
                  "max"
                ],
                "displayMode": "table",
                "enableFacetedFilter": false,
                "overflow": "ellipsis",
                "placement": "bottom",
                "showLegend": true
              },
              "tooltip": {
                "hideZeros": false,
                "mode": "multi",
                "sort": "none"
              }
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-2": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"gridVoltage\")\n  |> filter(fn: (r) => r._field == \"l1\")\n  |> last()"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 2,
        "links": [],
        "title": "House L1 (now)",
        "vizConfig": {
          "group": "stat",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "decimals": 1,
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "red",
                      "value": 0
                    },
                    {
                      "color": "orange",
                      "value": 207
                    },
                    {
                      "color": "green",
                      "value": 216
                    },
                    {
                      "color": "orange",
                      "value": 244
                    },
                    {
                      "color": "red",
                      "value": 255
                    }
                  ]
                },
                "unit": "volt"
              },
              "overrides": []
            },
            "options": {
              "colorMode": "value",
              "graphMode": "area",
              "justifyMode": "auto",
              "orientation": "auto",
              "percentChangeColorMode": "standard",
              "reduceOptions": {
                "calcs": [
                  "lastNotNull"
                ],
                "fields": "",
                "values": false
              },
              "showPercentChange": false,
              "textMode": "auto",
              "wideLayout": true
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-20": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"chargeCurrents\")\n  |> filter(fn: (r) => r._field == \"l1\" or r._field == \"l2\" or r._field == \"l3\")\n  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 20,
        "links": [],
        "title": "Charge current per phase (L1 / L2 / L3)",
        "vizConfig": {
          "group": "timeseries",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "color": {
                  "mode": "palette-classic"
                },
                "custom": {
                  "axisBorderShow": false,
                  "axisCenteredZero": false,
                  "axisColorMode": "text",
                  "axisLabel": "",
                  "axisPlacement": "auto",
                  "barAlignment": 0,
                  "barWidthFactor": 0.6,
                  "drawStyle": "line",
                  "fillOpacity": 5,
                  "gradientMode": "none",
                  "hideFrom": {
                    "legend": false,
                    "tooltip": false,
                    "viz": false
                  },
                  "insertNulls": false,
                  "lineInterpolation": "linear",
                  "lineWidth": 2,
                  "pointSize": 5,
                  "scaleDistribution": {
                    "type": "linear"
                  },
                  "showPoints": "never",
                  "showValues": false,
                  "spanNulls": true,
                  "stacking": {
                    "group": "A",
                    "mode": "none"
                  },
                  "thresholdsStyle": {
                    "mode": "off"
                  }
                },
                "decimals": 1,
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "green",
                      "value": 0
                    },
                    {
                      "color": "red",
                      "value": 80
                    }
                  ]
                },
                "unit": "amp"
              },
              "overrides": []
            },
            "options": {
              "annotations": {
                "clustering": -1,
                "multiLane": false
              },
              "legend": {
                "calcs": [
                  "mean",
                  "max"
                ],
                "displayMode": "table",
                "enableFacetedFilter": false,
                "overflow": "ellipsis",
                "placement": "bottom",
                "showLegend": true
              },
              "tooltip": {
                "hideZeros": false,
                "mode": "multi",
                "sort": "none"
              }
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-22": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"vehicleSoc\" or r._measurement == \"batterySoc\")\n  |> filter(fn: (r) => r._field == \"value\")\n  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 22,
        "links": [],
        "title": "State of charge \u2014 vehicle / battery",
        "vizConfig": {
          "group": "timeseries",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "color": {
                  "mode": "palette-classic"
                },
                "custom": {
                  "axisBorderShow": false,
                  "axisCenteredZero": false,
                  "axisColorMode": "text",
                  "axisLabel": "",
                  "axisPlacement": "auto",
                  "barAlignment": 0,
                  "barWidthFactor": 0.6,
                  "drawStyle": "line",
                  "fillOpacity": 10,
                  "gradientMode": "none",
                  "hideFrom": {
                    "legend": false,
                    "tooltip": false,
                    "viz": false
                  },
                  "insertNulls": false,
                  "lineInterpolation": "stepAfter",
                  "lineWidth": 2,
                  "pointSize": 5,
                  "scaleDistribution": {
                    "type": "linear"
                  },
                  "showPoints": "never",
                  "showValues": false,
                  "spanNulls": true,
                  "stacking": {
                    "group": "A",
                    "mode": "none"
                  },
                  "thresholdsStyle": {
                    "mode": "off"
                  }
                },
                "max": 100,
                "min": 0,
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "green",
                      "value": 0
                    },
                    {
                      "color": "red",
                      "value": 80
                    }
                  ]
                },
                "unit": "percent"
              },
              "overrides": []
            },
            "options": {
              "annotations": {
                "clustering": -1,
                "multiLane": false
              },
              "legend": {
                "calcs": [
                  "last"
                ],
                "displayMode": "table",
                "enableFacetedFilter": false,
                "overflow": "ellipsis",
                "placement": "bottom",
                "showLegend": true
              },
              "tooltip": {
                "hideZeros": false,
                "mode": "multi",
                "sort": "none"
              }
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-23": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"chargedEnergy\")\n  |> filter(fn: (r) => r._field == \"value\")\n  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 23,
        "links": [],
        "title": "Charged energy per session",
        "vizConfig": {
          "group": "timeseries",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "color": {
                  "mode": "palette-classic"
                },
                "custom": {
                  "axisBorderShow": false,
                  "axisCenteredZero": false,
                  "axisColorMode": "text",
                  "axisLabel": "",
                  "axisPlacement": "auto",
                  "barAlignment": 0,
                  "barWidthFactor": 0.6,
                  "drawStyle": "line",
                  "fillOpacity": 15,
                  "gradientMode": "none",
                  "hideFrom": {
                    "legend": false,
                    "tooltip": false,
                    "viz": false
                  },
                  "insertNulls": false,
                  "lineInterpolation": "linear",
                  "lineWidth": 2,
                  "pointSize": 5,
                  "scaleDistribution": {
                    "type": "linear"
                  },
                  "showPoints": "never",
                  "showValues": false,
                  "spanNulls": true,
                  "stacking": {
                    "group": "A",
                    "mode": "none"
                  },
                  "thresholdsStyle": {
                    "mode": "off"
                  }
                },
                "decimals": 2,
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "green",
                      "value": 0
                    },
                    {
                      "color": "red",
                      "value": 80
                    }
                  ]
                },
                "unit": "kwatth"
              },
              "overrides": []
            },
            "options": {
              "annotations": {
                "clustering": -1,
                "multiLane": false
              },
              "legend": {
                "calcs": [
                  "max"
                ],
                "displayMode": "table",
                "enableFacetedFilter": false,
                "overflow": "ellipsis",
                "placement": "bottom",
                "showLegend": true
              },
              "tooltip": {
                "hideZeros": false,
                "mode": "multi",
                "sort": "none"
              }
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-24": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"vehicleSoc\" and r._field == \"value\")\n  |> filter(fn: (r) => r._value > 0)\n  |> last()"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 24,
        "links": [],
        "title": "Battery",
        "vizConfig": {
          "group": "stat",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "color": {
                  "mode": "thresholds"
                },
                "decimals": 0,
                "max": 100,
                "min": 0,
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "red",
                      "value": 0
                    },
                    {
                      "color": "orange",
                      "value": 20
                    },
                    {
                      "color": "green",
                      "value": 50
                    }
                  ]
                },
                "unit": "percent"
              },
              "overrides": []
            },
            "options": {
              "colorMode": "value",
              "graphMode": "area",
              "justifyMode": "auto",
              "orientation": "auto",
              "percentChangeColorMode": "standard",
              "reduceOptions": {
                "calcs": [
                  "lastNotNull"
                ],
                "fields": "",
                "values": false
              },
              "showPercentChange": false,
              "textMode": "auto",
              "wideLayout": true
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-3": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"gridVoltage\")\n  |> filter(fn: (r) => r._field == \"l2\")\n  |> last()"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 3,
        "links": [],
        "title": "House L2 (now)",
        "vizConfig": {
          "group": "stat",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "decimals": 1,
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "red",
                      "value": 0
                    },
                    {
                      "color": "orange",
                      "value": 207
                    },
                    {
                      "color": "green",
                      "value": 216
                    },
                    {
                      "color": "orange",
                      "value": 244
                    },
                    {
                      "color": "red",
                      "value": 255
                    }
                  ]
                },
                "unit": "volt"
              },
              "overrides": []
            },
            "options": {
              "colorMode": "value",
              "graphMode": "area",
              "justifyMode": "auto",
              "orientation": "auto",
              "percentChangeColorMode": "standard",
              "reduceOptions": {
                "calcs": [
                  "lastNotNull"
                ],
                "fields": "",
                "values": false
              },
              "showPercentChange": false,
              "textMode": "auto",
              "wideLayout": true
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-4": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"gridVoltage\")\n  |> filter(fn: (r) => r._field == \"l3\")\n  |> last()"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 4,
        "links": [],
        "title": "House L3 (now)",
        "vizConfig": {
          "group": "stat",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "decimals": 1,
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "red",
                      "value": 0
                    },
                    {
                      "color": "orange",
                      "value": 207
                    },
                    {
                      "color": "green",
                      "value": 216
                    },
                    {
                      "color": "orange",
                      "value": 244
                    },
                    {
                      "color": "red",
                      "value": 255
                    }
                  ]
                },
                "unit": "volt"
              },
              "overrides": []
            },
            "options": {
              "colorMode": "value",
              "graphMode": "area",
              "justifyMode": "auto",
              "orientation": "auto",
              "percentChangeColorMode": "standard",
              "reduceOptions": {
                "calcs": [
                  "lastNotNull"
                ],
                "fields": "",
                "values": false
              },
              "showPercentChange": false,
              "textMode": "auto",
              "wideLayout": true
            }
          },
          "version": "13.1.1"
        }
      }
    },
    "panel-5": {
      "kind": "Panel",
      "spec": {
        "data": {
          "kind": "QueryGroup",
          "spec": {
            "queries": [
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"gridVoltage\")\n  |> filter(fn: (r) => r._field == \"l1\" or r._field == \"l2\" or r._field == \"l3\")\n  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)"
                    },
                    "version": "v0"
                  },
                  "refId": "A"
                }
              },
              {
                "kind": "PanelQuery",
                "spec": {
                  "hidden": false,
                  "query": {
                    "datasource": {
                      "name": "${DS_INFLUXDB}"
                    },
                    "group": "influxdb",
                    "kind": "DataQuery",
                    "spec": {
                      "query": "import \"array\"\narray.from(rows: [\n  {_time: v.timeRangeStart, _field: \"limit\", _value: 255.0},\n  {_time: v.timeRangeStop, _field: \"limit\", _value: 255.0}\n])"
                    },
                    "version": "v0"
                  },
                  "refId": "B"
                }
              }
            ],
            "queryOptions": {},
            "transformations": []
          }
        },
        "description": "",
        "id": 5,
        "links": [],
        "title": "House voltage per phase \u2014 Smart Meter (L1 / L2 / L3)",
        "vizConfig": {
          "group": "timeseries",
          "kind": "VizConfig",
          "spec": {
            "fieldConfig": {
              "defaults": {
                "color": {
                  "mode": "palette-classic"
                },
                "custom": {
                  "axisBorderShow": false,
                  "axisCenteredZero": false,
                  "axisColorMode": "text",
                  "axisLabel": "",
                  "axisPlacement": "auto",
                  "barAlignment": 0,
                  "barWidthFactor": 0.6,
                  "drawStyle": "line",
                  "fillOpacity": 5,
                  "gradientMode": "none",
                  "hideFrom": {
                    "legend": false,
                    "tooltip": false,
                    "viz": false
                  },
                  "insertNulls": false,
                  "lineInterpolation": "linear",
                  "lineWidth": 2,
                  "pointSize": 5,
                  "scaleDistribution": {
                    "type": "linear"
                  },
                  "showPoints": "never",
                  "showValues": false,
                  "spanNulls": true,
                  "stacking": {
                    "group": "A",
                    "mode": "none"
                  },
                  "thresholdsStyle": {
                    "mode": "area"
                  }
                },
                "decimals": 1,
                "max": 260,
                "min": 200,
                "thresholds": {
                  "mode": "absolute",
                  "steps": [
                    {
                      "color": "transparent",
                      "value": 0
                    },
                    {
                      "color": "red",
                      "value": 255
                    }
                  ]
                },
                "unit": "volt"
              },
              "overrides": [
                {
                  "matcher": {
                    "id": "byFrameRefID",
                    "options": "B"
                  },
                  "properties": [
                    {
                      "id": "color",
                      "value": {
                        "fixedColor": "red",
                        "mode": "fixed"
                      }
                    },
                    {
                      "id": "custom.lineWidth",
                      "value": 4
                    },
                    {
                      "id": "custom.fillOpacity",
                      "value": 0
                    },
                    {
                      "id": "custom.lineStyle",
                      "value": {
                        "fill": "solid"
                      }
                    },
                    {
                      "id": "custom.pointSize",
                      "value": 0
                    },
                    {
                      "id": "custom.thresholdsStyle",
                      "value": {
                        "mode": "off"
                      }
                    },
                    {
                      "id": "displayName",
                      "value": "Derate limit 255 V"
                    }
                  ]
                }
              ]
            },
            "options": {
              "annotations": {
                "clustering": -1,
                "multiLane": false
              },
              "legend": {
                "calcs": [
                  "min",
                  "mean",
                  "max"
                ],
                "displayMode": "table",
                "enableFacetedFilter": false,
                "overflow": "ellipsis",
                "placement": "bottom",
                "showLegend": true
              },
              "tooltip": {
                "hideZeros": false,
                "mode": "multi",
                "sort": "none"
              }
            }
          },
          "version": "13.1.1"
        }
      }
    }
  },
  "layout": {
    "kind": "RowsLayout",
    "spec": {
      "rows": [
        {
          "kind": "RowsLayoutRow",
          "spec": {
            "collapse": false,
            "layout": {
              "kind": "GridLayout",
              "spec": {
                "items": [
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-2"
                      },
                      "height": 5,
                      "width": 8,
                      "x": 0,
                      "y": 0
                    }
                  },
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-3"
                      },
                      "height": 5,
                      "width": 8,
                      "x": 8,
                      "y": 0
                    }
                  },
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-4"
                      },
                      "height": 5,
                      "width": 8,
                      "x": 16,
                      "y": 0
                    }
                  },
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-5"
                      },
                      "height": 9,
                      "width": 24,
                      "x": 0,
                      "y": 5
                    }
                  }
                ]
              }
            },
            "title": "House Voltage \u2014 Smart Meter (always on, at the feed-in point)"
          }
        },
        {
          "kind": "RowsLayoutRow",
          "spec": {
            "collapse": false,
            "layout": {
              "kind": "GridLayout",
              "spec": {
                "items": [
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-12"
                      },
                      "height": 5,
                      "width": 4,
                      "x": 0,
                      "y": 0
                    }
                  },
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-24"
                      },
                      "height": 5,
                      "width": 4,
                      "x": 4,
                      "y": 0
                    }
                  },
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-13"
                      },
                      "height": 5,
                      "width": 4,
                      "x": 8,
                      "y": 0
                    }
                  },
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-14"
                      },
                      "height": 5,
                      "width": 4,
                      "x": 12,
                      "y": 0
                    }
                  },
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-15"
                      },
                      "height": 5,
                      "width": 4,
                      "x": 16,
                      "y": 0
                    }
                  },
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-16"
                      },
                      "height": 5,
                      "width": 4,
                      "x": 20,
                      "y": 0
                    }
                  },
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-17"
                      },
                      "height": 6,
                      "width": 24,
                      "x": 0,
                      "y": 5
                    }
                  }
                ]
              }
            },
            "title": "Charging \u2014 status, reason & live values"
          }
        },
        {
          "kind": "RowsLayoutRow",
          "spec": {
            "collapse": false,
            "layout": {
              "kind": "GridLayout",
              "spec": {
                "items": [
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-19"
                      },
                      "height": 9,
                      "width": 12,
                      "x": 0,
                      "y": 0
                    }
                  },
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-20"
                      },
                      "height": 9,
                      "width": 12,
                      "x": 12,
                      "y": 0
                    }
                  }
                ]
              }
            },
            "title": "Power & Current"
          }
        },
        {
          "kind": "RowsLayoutRow",
          "spec": {
            "collapse": false,
            "layout": {
              "kind": "GridLayout",
              "spec": {
                "items": [
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-22"
                      },
                      "height": 9,
                      "width": 12,
                      "x": 0,
                      "y": 0
                    }
                  },
                  {
                    "kind": "GridLayoutItem",
                    "spec": {
                      "element": {
                        "kind": "ElementReference",
                        "name": "panel-23"
                      },
                      "height": 9,
                      "width": 12,
                      "x": 12,
                      "y": 0
                    }
                  }
                ]
              }
            },
            "title": "State of Charge & Energy"
          }
        }
      ]
    }
  },
  "links": [],
  "liveNow": false,
  "preload": false,
  "tags": [
    "evcc",
    "influxdb",
    "energy"
  ],
  "timeSettings": {
    "autoRefresh": "30s",
    "autoRefreshIntervals": [
      "5s",
      "10s",
      "30s",
      "1m",
      "5m",
      "15m",
      "30m",
      "1h",
      "2h",
      "1d"
    ],
    "fiscalYearStartMonth": 0,
    "from": "now-24h",
    "hideTimepicker": false,
    "timezone": "browser",
    "to": "now"
  },
  "title": "evcc \u2014 Grid Voltage & Energy",
  "variables": [
    {
      "kind": "DatasourceVariable",
      "spec": {
        "allowCustomValue": true,
        "current": {
          "text": "influxdb",
          "value": "aft2bbdtzo0lcd"
        },
        "hide": "dontHide",
        "includeAll": false,
        "label": "InfluxDB",
        "multi": false,
        "name": "DS_INFLUXDB",
        "options": [],
        "pluginId": "influxdb",
        "refresh": "onDashboardLoad",
        "regex": "",
        "skipUrlSync": false
      }
    }
  ]
}
evcc does not store the meter's voltageIt writes the grid meter's per-phase current and power to InfluxDB, but not its voltage — and chargeVoltages, the one voltage it does store, is measured at the Wall Connector and only carries data while the car is drawing current (the flat stretch on the left of the chart before charging starts). The always-on house voltage the voltage-watch service reads in step 12 is used for switching and then discarded. To keep a permanent record of it, a second tiny logger writes it in.

Always-on house voltage: the Smart Meter logger

This is the voltage that matters for the whole boost story — the level at the feed-in point, climbing through the middle of a sunny day whether or not the car is plugged in. The logger polls the same Solar API endpoint as voltage-watch every 15 seconds and writes a gridVoltage measurement (fields l1/l2/l3) straight into the evcc bucket.

/usr/local/bin/grid-voltage-logger.sh
#!/bin/bash
# Always-on: logs the Fronius Smart Meter's phase voltages into InfluxDB,
# whether the car is charging or not. Same Solar API as voltage-watch.
INVERTER="192.168.50.99"
INFLUX="http://localhost:8086"
ORG="home"; BUCKET="evcc"
TOKEN="my-super-secret-evcc-token"    # must match the compose file
INTERVAL=15

while true; do
  json=$(curl -s --max-time 10 \
    "http://$INVERTER/solar_api/v1/GetMeterRealtimeData.cgi?Scope=Device&DeviceId=0")
  v1=$(echo "$json" | jq -r '.Body.Data.Voltage_AC_Phase_1 // empty')
  v2=$(echo "$json" | jq -r '.Body.Data.Voltage_AC_Phase_2 // empty')
  v3=$(echo "$json" | jq -r '.Body.Data.Voltage_AC_Phase_3 // empty')
  if [ -n "$v1" ] && [ -n "$v2" ] && [ -n "$v3" ]; then
    curl -s --max-time 5 -XPOST \
      "$INFLUX/api/v2/write?org=$ORG&bucket=$BUCKET&precision=s" \
      -H "Authorization: Token $TOKEN" \
      --data-binary "gridVoltage l1=$v1,l2=$v2,l3=$v3" >/dev/null
  fi
  sleep $INTERVAL
done
/etc/systemd/system/grid-voltage-logger.service
[Unit]
Description=Log Fronius Smart Meter voltages to InfluxDB
After=network-online.target evcc.service
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/grid-voltage-logger.sh
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target
make executable and start
sudo chmod +x /usr/local/bin/grid-voltage-logger.sh
sudo systemctl daemon-reload
sudo systemctl enable --now grid-voltage-logger
journalctl -u grid-voltage-logger -f

With that running, the dashboard's lead block — House Voltage (Smart Meter) — draws continuously, day and night, carrying the thick 255 V red line and the danger-zone highlight. This is the trace that shows the midday climb the boost tier exists to answer, plugged in or not.

The 255 V line is drawn as its own seriesGrafana's built-in threshold lines are always thin, so the dashboard adds a constant reference series at 255 V for a genuinely thick line, plus a translucent red wash over the 255–260 V band. If that line ever goes missing after an import, open the panel and make sure the array/constant query has the InfluxDB data source selected.

To confirm data is landing before building anything, query the bucket directly — in Grafana's Explore tab, or from the Pi:

on the Raspberry Pi
docker exec influxdb influx query \
  'from(bucket:"evcc") |> range(start:-15m)' \
  --org home --token my-super-secret-evcc-token

Rows of chargePower, pvPower, batterySoc and the rest mean the loop is closed: evcc measures, InfluxDB remembers, Grafana shows.

Why it charged: the mode logger

The live values — amps, volts, kilowatts — are already in the bucket. What isn't is the reason the car was charging: evcc knows its own mode (PV surplus, Min+PV boost, fast) but, like the meter voltage, never writes it down. A second small logger reads evcc's API and records it, so the dashboard can colour the day by why charging happened — and, crucially, show the step-12 voltage trigger firing as an orange band.

It writes a chargeState measurement with three fields: mode (0 off / 1 pv / 2 minpv / 3 now), charging (0/1), and reason — the mode code while actually charging, otherwise 0. That single reason signal drives a state-timeline: grey when idle, green on surplus, orange on the high-voltage boost, blue on a manual fast charge.

/usr/local/bin/charge-state-logger.sh
#!/bin/bash
# Logs the loadpoint's mode + charging state, so the dashboard shows
# WHEN the Tesla charged and WHY. Reads evcc's own API.
EVCC="http://localhost:7070"
INFLUX="http://localhost:8086"
ORG="home"; BUCKET="evcc"
TOKEN="my-super-secret-evcc-token"    # must match the compose file
LP=0                                  # loadpoint index (0 = first)
INTERVAL=10

while true; do
  state=$(curl -s --max-time 8 "$EVCC/api/state")
  mode=$(echo "$state" | jq -r ".loadpoints[$LP].mode // empty")
  charging=$(echo "$state" | jq -r ".loadpoints[$LP].charging // empty")
  case "$mode" in
    off) m=0 ;; pv) m=1 ;; minpv) m=2 ;; now) m=3 ;; *) m=-1 ;;
  esac
  if [ "$charging" = "true" ]; then c=1; else c=0; fi
  if [ "$c" = "1" ] && [ "$m" -ge 0 ]; then r=$m; else r=0; fi
  if [ -n "$mode" ]; then
    curl -s --max-time 5 -XPOST \
      "$INFLUX/api/v2/write?org=$ORG&bucket=$BUCKET&precision=s" \
      -H "Authorization: Token $TOKEN" \
      --data-binary "chargeState mode=$m,charging=$c,reason=$r" >/dev/null
  fi
  sleep $INTERVAL
done
/etc/systemd/system/charge-state-logger.service
[Unit]
Description=Log evcc loadpoint mode and charging state to InfluxDB
After=network-online.target evcc.service
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/charge-state-logger.sh
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target
make executable and start
sudo chmod +x /usr/local/bin/charge-state-logger.sh
sudo systemctl daemon-reload
sudo systemctl enable --now charge-state-logger

The Charging — status, reason & live values section then lights up: a row of six equal tiles — a Charging tile that flips to CHARGING in green, a Battery tile showing the Tesla's state of charge (coloured red / orange / green by level), a Reason tile reading “PV · surplus”, “Boost · high V” or “Fast · manual”, then live kW, A and V — and, underneath, the state-timeline painting every charging window in the colour of the logic that drove it.

Make the tiles honest — power, not intentevcc’s charging flag means “the loadpoint is enabled and commanding a charge”, which is not the same as current actually flowing — so the Charging and Reason tiles are keyed off real power instead. The Charging query reduces chargePower to 1 above 100 W and 0 below (so 300 mA of standby reads Idle); the Reason query reads the loadpoint mode directly (1 PV / 2 Boost / 3 Fast / 0 off), which the logger records every cycle regardless of the charging flag — so it stays truthful even when evcc drifts out of sync (step 13). An earlier version tried to pivot mode against chargePower, but the two measurements carry different tags and the merge returned no rows (“No data”); reading mode alone is simpler and robust. It does need the logger to write a mode field — older builds only wrote reason, so add mode=$m to its InfluxDB write line. The Battery tile drops vehicleSoc == 0 and keeps the last non-zero reading, so it holds the real charge level while the car sleeps and evcc can’t reach it over Bluetooth, instead of a red 0 %.
If a tile freezes, check its feederCharging and Reason are fed by the charge-state-logger service, while power, current, voltage and PV come straight from evcc. A tile stuck on an old value while the graphs stay live means that logger has stopped — sudo systemctl restart charge-state-logger. Confirm all three helpers are enabled so they return after a power cut: systemctl is-enabled voltage-watch charge-state-logger grid-voltage-logger.
16

Fronius-style flow in Grafana

The radial "Current Power" widget from Solar.web, rebuilt on your own data — and improved, because the Tesla gets its own node.

Everything the Fronius Current Power circle shows — PV production, grid exchange and house draw — is already in the evcc bucket, so the animated flow can be rebuilt in Grafana without touching the inverter. The Solar Flow panel plugin (lehmannch-flow-panel) draws a central hub with particle-style flows out to Solar, Grid and Load, plus up to three extra nodes — one of which becomes the Tesla, something the real Fronius view can't show.

Installing the plugin — offline

This build keeps nothing external in the command path, and the Pi sits on the isolated IoT segment (step 02), so the Grafana container can't reach grafana.com. That rules out the usual online routes: grafana-cli plugins install and the GF_INSTALL_PLUGINS environment variable both download at run time and simply time out here. Instead, side-load the plugin — fetch the signed zip on a machine that does have internet, carry it across, and drop it into Grafana's plugin folder inside the grafana-data volume.

on a machine with internet
curl -L -o solar-flow.zip \
  "https://grafana.com/api/plugins/lehmannch-flow-panel/versions/latest/download"
copy solar-flow.zip to the Pi, then on the Pi
unzip solar-flow.zip                         # -> folder: lehmannch-flow-panel/
docker cp lehmannch-flow-panel grafana:/var/lib/grafana/plugins/
docker exec -u root grafana chown -R grafana:grafana /var/lib/grafana/plugins
docker restart grafana

Confirm the folder landed — it must contain a plugin.json:

verify
docker exec grafana ls /var/lib/grafana/plugins | grep -i lehmann
No unsigned-plugin flag neededThe panel is signed in the Grafana catalogue, so it loads straight from the plugins folder — no allow_loading_unsigned_plugins required. Because the files live in the grafana-data volume they survive restarts and even a docker compose up -d recreate; only docker compose down -v would wipe them.

One query per node

The panel picks each node by field name, so every query renames its value to a distinct label. All read the latest point (last()) over the dashboard's time range:

panel query A — Solar
from(bucket: "evcc")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "pvPower" and r._field == "value")
  |> last()
  |> keep(columns: ["_time", "_value"])
  |> rename(columns: {_value: "solar"})

The other four are identical but for the measurement and the alias:

NodeQuery_measurementrename value to
SolarApvPowersolar
GridBgridPowergrid
Load (house)ChomePowerload
Tesla (power)DchargePowerev
Tesla (SoC)EvehicleSocsoc

Wiring and styling the panel

Map the field names to nodes, then tune the look. These are the exact settings behind the panel in this build:

OptionValueWhy
Solar / Grid / Loadsolar / grid / loadthe three core nodes
Additional sources1 → value ev, SoC socthe Tesla, with its charge level in the ring
Source label / iconTesla · EV icon · Always showkeeps the car's node even at 0 W
Source colourviolet rgb(140, 82, 255)so the Tesla isn't the same blue as the Load node
Measurement unitAutoevcc logs watts; Auto renders 6090 as "6.09 kW"
Padding5shrinks the margin so the circles fill the panel
Show legendonlabels each node — Solar, Load, Grid, Tesla
Animation speed reference150lower value = faster particles, so the flow reads at a glance
Energy threshold5draws the line even for small flows
No size or line-width knobCircle size comes from the panel's dimensions and padding alone — there is no radius option — so a tall panel with small padding gives the biggest circles, while a full-width panel leaves side gutters that shrink the diagram. There's no line-thickness control either, so the flow is emphasised through animation speed and a bright linesColor instead.
Grid direction rides on the signevcc reports gridPower positive when importing and negative when exporting — the panel uses that to point the arrow, so exporting about 785 W reads as roughly -785. If the arrow ever looks reversed, flip it with |> map(fn: (r) => ({r with _value: r._value * -1.0})). Leave the battery node off — this build defines only grid and PV meters, so there is no battery data, and homePower already excludes the car, so Load and the Tesla never double-count.

Adding it to the existing dashboard

Rather than a separate dashboard, the panel is one more element in the model from step 15. On the Grafana 13.x schema that means a Panel under elements and a one-item row in layout. Paste both into Dashboard settings → JSON Model — put the row first so the flow leads the page — then save.

Show the flow panel JSON — add to your dashboard model
add under "elements"
"panel-30": {
  "kind": "Panel",
  "spec": {
    "data": {
      "kind": "QueryGroup",
      "spec": {
        "queries": [
          {
            "kind": "PanelQuery",
            "spec": {
              "hidden": false,
              "query": {
                "datasource": {
                  "name": "${DS_INFLUXDB}"
                },
                "group": "influxdb",
                "kind": "DataQuery",
                "spec": {
                  "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"pvPower\" and r._field == \"value\")\n  |> last()\n  |> keep(columns: [\"_time\", \"_value\"])\n  |> rename(columns: {_value: \"solar\"})"
                },
                "version": "v0"
              },
              "refId": "A"
            }
          },
          {
            "kind": "PanelQuery",
            "spec": {
              "hidden": false,
              "query": {
                "datasource": {
                  "name": "${DS_INFLUXDB}"
                },
                "group": "influxdb",
                "kind": "DataQuery",
                "spec": {
                  "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"gridPower\" and r._field == \"value\")\n  |> last()\n  |> keep(columns: [\"_time\", \"_value\"])\n  |> rename(columns: {_value: \"grid\"})"
                },
                "version": "v0"
              },
              "refId": "B"
            }
          },
          {
            "kind": "PanelQuery",
            "spec": {
              "hidden": false,
              "query": {
                "datasource": {
                  "name": "${DS_INFLUXDB}"
                },
                "group": "influxdb",
                "kind": "DataQuery",
                "spec": {
                  "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"homePower\" and r._field == \"value\")\n  |> last()\n  |> keep(columns: [\"_time\", \"_value\"])\n  |> rename(columns: {_value: \"load\"})"
                },
                "version": "v0"
              },
              "refId": "C"
            }
          },
          {
            "kind": "PanelQuery",
            "spec": {
              "hidden": false,
              "query": {
                "datasource": {
                  "name": "${DS_INFLUXDB}"
                },
                "group": "influxdb",
                "kind": "DataQuery",
                "spec": {
                  "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"chargePower\" and r._field == \"value\")\n  |> last()\n  |> keep(columns: [\"_time\", \"_value\"])\n  |> rename(columns: {_value: \"ev\"})"
                },
                "version": "v0"
              },
              "refId": "D"
            }
          },
          {
            "kind": "PanelQuery",
            "spec": {
              "hidden": false,
              "query": {
                "datasource": {
                  "name": "${DS_INFLUXDB}"
                },
                "group": "influxdb",
                "kind": "DataQuery",
                "spec": {
                  "query": "from(bucket: \"evcc\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"vehicleSoc\" and r._field == \"value\")\n  |> last()\n  |> keep(columns: [\"_time\", \"_value\"])\n  |> rename(columns: {_value: \"soc\"})"
                },
                "version": "v0"
              },
              "refId": "E"
            }
          }
        ],
        "queryOptions": {},
        "transformations": []
      }
    },
    "description": "Fronius-style live power flow (Solar Flow panel plugin).",
    "id": 30,
    "links": [],
    "title": "Live power flow",
    "vizConfig": {
      "group": "lehmannch-flow-panel",
      "kind": "VizConfig",
      "spec": {
        "fieldConfig": {
          "defaults": {},
          "overrides": []
        },
        "options": {
          "solarQuery": "solar",
          "gridQuery": "grid",
          "loadQuery": "load",
          "measurementUnit": "auto",
          "additionalSourceCount": 1,
          "additionalSourceLoadQuery": "ev",
          "additionalSourceSOCQuery": "soc",
          "additionalSourceLabel": "Tesla",
          "additionalSourceIcon": "evPanel",
          "additionalSourceColor": "rgb(140, 82, 255)",
          "additionalSourceAlwaysShow": true,
          "showEnergyThreshold": 5,
          "animationSpeedReference": 150,
          "padding": 5,
          "showLegend": true,
          "solarColor": "rgb(244, 174, 1)",
          "loadColor": "rgb(0, 141, 209)",
          "gridColor": "rgb(128, 128, 128)",
          "linesColor": "rgb(64, 196, 255)"
        }
      },
      "version": "2.0.2"
    }
  }
}
add to layout → spec → rows (first entry)
{
  "kind": "RowsLayoutRow",
  "spec": {
    "collapse": false,
    "layout": {
      "kind": "GridLayout",
      "spec": {
        "items": [
          {
            "kind": "GridLayoutItem",
            "spec": {
              "element": {
                "kind": "ElementReference",
                "name": "panel-30"
              },
              "height": 18,
              "width": 24,
              "x": 0,
              "y": 0
            }
          }
        ]
      }
    },
    "title": "Live power flow (Fronius-style)"
  }
}

The result is the four-node diagram that now leads the dashboard: Solar at the top, Load to the left, Grid to the right, and the Tesla below in violet with its charge level in the ring — power flowing from sun to house and car, and the surplus onward to the grid.

17

Appendix: addresses and ports

Fill in the right-hand column with your own values and you have a map of the whole system.

ElementAddressRole
Fronius GEN24192.168.50.99Modbus TCP 502, Solar API
Tesla Wall Connector_______________HTTP /api/1/vitals
Raspberry Pievcc.localhost for all services
evcc interfaceevcc.local:7070control and statistics
Bluetooth bridgeevcc.local:8080dashboard and API to the car
InfluxDBevcc.local:8086time-series store, bucket evcc
Grafanaevcc.local:3000dashboards
VIN_________________identifies the car to the bridge

Services that start on boot

ServiceWhat it doesHealthy state
evccenergy managementactive (running)
voltage-watchswitching to the boost tieractive (running)
grid-voltage-loggerlogs Smart Meter voltage to InfluxDBactive (running)
charge-state-loggerlogs charging mode & reason to InfluxDBactive (running)
bt-unblockunblocks the radio at bootactive (exited)
docker → tesla-ble-http-proxycommands to the carUp
docker → influxdbstores the historyUp
docker → grafanadraws the dashboardsUp

The result, live

The finished dashboard, led by a Fronius-style power-flow diagram and every panel fed from InfluxDB — with the status tiles now reading real power rather than evcc’s intent. Caught mid-act on PV surplus: solar is making 6.2 kW, the Model 3 is pulling 2.3 kW at 72 %, the house takes the rest and a little spills back to the grid, while L2 and L3 ride near 249 V — close to the derate line the boost tier exists to answer.

The finished evcc Grafana dashboard, led by the Fronius-style power-flow panel (Solar, Load, Grid and the Tesla), above per-phase house voltage with the 255 V line, and the status row reading CHARGING, 72 %, PV · surplus, with live power, current and voltage.
The finished dashboard — Fronius-style power flow on top, then house voltage, charging status, reason, battery & live values, all from evcc + the loggers