Bluecorner (BLINK) Electric Vehicle Charger (EV) Active Load Balancing

I’ve got a Bluecorner Curved Pro EV Charger installed with the OCPP server owned by Bluecorner (for billing of company car). It is connected to my smart meter to protect the grid connection from overload. I’d like to take over this connection with home assistant to throttle charging based on my solar output and grid prices. It is modbus RS485

communication so I was hoping to ‘fool’ the EV Charger into thinking it is overloading the grid connection and thus reduce charging amps to the car.

I’m planning on using an Elfin-E11A,edit: can’t get this to work so I’m going for an esp32. Keep you posted

Update: I’ve managed to read out the modbus of the eDP1B device, screenshots attached. For the rest of the integration, I’ve bought an esp32 to follow the guide mentioned here: GitHub - thomase1234/esphome-fake-xemex-csmb: ESPHome Modbus Server/Slave component to fake the Xemex CSMB



Was wondering how for you got controlling the blue corner/blink curved charging station from Home Assistant? You tried faking the load balancer device so I guess there is no way to control the charging station directly over modbus or ocpp ?

No, in my case the ocpp server is owned by the leasing company. Otherwise that would be the go to option

Thanks for sharing!
A couple of questions:

  • How do you like the solution and how flexible is it?
  • Is it possible to fool the BC Curved that no energy is available, and hence postpone charging? As this would come in handy to e.g. plug in the car in the morning and only start charging when enough solar is available.
  • Also, how granular can this be controlled (can this be on a 1A basis somehow?) and how fast is the charger to react?

Hi TyngRikku, can you please provide all code? because I have the same blue corner pro, and the I am very insterressed to get your code!
many thanks for your feedback!
kind regards
Zsolt

Hi TyngRikku, good morning,

9:34

Finally, I performed a configuration on ESP32, I receive the request from Bluecorner Wallbox, and ESP 32 answer, but nothing happend from Wallbox, When I put the current to 50A(Max), the wallbox should decrease the charge or stop the charge! but it’s not the case! do you met the same behavior?

Hi @TyngRikku,

Thanks for sharing this!

I was able to enable ESP8266 + RS485 based on the github repo you’ve shared. It looks like the modbus registers might be different from Shell compared to the Bluecorner Curved Pro.

Can you share your esphome yaml configuration please? That would be very valuable!

Thanks!

hi @TyngRikku,

exact same challenge/question here: I want to connect the curved charger to an esp32 running esphome and I’m looking for the register values as well. I don’t have the loadbalancer installed, so can’t sniff the bus myself.

Hi,

Very interesting. I have the same EV charger. Can you please share how did you manage to read out the modbus?

Thanks!

Ok so in my enthousiasm I got the charger to react but for the wrong reasons: It got valid data but wrong data so it stopped charging and waiting. it asks voltage, ampere and wattage (total and per phase), see modbuspoll attached. Will test it later on my charger

substitutions:
  device_name: "esphome-web-8aebbc"

esphome:
  name: ${device_name}
  friendly_name: "ESPHome Load Balancer"
  on_boot:
    priority: -10
    then:
      - lambda: |-
          union { float f; uint16_t w[2]; } v, a, p, pt;
          v.f = 231.8f; a.f = 2.8f; 
          p.f = v.f * a.f;     // ~650W per fase
          pt.f = p.f * 3.0f;   // ~1950W totaal

          // STAP 1: Alles op -1 (FFFF)
          for (int i = 20480; i <= 20519; i++) {
            id(mb_server).write_holding_register(i, 0xFFFF);
          }

          // STAP 2: Schrijf data (Big Endian ABCD)
          // Voltages (20482, 20484, 20486)
          for (int i = 0; i < 3; i++) {
            id(mb_server).write_holding_register(20482 + (i*2), v.w[1]);
            id(mb_server).write_holding_register(20483 + (i*2), v.w[0]);
          }

          // Amperes (20492, 20494, 20496)
          for (int i = 0; i < 3; i++) {
            id(mb_server).write_holding_register(20492 + (i*2), a.w[1]);
            id(mb_server).write_holding_register(20493 + (i*2), a.w[0]);
          }

          // NIEUWE MAPPING: Vermogen per fase (20498, 20500, 20502)
          id(mb_server).write_holding_register(20498, p.w[1]); id(mb_server).write_holding_register(20499, p.w[0]);
          id(mb_server).write_holding_register(20500, p.w[1]); id(mb_server).write_holding_register(20501, p.w[0]);
          id(mb_server).write_holding_register(20502, p.w[1]); id(mb_server).write_holding_register(20503, p.w[0]);

          // Totaal Vermogen (20504)
          id(mb_server).write_holding_register(20504, pt.w[1]); id(mb_server).write_holding_register(20505, pt.w[0]);

esp32:
  board: esp32dev
  framework: {type: arduino}

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.0.148
    gateway: 192.168.0.1
    subnet: 255.255.255.0

logger:
  level: VERY_VERBOSE
  baud_rate: 0 

uart:
  - id: intmodbus
    tx_pin: 18
    rx_pin: 19
    baud_rate: 9600
    stop_bits: 1
    data_bits: 8
    parity: EVEN 
    rx_buffer_size: 1024

external_components:
  - source: github://thomase1234/esphome-fake-xemex-csmb@master
    components: [ modbus_server ]

modbus_server:
  - id: mb_server
    uart_id: intmodbus
    address: 1
    holding_registers: [{start_address: 20480, number: 60}]

# Script om alle vermogensregisters in één keer te updaten (P1, P2, P3 en Totaal)
script:
  - id: update_all_power
    then:
      - lambda: |-
          union { float f; uint16_t w[2]; } p1, p2, p3, pt;
          p1.f = id(v_l1).state * id(a_l1).state;
          p2.f = id(v_l2).state * id(a_l2).state;
          p3.f = id(v_l3).state * id(a_l3).state;
          pt.f = p1.f + p2.f + p3.f;

          id(mb_server).write_holding_register(20498, p1.w[1]); id(mb_server).write_holding_register(20499, p1.w[0]);
          id(mb_server).write_holding_register(20500, p2.w[1]); id(mb_server).write_holding_register(20501, p2.w[0]);
          id(mb_server).write_holding_register(20502, p3.w[1]); id(mb_server).write_holding_register(20503, p3.w[0]);
          id(mb_server).write_holding_register(20504, pt.w[1]); id(mb_server).write_holding_register(20505, pt.w[0]);

number:
  - platform: template
    name: "Net Frequentie (Hz)"
    id: n_freq
    initial_value: 50.0
    optimistic: true
    min_value: 45
    max_value: 55
    step: 0.1
    on_value:
      then:
        - lambda: |-
            union { float f; uint16_t w[2]; } f; f.f = (float)x;
            id(mb_server).write_holding_register(20480, f.w[1]); id(mb_server).write_holding_register(20481, f.w[0]);

  # FASE 1
  - platform: template
    name: "L1 Spanning (V)"
    id: v_l1
    initial_value: 231.8
    optimistic: true
    min_value: 200
    max_value: 250
    step: 0.1
    on_value:
      then:
        - lambda: |-
            union { float f; uint16_t w[2]; } v; v.f = (float)x;
            id(mb_server).write_holding_register(20482, v.w[1]); id(mb_server).write_holding_register(20483, v.w[0]);
        - script.execute: update_all_power

  - platform: template
    name: "L1 Stroom (A)"
    id: a_l1
    initial_value: 2.8
    optimistic: true
    min_value: 0
    max_value: 100
    step: 0.1
    on_value:
      then:
        - lambda: |-
            union { float f; uint16_t w[2]; } a; a.f = (float)x;
            id(mb_server).write_holding_register(20492, a.w[1]); id(mb_server).write_holding_register(20493, a.w[0]);
        - script.execute: update_all_power

  # FASE 2
  - platform: template
    name: "L2 Spanning (V)"
    id: v_l2
    initial_value: 231.8
    optimistic: true
    min_value: 200
    max_value: 250
    step: 0.1
    on_value:
      then:
        - lambda: |-
            union { float f; uint16_t w[2]; } v; v.f = (float)x;
            id(mb_server).write_holding_register(20484, v.w[1]); id(mb_server).write_holding_register(20485, v.w[0]);
        - script.execute: update_all_power

  - platform: template
    name: "L2 Stroom (A)"
    id: a_l2
    initial_value: 2.8
    optimistic: true
    min_value: 0
    max_value: 100
    step: 0.1
    on_value:
      then:
        - lambda: |-
            union { float f; uint16_t w[2]; } a; a.f = (float)x;
            id(mb_server).write_holding_register(20494, a.w[1]); id(mb_server).write_holding_register(20495, a.w[0]);
        - script.execute: update_all_power

  # FASE 3
  - platform: template
    name: "L3 Spanning (V)"
    id: v_l3
    initial_value: 231.8
    optimistic: true
    min_value: 200
    max_value: 250
    step: 0.1
    on_value:
      then:
        - lambda: |-
            union { float f; uint16_t w[2]; } v; v.f = (float)x;
            id(mb_server).write_holding_register(20486, v.w[1]); id(mb_server).write_holding_register(20487, v.w[0]);
        - script.execute: update_all_power

  - platform: template
    name: "L3 Stroom (A)"
    id: a_l3
    initial_value: 2.8
    optimistic: true
    min_value: 0
    max_value: 100
    step: 0.1
    on_value:
      then:
        - lambda: |-
            union { float f; uint16_t w[2]; } a; a.f = (float)x;
            id(mb_server).write_holding_register(20496, a.w[1]); id(mb_server).write_holding_register(20497, a.w[0]);
        - script.execute: update_all_power

