[EV Charger] ABB Terra AC Wallbox - Modbus RS485

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,

  1. Ethernet / Wifi can be used for ABB chargersync app. (connection is actually OCPP to ABB’s server)
  2. 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.
  3. 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


Installation pictures:



References

1 Like