-
-
Save LucaTNT/4adf01a7252386559070023612efa117 to your computer and use it in GitHub Desktop.
| 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;' } |
Hi @Klangen82,
I used this RS485 board from Aliexpress), to which I soldered header pins to connect to my ESP32 and some screw terminals to connect to the TWC. The connection was straight from A to the "+" connection of the TWC and from B to the "-". No ground connection, no resistors, no further configuration on the RS485 board.
Tesla One just saw there was a meter connected and offered to configure it.
Here is a picture of the setup on my desk, I didn't take one while connected to the TWC. Ignore the white/red cable soldered to the ESP32 board, it's a residue from a previous project.
Let me know if you need anything else!
Hi Luca, thanks a lot for the details and the photo, super helpful!
A couple of follow-up questions that might explain why my Wall Connector never starts polling Modbus:
- What firmware version is your Tesla Wall Connector running?
- Did you have to follow any specific startup sequence to make Modbus activate? For example:
- power off Wall Connector
- connect RS485 / ESP32
- power on Wall Connector
- then enable energy meter in Tesla One
- Did you need to explicitly enable “Energy meter / Neurio / RS485” in Tesla One, or was it auto-detected for you?
- After enabling the meter in Tesla One, did the Wall Connector require a reboot before it started polling?
- Do you remember if the Wall Connector started polling immediately, or only after starting a charging session?
I’m trying to rule out firmware differences or a required init sequence on the TWC side.
Thanks again, this is super valuable!
Hi Luca, quick update, I got it working on my side now 🎉
No need to reply to my previous questions.
Thanks again for the help and for sharing your setup, it pointed me in the right direction. Much appreciated!
Glad you managed to get it working! Would you mind sharing which steps you took/answer your own questions from the previous post just in case anyone stumbles upon this thread in the future?

Hi @LucaTNT,
thanks a lot for sharing this – awesome work!
I’ve tried to replicate your setup with ESPHome + RS485 to a Tesla Wall Connector, but I can’t get the Wall Connector to poll the Modbus registers at all (poll count stays at 0).
Would you mind sharing a bit more detail about your hardware and wiring? For example:
If you have a wiring diagram or photo of your setup, that would be super helpful.
Thanks again for publishing this!