Tested it and it works: charger stops when load too high, starts again when low!

final code:

substitutions:
  device_name: "esphome-web-8aebbc"

esphome:
  name: ${device_name}
  friendly_name: "ESPHome Load Balancer"
  on_boot:
    priority: -10
    then:
      - lambda: |-
          union { float f; uint16_t w[2]; } v, a, p, pt;
          v.f = 231.8f; a.f = 1.5f; 
          p.f = v.f * a.f;     // ~347W
          pt.f = p.f * 3.0f;   // ~1043W

          // STAP 1: Reset alles naar -1 (FFFF)
          for (int i = 20480; i <= 20519; i++) {
            id(mb_server).write_holding_register(i, 0xFFFF);
          }

          // STAP 2: Schrijf data (Big Endian ABCD: w[1]=High, w[0]=Low)
          for (int i = 0; i < 3; i++) {
            id(mb_server).write_holding_register(20482 + (i*2), v.w[1]);
            id(mb_server).write_holding_register(20483 + (i*2), v.w[0]);
            id(mb_server).write_holding_register(20492 + (i*2), a.w[1]);
            id(mb_server).write_holding_register(20493 + (i*2), a.w[0]);
          }

          // Vermogens volgens nieuwe inzichten (P1=20498, P2=20500, P3=20502, Tot=20504)
          id(mb_server).write_holding_register(20498, p.w[1]); id(mb_server).write_holding_register(20499, p.w[0]);
          id(mb_server).write_holding_register(20500, p.w[1]); id(mb_server).write_holding_register(20501, p.w[0]);
          id(mb_server).write_holding_register(20502, p.w[1]); id(mb_server).write_holding_register(20503, p.w[0]);
          id(mb_server).write_holding_register(20504, pt.w[1]); id(mb_server).write_holding_register(20505, pt.w[0]);

esp32:
  board: esp32dev
  framework: {type: arduino}

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.0.148
    gateway: 192.168.0.1
    subnet: 255.255.255.0
  use_address: 192.168.0.148  # FORCEERT DIRECTE VERBINDING

api:
  reboot_timeout: 0s

logger:
  level: VERY_VERBOSE
  baud_rate: 0  # Houd op 0 voor Modbus stabiliteit op pins 18/19

uart:
  - id: intmodbus
    tx_pin: 18
    rx_pin: 19
    baud_rate: 9600
    stop_bits: 1
    data_bits: 8
    parity: EVEN 
    rx_buffer_size: 1024
    # Voeg dit toe om de rauwe data weer in de logs te zien:
    debug:
      direction: BOTH
      dummy_receiver: false
      after:
        delimiter: "\n"
      sequence:
        - lambda: |-
            ESP_LOGD("modbus_traffic", "Hex: %s", format_hex_pretty(bytes).c_str());

