An ESPHome component that turns a Zuidwijk SlimmeLezer Pro (P1 smart meter reader, ESP32-C3-based) into a Modbus TCP meter that an Alfen Eve Single Plus charging station can read for Active Load Balancing (ALB) — no separate physical meter or gateway device needed.
Alfen's ALB feature can read current/power from an external Modbus TCP
meter to know how much headroom is left before allowing the car to draw
more current. The obvious approach — emulating a real commercial meter's
32-bit register format — doesn't work reliably against Alfen's own Modbus
client. This component works around two real bugs found on Alfen's side
(32-bit values get misassembled; a field mapped to register 0 is
silently skipped) by serving plain, single-register SIGNED16 values
instead.
- An ESP32-based Zuidwijk SlimmeLezer — Pro or Mini (both are
ESP32-C3 running ESP-IDF). The example config in this repo targets the
Pro's Ethernet setup; the Mini should work too but needs its own
WiFi-only config, not included here. This will not work on the
original/Classic SlimmeLezer (ESP8266-based): the component talks
directly to raw POSIX sockets (
socket()/bind()/accept()/...), which only exist because ESP-IDF provides them — and ESP-IDF is Espressif's SDK for the ESP32 family only, it was never built for the ESP8266. - Ethernet connection recommended (more reliable for something feeding a charger's load-balancing input than WiFi, though WiFi should work too).
- An Alfen charging station with Active Load Balancing licensed and enabled, configured for an external Modbus TCP/IP meter (Data Source = "Meter", not "Energy Management System").
- Copy
example/slimmelezer_pro_alfen_meter.yamlandexample/secrets.yaml.example→secrets.yamlinto your ESPHome config folder, filling in your own WiFi credentials. - In the YAML's
external_components:block, replace<your-github-username>with wherever you've actually published this. - Flash it (see ESPHome's docs if you're new to this).
- Configure Alfen — see the register table below.
- Verify — see Testing, below. Don't skip this; confirm the firmware is serving correct data before trusting what Alfen's screen shows.
ACE Service Installer → Load Balancing → Active balancing → Data Source = Meter → Protocol = Modbus TCP/IP → register mapping dialog → select "generic modbus meter" → enter exactly this table:
| Field | Register | Data type | Scale |
|---|---|---|---|
| Current L1 | 1 | SIGNED16 | x0.01 |
| Current L2 | 2 | SIGNED16 | x0.01 |
| Current L3 | 3 | SIGNED16 | x0.01 |
| Real Power L1 | 4 | SIGNED16 | x1 |
| Real Power L2 | 5 | SIGNED16 | x1 |
| Real Power L3 | 6 | SIGNED16 | x1 |
Then enter the SlimmeLezer's IP address and port 502 as the meter
address.
Enter one field at a time and save between each, if the installer allows it. Values outside the expected range have been observed to require an ACE Service Installer reboot to recover from.
simple_meter:
current_l1: current_l1 # required -- ESPHome sensor ids
current_l2: current_l2
current_l3: current_l3
power_l1: net_power_l1 # optional
power_l2: net_power_l2
power_l3: net_power_l3
port: 502
current_scale: 100.0
power_scale: 1.0
data_timeout: 30s| Option | Required | Default | Meaning |
|---|---|---|---|
current_l1 / l2 / l3 |
Yes | — | ESPHome sensor id for each phase's current (amps). Typically the DSMR current_l1/l2/l3 sensors. |
power_l1 / l2 / l3 |
No | — | ESPHome sensor id for each phase's net power (positive = import, negative = export). See "Net power" below — you almost certainly want a computed sensor here, not a raw DSMR one. |
port |
No | 502 |
Modbus TCP listen port. Alfen expects 502; no reason to change this normally. |
current_scale |
No | 100.0 |
Register value = round(amps × current_scale). Default gives a resolution of 0.01 A and a range of ±327.67 A. Must match the reciprocal of whatever scale you enter in Alfen (default → Alfen scale x0.01). |
power_scale |
No | 1.0 |
Register value = round(watts × power_scale). Default gives whole-watt resolution and ±32,767 W range. Must match Alfen's scale setting (default → Alfen scale x1). |
data_timeout |
No | 30s |
If no sensor has produced a valid (non-NaN) reading for this long, the Modbus TCP server stops accepting/serving connections entirely, rather than let Alfen read frozen values. Set to 0s to disable. |
Registers served: 0 (always 0, deliberately unused),
1-3 = current L1-L3, 4-6 = power L1-L3. Any other
address returns 0. Supports Modbus function codes 03/04 (read),
06/16 (write -- accepted and acknowledged, but ignored; this is a
read-only meter).
DSMR gives two always-non-negative fields per phase -- power_delivered
(import) and power_returned (export) -- never a signed value directly.
The example config computes net power itself:
sensor:
- platform: template
id: net_power_l1
lambda: return id(power_delivered_l1).state - id(power_returned_l1).state;...and -- importantly -- wires it to recompute immediately when either
underlying value changes (via on_value: - component.update: net_power_l1
on both power_delivered_l1 and power_returned_l1), rather than on a
fixed timer. This keeps the served power value as fresh as the underlying
~1 Hz DSMR telegram rate, with no added polling lag.
sensor:
- platform: template
name: "P1 Telegram Age"
id: p1_telegram_age
unit_of_measurement: "s"
update_interval: 1s
lambda: |-
return (millis() - id(last_telegram_ms)) / 1000.0;Tracks milliseconds since the last real DSMR telegram was received (a
globals.set on the telegram: text sensor's on_value records the
timestamp). Expect this to sawtooth between roughly 0 and ~1.2s under
normal operation. If it climbs steadily or stops resetting, the P1
connection itself has stalled -- independent of anything Alfen-related.
Don't rely on Alfen's test screen alone -- verify the firmware independently first, so any mismatch you see later is unambiguous about which side it's on.
1. Check the firmware directly, bypassing Alfen:
python3 -m pip install pymodbus
python3 tools/check_registers.py <device-ip>Compare the printed values against the SlimmeLezer's own web UI
(http://<device-ip>/) at the same moment. Note: DSMR reports current in
whole amps only (no decimal resolution in the telegram itself) -- small
loads under ~0.5A will genuinely read as 0, which is correct behavior,
not a bug.
2. Once the firmware is confirmed correct, check what Alfen actually requests. Set logging to capture every incoming Modbus request:
logger:
level: VERBOSE
logs:
dsmr: DEBUG
sensor: DEBUG
text_sensor: DEBUG(Narrowing dsmr/sensor/text_sensor back to DEBUG keeps their
per-telegram/per-sensor noise from burying the sparser simple_meter
lines -- and matters given the ESP32's log buffer is only 768 bytes.)
Watch the logs (or esphome logs your-config.yaml) while Alfen polls.
You're looking for lines like:
[V][simple_meter]: FC03 start=0x0001 count=6
This tells you, with certainty: which function code Alfen uses, the exact starting address, and how many registers it reads in one request. If a field isn't behaving as expected, check whether its configured register number is actually present in what Alfen requests -- if it's missing entirely (not misread, just absent), that's the register-0-style bug described above, not a data problem.
3. Cross-check the two. Compare the register-check script's output against Alfen's smart meter test screen at the same moment. Matching (accounting for Alfen's own display conventions -- e.g. it shows power in kW, not W) confirms the whole chain end to end.
Revert logger: back to level: DEBUG once done -- VERBOSE has a real
performance cost and ESPHome will remind you of this in the logs.
- SIGNED16 range: current up to ±327.67 A, power up to ±32,767 W per
phase at the default scales -- comfortable margin for any residential/
light-commercial setup, but adjust
current_scale/power_scaleif you need different resolution or Alfen's scale dropdown doesn't offerx0.01/x1. - Voltage is not served. Alfen's ALB doesn't request it for current limiting, so it was left out. Straightforward to add if needed elsewhere.
- Built and tested against one specific Alfen ACE Service Installer / firmware version. Alfen's Modbus client behavior may differ across versions -- if this doesn't work for you, the verbose-logging technique above is the fastest way to see what's actually happening on your setup.
- The register-mapping dialog has been observed to require an installer reboot after certain out-of-range inputs. Cause not fully isolated.
The Modbus TCP server itself -- connection handling, request framing,
non-blocking accept loop, and the stale-data watchdog -- is substantially
reused, near line-for-line, from RemCom/victron-grid-meter-esphome
(built for Victron Cerbo GX EM24 emulation). That project's networking
implementation is solid and this one didn't need to reinvent it.
MIT -- see LICENSE.