ABB Terra AC EV Charger Modbus RS485 RTU Bridge - FULL LOCAL HA Control
Tags: esphome modbus ev-charger esp32 rs485 abb
Overview
ABB Terra AC Wallbox chargers future multiple wired and wireless interfaces but communication protocols differ for each interface,
- Ethernet / Wifi can be used for ABB chargersync app. (connection is actually OCPP to ABB’s server)
- My charger model is without LCD, I’ve read here on the forum that it doesn’t support - Modbus TCP, didn’t bother to check that.
- My charger model has 4G daughter board, located on top of main board.
The charger supports Modbus RTU over RS485, so I built a small ESP32-based bridge that translates Modbus into ESPHome’s native API. Total hardware cost: under $5.
The result: real-time voltage, current, power and session energy readings, plus full start/stop/current-limit control, all appearing as native HA entities.
Hardware
| Component | Notes |
|---|---|
| ESP32 (any devkit) | I used a generic ESP32-WROOM-38pin |
| HW-519 RS485 module | Powered from ESP32 3.3V |
| DCDC 5V | Leeched 12V from 4G daughter board supply |
Wiring:
ESP32 GPIO17 (TX) → HW-519 TX (yes it's not a mistake - TX)
ESP32 GPIO16 (RX) → HW-519 RX (yes it's not a mistake - RX)
HW-519 A+ → ABB Terra A
HW-519 B- → ABB Terra B
Power:
Direct solder to 4G board header (need to take out from charger for soldering)
12V (4G Board) → JST Conn. (W2W) → DCDC 5V → ESP32 5V Input
HW-519 powered from ES32 3.3V pin
The ABB Terra exposes an RS485 port on its internal terminal block — refer to the Terra AC Installation Manual for the exact terminal numbers.
What It Exposes in HA
Sensors
| Entity | Register | Notes |
|---|---|---|
| Voltage L1/L2/L3 | 0x4016–0x401A |
0.1V resolution |
| Current L1/L2/L3 | 0x4010–0x4014 |
0.001A resolution |
| Active Power | 0x401C |
Watts |
| Session Energy | 0x401E |
Wh, resets each session |
| Active Current Limit | 0x400E |
Charger’s actual applied limit |
| Max Hardware Current | 0x4006 |
Set via Terra Config app |
| Modbus Error Code | 0x4008 |
0 = no error |
| Charger Status | 0x400C |
Decoded text + binary sensor |
Controls
| Entity | Register | Notes |
|---|---|---|
| Set Charging Current Limit | 0x4100 |
Slider 6–16A, sends mA to charger |
| Start Charging | 0x4105 |
Writes 0 |
| Stop Charging | 0x4105 |
Writes 1 |
Binary Sensors
| Entity | Notes |
|---|---|
| Charging at Rated Current | True = full power, False = throttled |
ESPhome YAML file:
esphome:
name: abb-terra-bridge
friendly_name: abb-terra-bridge
on_boot:
priority: -100
then:
- number.set:
id: charging_current_limit
value: 8 # ← 8A default on boot, writes 8000 to 0x4100
esp32:
board: esp32dev
framework:
type: esp-idf
sdkconfig_options:
CONFIG_ESP32_BROWNOUT_DET: "y"
logger:
level: DEBUG # Change to VERBOSE if you need to see raw Modbus hex bytes
baud_rate: 0
api:
encryption:
key: "YOURKEY"
ota:
- platform: esphome
password: "YOUROTAPASS"
wifi:
ssid: !secret YOURWIFI_SSD
password: !secret YOURWIFI_PASS
ap:
ssid: "Abb-Terra-Bridge"
password: "YOUR_AP_PASS"
captive_portal:
# Built-in ESPHome web UI (shows all entities)
web_server:
port: 80
include_internal: true
local: true
# --- ABB BRIDGE LOGIC STARTS HERE ---
uart:
id: mod_bus_uart
tx_pin: GPIO17
rx_pin: GPIO16
baud_rate: 9600
data_bits: 8
parity: EVEN
stop_bits: 1
modbus:
id: modbus_bus
uart_id: mod_bus_uart
modbus_controller:
- id: abb_charger
address: 1
modbus_id: modbus_bus
update_interval: 10s
sensor:
# --- WiFi link quality (for web/debug) ---
- platform: wifi_signal
id: wifi_rssi
name: "WiFi Signal (dBm)"
device_class: signal_strength
update_interval: 30s
- platform: template
name: "WiFi Signal (%)"
unit_of_measurement: "%"
accuracy_decimals: 0
update_interval: 30s
lambda: |-
if (isnan(id(wifi_rssi).state)) return NAN;
float rssi = id(wifi_rssi).state; // in dBm
// Map -100..-50 dBm to 0..100% (clamped)
if (rssi <= -100) return 0;
if (rssi >= -50) return 100;
return (int) (2 * (rssi + 100));
# Optional smoothing to avoid jitter:
filters:
- sliding_window_moving_average:
window_size: 5
send_every: 1
# --- Diagnostic Data ---
- platform: modbus_controller
modbus_controller_id: abb_charger
name: "Max Hardware Current"
address: 16390
register_type: holding
value_type: U_DWORD
filters:
- multiply: 0.001
unit_of_measurement: "A"
entity_category: diagnostic
- platform: modbus_controller
modbus_controller_id: abb_charger
name: "Modbus Error Code"
icon: "mdi:alert-circle"
address: 16392
register_type: holding
value_type: U_DWORD
entity_category: diagnostic
# --- Charging Status ---
- platform: modbus_controller
modbus_controller_id: abb_charger
id: raw_charging_state
name: "Raw Charging State"
address: 16396
register_type: holding
value_type: U_DWORD
internal: true # Hide this from HA once the templates work
- platform: template
name: "Charging State"
icon: "mdi:ev-station"
internal: true
lambda: |-
return (uint32_t)id(raw_charging_state).state & 0xFF;
- platform: modbus_controller
modbus_controller_id: abb_charger
name: "Active Current Limit"
address: 16398
register_type: holding
value_type: U_DWORD
filters:
- multiply: 0.001
unit_of_measurement: "A"
# --- Phase Currents ---
- platform: modbus_controller
modbus_controller_id: abb_charger
name: "Current L1"
icon: "mdi:current-ac"
address: 16400
register_type: holding
value_type: U_DWORD
filters: { multiply: 0.001 }
unit_of_measurement: "A"
accuracy_decimals: 3
- platform: modbus_controller
modbus_controller_id: abb_charger
name: "Current L2"
icon: "mdi:current-ac"
address: 16402
register_type: holding
value_type: U_DWORD
filters: { multiply: 0.001 }
unit_of_measurement: "A"
accuracy_decimals: 3
- platform: modbus_controller
modbus_controller_id: abb_charger
name: "Current L3"
icon: "mdi:current-ac"
address: 16404
register_type: holding
value_type: U_DWORD
filters: { multiply: 0.001 }
unit_of_measurement: "A"
accuracy_decimals: 3
# --- Phase Voltages ---
- platform: modbus_controller
modbus_controller_id: abb_charger
name: "Voltage L1"
icon: "mdi:lightning-bolt"
address: 16406
register_type: holding
value_type: U_DWORD
filters: { multiply: 0.1 }
unit_of_measurement: "V"
accuracy_decimals: 1
- platform: modbus_controller
modbus_controller_id: abb_charger
name: "Voltage L2"
icon: "mdi:lightning-bolt"
address: 16408
register_type: holding
value_type: U_DWORD
filters: { multiply: 0.1 }
unit_of_measurement: "V"
accuracy_decimals: 1
- platform: modbus_controller
modbus_controller_id: abb_charger
name: "Voltage L3"
icon: "mdi:lightning-bolt"
address: 16410
register_type: holding
value_type: U_DWORD
filters: { multiply: 0.1 }
unit_of_measurement: "V"
accuracy_decimals: 1
# --- Power and Energy ---
- platform: modbus_controller
modbus_controller_id: abb_charger
name: "Active Power"
icon: "mdi:flash"
address: 16412
register_type: holding
value_type: U_DWORD
unit_of_measurement: "W"
accuracy_decimals: 0
- platform: modbus_controller
modbus_controller_id: abb_charger
name: "Session Energy"
icon: "mdi:battery-charging"
address: 16414
register_type: holding
value_type: U_DWORD
unit_of_measurement: "Wh"
accuracy_decimals: 0
# --- User Control Interace ---
number:
- platform: modbus_controller
modbus_controller_id: abb_charger
name: "Set Charging Current Limit"
icon: "mdi:ev-plug-type2"
id: charging_current_limit
unit_of_measurement: "A"
address: 16640 # 0x4100 — Set Charging Current Limit (ABB manual §5.13)
register_type: holding # ← required, was missing
value_type: U_DWORD # size 2, resolution 0.001 A per ABB manual
min_value: 6 # IEC 61851-1 minimum (ABB manual §5.13)
max_value: 16 # adjust to your breaker / Max HW current
step: 1
multiply: 1000 # HA sends amps, charger expects mA (e.g. 16A → 16000)
use_write_multiple: true # U_DWORD = 2 registers → forces FC16
skip_updates: 2147483647 # ← this is the correct param for number
output:
- platform: modbus_controller
id: out_start_stop
modbus_controller_id: abb_charger
address: 16645 # 0x4105 — Start/Stop register (ABB manual §5.15)
value_type: U_WORD # Size 1 register per ABB manual
register_type: holding
write_lambda: |-
// x is the float passed in from output.set_level (0.0 or 1.0)
// We write it directly as uint16_t: 0 = Start, 1 = Stop
payload.push_back((uint16_t)x);
return x;
button:
- platform: template
name: "Start Charging"
icon: "mdi:play"
on_press:
- output.set_level:
id: out_start_stop
level: 0.0 # 0 = Start session (ABB manual §5.15)
- platform: template
name: "Stop Charging"
icon: "mdi:stop"
on_press:
- output.set_level:
id: out_start_stop
level: 1.0 # 1 = Stop session (ABB manual §5.15)
text_sensor:
- platform: template
name: "Charger Status Description"
icon: "mdi:information-outline"
update_interval: 10s
lambda: |-
// Isolate Byte 0 (Bits 0-7) as defined in manual section 5.6
int state_val = (int)((uint32_t)id(raw_charging_state).state & 0xFF);
switch (state_val) {
case 0:
return {"State A: Idle"};
case 1:
return {"State B1: Plugged In (Pending Auth)"};
case 2:
return {"State B2: Plugged In (Ready)"};
case 3:
return {"State C1: EV Ready (S2 Closed)"};
case 4:
return {"State C2: Charging (Energy Delivering)"};
case 5:
return {"Others/Transitional"};
default:
return {"Unknown State"};
}
binary_sensor:
- platform: template
name: "Charging at Rated Current"
icon: "mdi:speedometer"
device_class: power # shows as On/Off with power icon in HA
lambda: |-
uint8_t byte1 = ((uint32_t)id(raw_charging_state).state >> 8) & 0xFF;
return ((byte1 >> 7) & 1) == 0; // true = at rated current, false = throttled