external_components:
  - source: github://thomase1234/esphome-fake-xemex-csmb@master
    components: [ modbus_server ]

modbus_server:
  - id: mb_server
    uart_id: intmodbus
    address: 1
    holding_registers: [{start_address: 20480, number: 60}]

script:
  - id: update_all_p
    then:
      - lambda: |-
          union { float f; uint16_t w[2]; } p1, p2, p3, pt;
          p1.f = id(v_l1).state * id(a_l1).state;
          p2.f = id(v_l2).state * id(a_l2).state;
          p3.f = id(v_l3).state * id(a_l3).state;
          pt.f = p1.f + p2.f + p3.f;
          id(mb_server).write_holding_register(20498, p1.w[1]); id(mb_server).write_holding_register(20499, p1.w[0]);
          id(mb_server).write_holding_register(20500, p2.w[1]); id(mb_server).write_holding_register(20501, p2.w[0]);
          id(mb_server).write_holding_register(20502, p3.w[1]); id(mb_server).write_holding_register(20503, p3.w[0]);
          id(mb_server).write_holding_register(20504, pt.w[1]); id(mb_server).write_holding_register(20505, pt.w[0]);

number:
  # FASE 1
  - platform: template
    name: "L1 Spanning"
    id: v_l1
    initial_value: 231.8
    optimistic: true
    min_value: 200
    max_value: 250
    step: 0.1
    on_value: 
      then: 
        - lambda: "union { float f; uint16_t w[2]; } v; v.f = (float)x; id(mb_server).write_holding_register(20482, v.w[1]); id(mb_server).write_holding_register(20483, v.w[0]);"
        - script.execute: update_all_p

  - platform: template
    name: "L1 Stroom"
    id: a_l1
    initial_value: 1.5
    optimistic: true
    min_value: 0
    max_value: 100
    step: 0.1
    on_value: 
      then: 
        - lambda: "union { float f; uint16_t w[2]; } a; a.f = (float)x; id(mb_server).write_holding_register(20492, a.w[1]); id(mb_server).write_holding_register(20493, a.w[0]);"
        - script.execute: update_all_p

  # FASE 2
  - platform: template
    name: "L2 Spanning"
    id: v_l2
    initial_value: 231.8
    optimistic: true
    min_value: 200
    max_value: 250
    step: 0.1
    on_value: 
      then: 
        - lambda: "union { float f; uint16_t w[2]; } v; v.f = (float)x; id(mb_server).write_holding_register(20484, v.w[1]); id(mb_server).write_holding_register(20485, v.w[0]);"
        - script.execute: update_all_p

  - platform: template
    name: "L2 Stroom"
    id: a_l2
    initial_value: 1.5
    optimistic: true
    min_value: 0
    max_value: 100
    step: 0.1
    on_value: 
      then: 
        - lambda: "union { float f; uint16_t w[2]; } a; a.f = (float)x; id(mb_server).write_holding_register(20494, a.w[1]); id(mb_server).write_holding_register(20495, a.w[0]);"
        - script.execute: update_all_p

  # FASE 3
  - platform: template
    name: "L3 Spanning"
    id: v_l3
    initial_value: 231.8
    optimistic: true
    min_value: 200
    max_value: 250
    step: 0.1
    on_value: 
      then: 
        - lambda: "union { float f; uint16_t w[2]; } v; v.f = (float)x; id(mb_server).write_holding_register(20486, v.w[1]); id(mb_server).write_holding_register(20487, v.w[0]);"
        - script.execute: update_all_p

  - platform: template
    name: "L3 Stroom"
    id: a_l3
    initial_value: 1.5
    optimistic: true
    min_value: 0
    max_value: 100
    step: 0.1
    on_value: 
      then: 
        - lambda: "union { float f; uint16_t w[2]; } a; a.f = (float)x; id(mb_server).write_holding_register(20496, a.w[1]); id(mb_server).write_holding_register(20497, a.w[0]);"
        - script.execute: update_all_p