Skip to content

Instantly share code, notes, and snippets.

@LucaTNT
Created January 16, 2026 14:00
Show Gist options
  • Select an option

  • Save LucaTNT/4adf01a7252386559070023612efa117 to your computer and use it in GitHub Desktop.

Select an option

Save LucaTNT/4adf01a7252386559070023612efa117 to your computer and use it in GitHub Desktop.
ESPHome configuration to run a Modbus adapter that can talk to the Tesla Wall Charger 3 to dynamically adjust power. Includes API connection to a Shelly Pro EM 50 (easily adaptable to another type of Shelly meter)
substitutions:
device_name: esp-tesla
friendly_name: ESPHome Tesla
mqtt_broker: 10.42.1.10 # CHANGE!
tx_pin: GPIO32 # connect to RX on modbus bridge # CHANGE!
rx_pin: GPIO33 # connect to TX on modbus bridge # CHANGE!
ct1_value_id: internal_meter_reading # Name of the (number) variable that needs to be set as ct reading
min_value: 0.0
max_value: 35.0 # adjust to a value above configured max-value in Wall Connector config
start_value: 0.0 # Initial value at boot - determines the behavior when home assistant
# is not dynamically changing the ct amps: set it at zero and the maximum charging amps
# will be available to the car. Set it to max_value and the charger will only offer the car 6A
grid_voltage: 230 # used for power calculation
# serial <-> modbus bridge config
baud_rate: 115200
data_bits: 8
parity: NONE
stop_bits: 1
shelly_pro_em_ip: 10.42.40.250 # CHANGE!
esp32:
board: nodemcu-32s
framework:
type: esp-idf
logger:
level: DEBUG # Don't use DEBUG in production
baud_rate: 0 # disable serial in order to use serial ports for modbus
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
min_version: 2025.12.0
name_add_mac_suffix: false
wifi:
networks:
- ssid: "Your wifi"
password: "Your Password"
min_auth_mode: WPA2
ota:
- platform: esphome
id: esphome_ota
api:
# Get from shelly
http_request:
interval:
- interval: 1s
then:
- if:
condition:
lambda: 'return id(enable_shelly);'
then:
- http_request.get:
url: http://${shelly_pro_em_ip}/rpc/EM1.GetStatus?id=0
capture_response: true
on_response:
then:
- if:
condition:
lambda: return response->status_code == 200;
then:
- lambda: |-
json::parse_json(body, [](JsonObject root) -> bool {
if (root["current"].is<double>()) {
float current = root["current"];
// Round to 1 decimal
id(${ct1_value_id}).publish_state(round(current * 10) / 10);
if (root["voltage"].is<double>()) {
float voltage = root["voltage"];
id(ct1_voltage_v) = voltage;
} else {
ESP_LOGI("Shelly-call", "No 'voltage' key in this json!");
}
return true;
} else {
ESP_LOGI("Shelly-call", "No 'current' key in this json!");
return false;
}
});
else:
- logger.log:
format: "Error: Response status: %d, message %s"
args: [ 'response->status_code', 'body.c_str()' ]
globals:
- id: enable_shelly
type: bool
initial_value: "true"
restore_value: true
- id: all_phases_equal
type: bool
initial_value: "true"
restore_value: true
- id: ct1_voltage_v
type: float
initial_value: "230"
- id: ct2_voltage_v
type: float
initial_value: "230"
- id: ct3_voltage_v
type: float
initial_value: "230"
- id: ct1_power_w
type: float
initial_value: "0.0"
- id: ct2_power_w
type: float
initial_value: "0.0"
- id: ct3_power_w
type: float
initial_value: "0.0"
- id: ct4_power_w
type: float
initial_value: "0.0"
- id: ct_total_w
type: float
initial_value: "0.0"
- id: ct1_current_a
type: float
initial_value: "0.0"
- id: ct2_current_a
type: float
initial_value: "0.0"
- id: ct3_current_a
type: float
initial_value: "0.0"
- id: ct4_current_a
type: float
initial_value: "0.0"
- id: ct_total_a
type: float
initial_value: "0.0"
switch:
- platform: template
id: read_from_shelly
name: "Read from Shelly global"
restore_mode: RESTORE_DEFAULT_ON
lambda: |-
return id(enable_shelly);
turn_on_action:
globals.set:
id: enable_shelly
value: "true"
turn_off_action:
globals.set:
id: enable_shelly
value: "false"
- platform: template
id: all_phases_equal_mode
name: "All-phases-equal mode"
restore_mode: RESTORE_DEFAULT_ON
lambda: |-
return id(all_phases_equal);
turn_on_action:
globals.set:
id: all_phases_equal
value: "true"
turn_off_action:
globals.set:
id: all_phases_equal
value: "false"
button:
- platform: restart
name: "${friendly_name} Restart"
# Power config value
number:
# The number to be changed externally, used if shelly mode is disabled
- platform: template
id: meter_reading
name: "Meter Reading"
icon: "mdi:meter-electric"
disabled_by_default: false
unit_of_measurement: "A"
min_value: ${min_value}
max_value: ${max_value}
step: 0.1
initial_value: ${start_value} # cannot be used with lambda
optimistic: true # update when set
on_value:
then:
lambda: |-
// Copy this value to the internal number, used for modbus
id(internal_meter_reading).publish_state(id(meter_reading).state);
# The actual number used to calculate modbus data
- platform: template
id: internal_meter_reading
icon: "mdi:meter-electric"
disabled_by_default: false
unit_of_measurement: "A"
min_value: ${min_value}
max_value: ${max_value}
step: 0.1
initial_value: ${start_value} # cannot be used with lambda
optimistic: true # update when set
on_value:
then:
lambda: |-
// set ct1_current to meter reading
id(ct1_current_a) = id(${ct1_value_id}).state + id(reading_offset).state;
// set all other ct_currents to same. Ignore ct4 (it was initialized with 0.0).
id(ct2_current_a) = id(ct3_current_a) = id(ct1_current_a);
id(ct_total_a) = id(ct1_current_a) + id(ct2_current_a) + id(ct3_current_a) + id(ct4_current_a);
id(ct1_power_w) = id(ct1_voltage_v) * id(ct1_current_a); // assume cos phi = 1
if (id(all_phases_equal)) {
id(ct2_power_w) = id(ct3_power_w) = id(ct1_power_w); // all amps and watts are equal, ignore ct4
} else {
id(ct2_power_w) = id(ct2_voltage_v) * id(ct2_current_a); // assume cos phi = 1
id(ct3_power_w) = id(ct3_voltage_v) * id(ct3_current_a); // assume cos phi = 1
}
id(ct_total_w) = id(ct1_power_w) + id(ct2_power_w) + id(ct3_power_w) + id(ct4_power_w);
# Add an offset to the current set externally or read by Shelly. Useful to further reduce charging current.
- platform: template
id: reading_offset
name: "Meter reading offset"
unit_of_measurement: "A"
initial_value: 0
step: 0.1
min_value: 0
max_value: ${max_value}
optimistic: true
restore_value: true
on_value:
then:
lambda: |-
// Refresh the meter value if the offset changed
id(${ct1_value_id}).publish_state(id(${ct1_value_id}).state);
sensor:
- platform: template
name: "CT Sensor 1 current"
unit_of_measurement: A
state_class: measurement
device_class: current
lambda: |-
return id(ct1_current_a);
update_interval: 5s
accuracy_decimals: 1
disabled_by_default: true
- platform: template
name: "CT Sensor 1 power"
unit_of_measurement: W
state_class: measurement
device_class: power
lambda: |-
return id(ct1_power_w);
update_interval: 5s
accuracy_decimals: 0
disabled_by_default: true
uart:
- id: wallconn_uart
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: ${baud_rate}
data_bits: ${data_bits}
parity: ${parity}
stop_bits: ${stop_bits}
modbus:
- id: wallconn_modbus
uart_id: wallconn_uart
role: server
modbus_controller:
- id: wc_mb_server # modbus server (wall conector is client/requestor)
modbus_id: wallconn_modbus
address: 1
server_registers:
# Serial number / MAC address
- { address: 1, value_type: U_WORD, read_lambda: 'return 0x3078;' }
- { address: 2, value_type: U_WORD, read_lambda: 'return 0x3030;' }
- { address: 3, value_type: U_WORD, read_lambda: 'return 0x3030;' }
- { address: 4, value_type: U_WORD, read_lambda: 'return 0x3034;' }
- { address: 5, value_type: U_WORD, read_lambda: 'return 0x3731;' }
- { address: 6, value_type: U_WORD, read_lambda: 'return 0x3442;' }
- { address: 7, value_type: U_WORD, read_lambda: 'return 0x3035;' }
- { address: 8, value_type: U_WORD, read_lambda: 'return 0x3638;' }
- { address: 9, value_type: U_WORD, read_lambda: 'return 0x3631;' }
- { address: 10, value_type: U_WORD, read_lambda: 'return 0x0000;' }
# "1.6.1‑Tesla"
- { address: 11, value_type: U_WORD, read_lambda: 'return 0x312E;' }
- { address: 12, value_type: U_WORD, read_lambda: 'return 0x362E;' }
- { address: 13, value_type: U_WORD, read_lambda: 'return 0x312D;' }
- { address: 14, value_type: U_WORD, read_lambda: 'return 0x5465;' }
- { address: 15, value_type: U_WORD, read_lambda: 'return 0x736C;' }
- { address: 16, value_type: U_WORD, read_lambda: 'return 0x6100;' }
# Four reserved words (all 0xFFFF)
- { address: 17, value_type: U_WORD, read_lambda: 'return 0xFFFF;' }
- { address: 18, value_type: U_WORD, read_lambda: 'return 0xFFFF;' }
- { address: 19, value_type: U_WORD, read_lambda: 'return 0xFFFF;' }
- { address: 20, value_type: U_WORD, read_lambda: 'return 0xFFFF;' }
# "012.00020A.H"
- { address: 21, value_type: U_WORD, read_lambda: 'return 0x3031;' }
- { address: 22, value_type: U_WORD, read_lambda: 'return 0x322E;' }
- { address: 23, value_type: U_WORD, read_lambda: 'return 0x3030;' }
- { address: 24, value_type: U_WORD, read_lambda: 'return 0x3032;' }
- { address: 25, value_type: U_WORD, read_lambda: 'return 0x3041;' }
- { address: 26, value_type: U_WORD, read_lambda: 'return 0x2E48;' }
- { address: 27, value_type: U_WORD, read_lambda: 'return 0x0000;' }
# Reserved
- { address: 28, value_type: U_WORD, read_lambda: 'return 0xFFFF;' }
# Meter number "90954"
- { address: 29, value_type: U_WORD, read_lambda: 'return 0x3930;' }
- { address: 30, value_type: U_WORD, read_lambda: 'return 0x3935;' }
- { address: 31, value_type: U_WORD, read_lambda: 'return 0x3400;' }
# Model? "VAH4810AB0231"
- { address: 32, value_type: U_WORD, read_lambda: 'return 0x5641;' }
- { address: 33, value_type: U_WORD, read_lambda: 'return 0x4834;' }
- { address: 34, value_type: U_WORD, read_lambda: 'return 0x3831;' }
- { address: 35, value_type: U_WORD, read_lambda: 'return 0x3041;' }
- { address: 36, value_type: U_WORD, read_lambda: 'return 0x4230;' }
- { address: 37, value_type: U_WORD, read_lambda: 'return 0x3233;' }
- { address: 38, value_type: U_WORD, read_lambda: 'return 0x3100;' }
# eight more reserved (0xFFFF)
- { address: 39, value_type: U_WORD, read_lambda: 'return 0xFFFF;' }
- { address: 40, value_type: U_WORD, read_lambda: 'return 0xFFFF;' }
- { address: 41, value_type: U_WORD, read_lambda: 'return 0xFFFF;' }
- { address: 42, value_type: U_WORD, read_lambda: 'return 0xFFFF;' }
- { address: 43, value_type: U_WORD, read_lambda: 'return 0xFFFF;' }
- { address: 44, value_type: U_WORD, read_lambda: 'return 0xFFFF;' }
- { address: 45, value_type: U_WORD, read_lambda: 'return 0xFFFF;' }
- { address: 46, value_type: U_WORD, read_lambda: 'return 0xFFFF;' }
# Mac "04:71:4B:05:68:61"
- { address: 47, value_type: U_WORD, read_lambda: 'return 0x3034;' }
- { address: 48, value_type: U_WORD, read_lambda: 'return 0x3A37;' }
- { address: 49, value_type: U_WORD, read_lambda: 'return 0x313A;' }
- { address: 50, value_type: U_WORD, read_lambda: 'return 0x3442;' }
- { address: 51, value_type: U_WORD, read_lambda: 'return 0x3A30;' }
- { address: 52, value_type: U_WORD, read_lambda: 'return 0x353A;' }
- { address: 53, value_type: U_WORD, read_lambda: 'return 0x3638;' }
- { address: 54, value_type: U_WORD, read_lambda: 'return 0x3A36;' }
- { address: 55, value_type: U_WORD, read_lambda: 'return 0x3100;' }
# CT1 power W
- { address: 0x88, value_type: FP32, read_lambda: 'return id(ct1_power_w);' }
# CT2 power W
- { address: 0x8A, value_type: FP32, read_lambda: 'return id(ct2_power_w);' }
# CT3 power W
- { address: 0x8C, value_type: FP32, read_lambda: 'return id(ct3_power_w);' }
# CT4 power W
- { address: 0x8E, value_type: FP32, read_lambda: 'return id(ct4_power_w);' }
# Aggregate watts
- { address: 0x90, value_type: FP32, read_lambda: 'return id(ct_total_w);' }
# Reserved
- { address: 0x92, value_type: U_WORD, read_lambda: 'return 0;' }
# CT1 current amps
- { address: 0xF4, value_type: FP32, read_lambda: 'return id(ct1_current_a);' }
# CT2 current amps
- { address: 0xF6, value_type: FP32, read_lambda: 'return id(ct2_current_a);' }
# CT3 current amps
- { address: 0xF8, value_type: FP32, read_lambda: 'return id(ct3_current_a);' }
# CT4 current amps
- { address: 0xFA, value_type: FP32, read_lambda: 'return id(ct4_current_a);' }
# Total amps
- { address: 0xFC, value_type: FP32, read_lambda: 'return id(ct_total_a);' }
# Initialization handshake
- { address: 40002, value_type: U_WORD, read_lambda: 'return 0x0001;' }
- { address: 40003, value_type: U_WORD, read_lambda: 'return 0x0042;' }
- { address: 40004, value_type: U_WORD, read_lambda: 'return 0x4765;' }
- { address: 40005, value_type: U_WORD, read_lambda: 'return 0x6E65;' }
- { address: 40006, value_type: U_WORD, read_lambda: 'return 0x7261;' }
- { address: 40007, value_type: U_WORD, read_lambda: 'return 0x6300;' }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment