(WIP - last updated 13/01/2025)
I recently bought a 2020 Nissan Leaf, having previously owned a 2015 model. The car has essentially not changed much over time, different batteries, a new body, but the interior/electrical system is quite a bit different on the newer models.
One significant change is that the OBD port is disabled when the car is switched off (other than 12V/GND). This means that it's not possible to remotely monitor the vehicle other than using the Nissan Connect EV app. If you want full-time access you need to tap directly into the CAN buses. A neat solution found by the OVMS community is to tap the harness which goes into the "CAN gateway", which has all the CAN buses. You can buy a short harness extension cable and the cut/splice/crimp/solder that without worrying about damaging the factory harness.
Splicing in
The CAN gateway box is right behind the main instrument cluster. This sounds bad, but it's actually pretty easy to get to, and far easier than attempting to access it by reaching up behind the dash (you can just about see it and might be able to reach up). The other advantage is you'll have plenty of space to secure the extension cable and make it all neat at your own leisure. Here is a good video guide. One tip, you do need to use a fair bit of force to pull out the various parts, but the clips are quite sturdy and I don't think there's much chance of breaking them (as was common on older vehicles).
![](https://blog.jingo.uk/content/images/2024/12/PXL_20241229_152149175.jpg)
![](https://blog.jingo.uk/content/images/2024/12/PXL_20241227_152847521.jpg)
![](https://blog.jingo.uk/content/images/2024/12/PXL_20241229_154959911--1-.jpg)
Which CAN bus do we need?
Below are which car functions are hooked onto which CAN buses, I compiled it by looking through the service manual's block wiring diagrams. As you can see, the EV CAN is necessary to monitor (and presumably control) the battery, the Vehicle (also called CAR) CAN is necessary to monitor the basic car stats, and the ITS CAN would be needed if you want to monitor/control the AV unit. The others might be interesting, but given they are safety-related and we are limited to 2-3 CANs on most hardware implementations probably not worth investigating further. The Diagnostic CAN is the one connected to the dashboard socket, and should be ignored.
CAN Bus | M101 Pins (H,L) | Modules connected | |
---|---|---|---|
ITS CAN | Intelligent Transport Systems, the component parts of ADAS | 7,19 | Around view monitor control unit (M32), side radar (B86,B87), distance sensor (E47), Lane camera unit (R17), ADAS control unit (M2) |
IT CAN | Presumably Information Technology | 4,16 | AV Control Unit (M96,M103), TCU (M67) |
IC CAN | Inter-chassis? | n/a | ADAS control unit (M2), Chassis control module (B64) |
Vehicle CAN | Also called "CAR CAN", the higher-level small vehicle components | 1,13 | VCM (E61), Airbag diagnostic sensor (M61), TPM (M75), ASP (vehicle approaching sound) unit (M47), BCM (M24), CPU/"IPDM E/R" (E13), Combination meter (instrument cluster/speedo) (M34) |
Chassis CAN | All the low-level vehicle bits | 9/6,21/18 | Electrically driven brake (E34), parking brake (B70), ABS (E35), chassis control module (B64), steering angle sensor (M30), EPS control unit (M37) |
EV CAN | The battery and other high voltage parts. Only active when the car is switched on or charging. | 12,24 | VCM (E61), A/C auto amp(lifier) (M55), Li-ion battery controller, traction motor inverter (F13), PDM (power delivery module) (F23), steering angle sensor (M30), EPS control unit (M37) |
Diagnostic CAN | 8/11,20/23 | Data link connector (OBD socket) (M105) | |
ADAS CAN | Advanced Driver Assistance System | 10/2,22/14 | ADAS control unit (M2), Chassis control module (B64) |
M CAN | Comms between audio system and instrument panel? | n/a | Combination meter (instrument cluster/speedo) (M34), TCU (M67), audio system (various) |
3 | 12V | ||
5/17 | Vehicle GND |
Connector pinouts
M101 CAN Gateway
This is behind the instrument cluster
![](https://blog.jingo.uk/content/images/2024/12/image.png)
![](https://blog.jingo.uk/content/images/2024/12/image-1.png)
M67 TCU (Telematics Control Unit)
This is behind the glovebox.
![](https://blog.jingo.uk/content/images/2024/12/image-2.png)
![](https://blog.jingo.uk/content/images/2024/12/image-3.png)
![](https://blog.jingo.uk/content/images/2025/01/image-1.png)
![](https://blog.jingo.uk/content/images/2025/01/image-2.png)
Testing
To fiddle around, my initial testing is an ESP32 devkit module with ESP32RET installed, and connected to SavvyCAN. Separately, I tested that setup with a second ESP32 with some simple Arduino code which pinged a "hello world". This guide is helpful in getting started from scratch doing that. The second device (connected in parallel to the same CAN bus) is an M5Stack AtomS3 with the CAN Base. I've had to add a PCB to join them because I didn't have a way to power it. That is running ESPHome to test the actual HA integration, however that ends up.
![](https://blog.jingo.uk/content/images/2024/12/PXL_20241230_174649938.jpg)
The CAN Messages
(WIP)
ESPHome Code (WIP)
NB: This was heavily ChatGPT-assisted, especially for the lambda and bitwise work.
NB2: The code includes sensors for both the EV and CAR CAN buses. Currently only sensors relating to the relevant bus will be read and updated. For example on the EV bus only the gear shift, car state, outside temperature and LBC SOC will update. On the CAR bus all the others should update.
esp32:
board: esp32-s3-devkitc-1
variant: ESP32S3
framework:
type: arduino
esphome:
name: can-test-atoms3
friendly_name: can-test-atoms3
on_boot:
- priority: -100.0
then:
- deep_sleep.prevent: deep_sleep_handler
- script.execute: test_ota
- delay: 1s
- script.execute: send_can_queries
wifi:
manual_ip:
static_ip: 192.168.1.62
packages:
wifi: !include includes/wifi.yaml
config: !include includes/config.yaml
ha: !include includes/ha.yaml
logger:
logs:
canbus: INFO
globals:
- id: otamode_default_state
type: bool
restore_value: no
initial_value: "true" # Set the default to ON (true)
canbus:
- platform: esp32_can
tx_pin: GPIO4
rx_pin: GPIO5
can_id: 999
bit_rate: 500KBPS
on_frame:
# VCM statuses
- can_id: 0x11A
use_extended_id: false
then:
- lambda: |-
if (x.size() >= 2) { // Ensure the frame has at least 2 bytes
// Extract Gear Shift (MSBits of byte 0)
uint8_t gear_shift = (x[0] & 0xF0) >> 4; // MSBits are the top 4 bits
// Extract Car Status (byte 1)
uint8_t car_status = x[1];
// Publish the gear shift state
if (gear_shift == 4) {
id(gear_shift_sensor).publish_state("Drive/B");
} else if (gear_shift == 3) {
id(gear_shift_sensor).publish_state("Neutral");
} else if (gear_shift == 2) {
id(gear_shift_sensor).publish_state("Reverse");
} else if (gear_shift == 0) {
id(gear_shift_sensor).publish_state("Park");
} else {
id(gear_shift_sensor).publish_state("Unknown");
}
// Publish the car status
if (car_status == 0x40) {
id(car_status_sensor).publish_state("ON");
} else if (car_status == 0x80) {
id(car_status_sensor).publish_state("OFF");
} else {
id(car_status_sensor).publish_state("Unknown");
}
ESP_LOGI("Gear Shift Sensor", "Gear Shift: %d", gear_shift);
ESP_LOGI("Car Status Sensor", "Car Status: %02X", car_status);
} else {
ESP_LOGW("0x11A Frame", "Frame too short to process");
}
- can_id: 0x55b
use_extended_id: false
then:
- lambda: |-
std::string b(x.begin(), x.end());
ESP_LOGI("can id 0x60d", "%s", &b[0] );
# Climate
- can_id: 0x510
use_extended_id: false
then:
- lambda: |-
std::string b(x.begin(), x.end());
ESP_LOGI("can id 0x510", "%s", &b[0] );
# Shifter position
- can_id: 0x421
use_extended_id: false
then:
- lambda: |-
std::string b(x.begin(), x.end());
ESP_LOGI("can id 0x421", "%s", &b[0] );
- can_id: 0x60d
use_extended_id: false
then:
- lambda: |-
if (x.size() >= 2) {
// Extract light and door statuses from the first byte
bool trunk_open = x[0] & (1 << 7);
bool rear_right_door_open = x[0] & (1 << 6);
bool rear_left_door_open = x[0] & (1 << 5);
bool driver_door_open = x[0] & (1 << 4);
bool passenger_door_open = x[0] & (1 << 3);
// Extract light modes (bits 1-2 of byte 0)
uint8_t light_mode = (x[0] >> 1) & 0x03;
// Extract fog and high beam lights (bit 0 of byte 0 and bit 7 of byte 1)
bool fog_lights = x[0] & (1 << 0);
bool high_beam = x[1] & (1 << 7);
// Extract indicators (bits 6 and 5 of byte 1)
bool right_indicator = x[1] & (1 << 6);
bool left_indicator = x[1] & (1 << 5);
// Extract car state (bits 1-2 of byte 1)
uint8_t car_state = (x[1] >> 1) & 0x03;
// Publish values to respective sensors
id(trunk_open_sensor).publish_state(trunk_open);
id(rear_right_door_open_sensor).publish_state(rear_right_door_open);
id(rear_left_door_open_sensor).publish_state(rear_left_door_open);
id(driver_door_open_sensor).publish_state(driver_door_open);
id(passenger_door_open_sensor).publish_state(passenger_door_open);
id(light_mode_sensor).publish_state(light_mode);
id(fog_lights_sensor).publish_state(fog_lights);
id(high_beam_sensor).publish_state(high_beam);
id(right_indicator_sensor).publish_state(right_indicator);
id(left_indicator_sensor).publish_state(left_indicator);
id(car_state_sensor).publish_state(car_state);
} else {
ESP_LOGW("can id 0x60d", "Frame too short to extract statuses");
}
- can_id: 0x763
use_extended_id: false
then:
- lambda: |-
if (x.size() >= 7) {
uint8_t parameter_id = x[3];
if (parameter_id == 0x01) {
// Odometer response
uint32_t odometer = (x[4] << 16) | (x[5] << 8) | x[6];
id(odometer_sensor).publish_state(odometer);
} else if (parameter_id == 0x25 || parameter_id == 0x26 || parameter_id == 0x27 || parameter_id == 0x28) {
// Tire pressure responses
float pressure = (x[4] * 0.068947576) * 100 / 4;
if (parameter_id == 0x25) {
id(tp_fr_sensor).publish_state(pressure);
} else if (parameter_id == 0x26) {
id(tp_fl_sensor).publish_state(pressure);
} else if (parameter_id == 0x27) {
id(tp_rr_sensor).publish_state(pressure);
} else if (parameter_id == 0x28) {
id(tp_rl_sensor).publish_state(pressure);
}
} else {
ESP_LOGW("CAN Response", "Unhandled parameter ID: 0x%02X", parameter_id);
}
} else {
ESP_LOGW("CAN Response", "Frame too short to process");
}
- can_id: 0x79A
use_extended_id: false
then:
- lambda: |-
if (x.size() >= 5) {
uint8_t parameter_id = x[3];
if (parameter_id == 0x34) {
// PlugState response
id(plug_state_sensor).publish_state(x[4]);
ESP_LOGI("PlugState Sensor", "PlugState: %d", x[4]);
}
else if (parameter_id == 0x4E) {
// Charge Mode response
id(charge_mode_sensor).publish_state(x[4]);
ESP_LOGI("Charge Mode Sensor", "Charge Mode: %d", x[4]);
}
else if (parameter_id == 0x36) {
// Calculate OBC Out Power using the formula
uint32_t obc_out_power = ((x[4] << 8) | x[5]) * 100;
id(obc_out_power_sensor).publish_state(obc_out_power);
}
else if (parameter_id == 0x03) {
// Bat 12V Voltage response
float voltage = x[4] * 0.08;
id(bat_12v_voltage_sensor).publish_state(voltage);
ESP_LOGI("Bat 12V Voltage Sensor", "Voltage: %.2f V", voltage);
}
else if (parameter_id == 0x83 && x.size() >= 6) {
// Bat 12V Current - comment: this is from a document I found, but almost certainly not correct, mine reads -2A but this would drain the battery v.quickly
int16_t current_temp = (x[4] << 8) | x[5];
if (current_temp & 32768) {
current_temp |= -65536; // Convert to signed negative
}
float current = current_temp / 256.0;
id(bat_12v_current_sensor).publish_state(current);
ESP_LOGI("Bat 12V Current Sensor", "Current: %.3f A", current);
}
} else {
ESP_LOGW("CAN Response", "Frame too short to process");
}
- can_id: 0x54C
use_extended_id: false
then:
- lambda: |-
if (x.size() >= 7) { // Ensure the frame has at least 7 bytes
// Extract raw ambient temperature from byte 6
float ambient_temp_c = (x[6] / 2.0) - 40; // Divide by 2 and subtract 40
// Publish the value to the sensor
id(ambient_temp_sensor).publish_state(ambient_temp_c);
ESP_LOGI("Ambient Temp Sensor", "Ambient Temperature: %.1f°C", ambient_temp_c);
} else {
ESP_LOGW("0x54C Frame", "Frame too short to process");
}
# Lithium Battery Controller
- can_id: 0x55B
use_extended_id: false
then:
- lambda: |-
if (x.size() >= 2) { // Ensure the frame has at least 2 bytes
// Extract the first 10 bits from the frame
uint16_t raw_lbc_soc = ((x[0] << 8) | x[1]) >> 6; // Combine first two bytes and shift right by 6
// Convert to percentage with one decimal place
float lbc_soc = raw_lbc_soc / 10.0;
// Publish the value to the sensor
id(lbc_soc_sensor).publish_state(lbc_soc);
ESP_LOGI("LBC SOC Sensor", "LBC SOC: %.1f%%", lbc_soc);
} else {
ESP_LOGW("LBC SOC Sensor", "Frame too short to process");
}
# 5C0 battery heater
# 5BC Battery in GIDS and temp for display
# 59E SOC related correction
# 55B is LBC SOC
# 55A various temps
# 54F ac auo amp temps and consumption
# 54C evap temp, cc status, fan v, outside temp, screen defrost, ac status
# 54B climate status and modes
# 54A
# 1DB LBC stats
# 1DA Inverter stuff
# 11A car statuses
binary_sensor:
- platform: template
name: "Trunk Open"
id: trunk_open_sensor
icon: "mdi:car-back"
- platform: template
name: "Rear Right Door Open"
id: rear_right_door_open_sensor
icon: "mdi:car-door"
- platform: template
name: "Rear Left Door Open"
id: rear_left_door_open_sensor
icon: "mdi:car-door"
- platform: template
name: "Driver Door Open"
id: driver_door_open_sensor
icon: "mdi:car-door"
- platform: template
name: "Passenger Door Open"
id: passenger_door_open_sensor
icon: "mdi:car-door"
- platform: template
name: "Fog Lights"
id: fog_lights_sensor
icon: "mdi:car-light-fog"
- platform: template
name: "High Beam"
id: high_beam_sensor
icon: "mdi:car-light-high"
- platform: template
name: "Right Indicator"
id: right_indicator_sensor
icon: "mdi:car-right"
- platform: template
name: "Left Indicator"
id: left_indicator_sensor
icon: "mdi:car-left"
## Deep sleep stuff
- platform: status
name: "Leaf Status"
- platform: homeassistant
id: otamode
entity_id: input_boolean.leaf_ota_mode
on_state:
then:
- lambda: |-
id(otamode_default_state) = id(otamode).state;
- platform: template
name: "OTA Mode Sensor"
lambda: |-
// Use default state until Home Assistant updates the sensor
if (id(otamode).has_state()) {
return id(otamode).state;
} else {
return id(otamode_default_state);
}
sensor:
- platform: template
name: "Light Mode"
id: light_mode_sensor
unit_of_measurement: ""
- platform: template
name: "Car State"
id: car_state_sensor
unit_of_measurement: ""
- platform: template
name: "Leaf SOC"
id: can_value_sensor
unit_of_measurement: "%"
icon: "mdi:numeric"
- platform: template
name: "Odometer"
id: odometer_sensor
unit_of_measurement: "mile"
accuracy_decimals: 0
icon: "mdi:counter"
- platform: template
name: "TP Front Right (kPa)"
id: tp_fr_sensor
unit_of_measurement: "kPa"
accuracy_decimals: 2
icon: "mdi:car-tire-alert"
- platform: template
name: "TP Front Left (kPa)"
id: tp_fl_sensor
unit_of_measurement: "kPa"
accuracy_decimals: 2
icon: "mdi:car-tire-alert"
- platform: template
name: "TP Rear Right (kPa)"
id: tp_rr_sensor
unit_of_measurement: "kPa"
accuracy_decimals: 2
icon: "mdi:car-tire-alert"
- platform: template
name: "TP Rear Left (kPa)"
id: tp_rl_sensor
unit_of_measurement: "kPa"
accuracy_decimals: 2
icon: "mdi:car-tire-alert"
- platform: template
name: "OBC Out Power (W)"
id: obc_out_power_sensor
unit_of_measurement: "W"
accuracy_decimals: 0
icon: "mdi:car-electric"
- platform: template
name: "PlugState"
id: plug_state_sensor
unit_of_measurement: ""
accuracy_decimals: 0
icon: "mdi:power-plug"
- platform: template
name: "Charge Mode"
id: charge_mode_sensor
unit_of_measurement: ""
accuracy_decimals: 0
icon: "mdi:ev-station"
- platform: template
name: "Bat 12V Voltage (V)"
id: bat_12v_voltage_sensor
unit_of_measurement: "V"
accuracy_decimals: 2
icon: "mdi:car-battery"
- platform: template
name: "Bat 12V Current (A)"
id: bat_12v_current_sensor
unit_of_measurement: "A"
accuracy_decimals: 3
icon: "mdi:current-dc"
- platform: template
name: "LBC SOC"
id: lbc_soc_sensor
unit_of_measurement: "%"
accuracy_decimals: 1
icon: "mdi:battery"
- platform: template
name: "Leaf Outside Ambient Temperature"
id: ambient_temp_sensor
unit_of_measurement: "°C"
accuracy_decimals: 1
icon: "mdi:thermometer"
text_sensor:
- platform: template
name: "Gear Shift"
id: gear_shift_sensor
icon: "mdi:car-shift-pattern"
- platform: template
name: "Car Status"
id: car_status_sensor
icon: "mdi:car-info"
interval:
- interval: 600s
then:
- script.execute: send_can_queries
#################################################
# Script to test if the otamode switch is on or off
script:
- id: send_can_queries
mode: queued
then:
- logger.log: "Sending CAN queries"
#then:
# wake up
- repeat:
count: 5
then:
- canbus.send:
data: [0x00]
can_id: 0x56E
use_extended_id: false
- delay: 50ms
# Odometer
- delay: 1000ms
- canbus.send:
data: [0x03, 0x22, 0x0E, 0x01, 0x00, 0x00, 0x00, 0x00]
can_id: 0x743
use_extended_id: false
# Tyre pressures
- delay: 100ms
- canbus.send:
data: [0x03, 0x22, 0x0E, 0x25, 0x00, 0x00, 0x00, 0x00]
can_id: 0x743
use_extended_id: false
- delay: 100ms
- canbus.send:
data: [0x03, 0x22, 0x0E, 0x26, 0x00, 0x00, 0x00, 0x00]
can_id: 0x743
use_extended_id: false
- delay: 100ms
- canbus.send:
data: [0x03, 0x22, 0x0E, 0x27, 0x00, 0x00, 0x00, 0x00]
can_id: 0x743
use_extended_id: false
- delay: 100ms
- canbus.send:
data: [0x03, 0x22, 0x0E, 0x28, 0x00, 0x00, 0x00, 0x00]
can_id: 0x743
use_extended_id: false
# OBC Out Pwr
- delay: 100ms
- canbus.send:
data: [0x03, 0x22, 0x12, 0x36, 0x00, 0x00, 0x00, 0x00]
can_id: 0x797
use_extended_id: false
# Send query for PlugState
- delay: 100ms
- canbus.send:
data: [0x03, 0x22, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00]
can_id: 0x797
use_extended_id: false
# Send query for Charge Mode
- delay: 100ms
- canbus.send:
data: [0x03, 0x22, 0x11, 0x4E, 0x00, 0x00, 0x00, 0x00]
can_id: 0x797
use_extended_id: false
# Send query for Bat 12V Voltage
- delay: 100ms
- canbus.send:
data: [0x03, 0x22, 0x11, 0x03, 0x00, 0x00, 0x00, 0x00]
can_id: 0x797
use_extended_id: false
# Send query for Bat 12V Current
- delay: 100ms
- canbus.send:
data: [0x03, 0x22, 0x11, 0x83, 0x00, 0x00, 0x00, 0x00]
can_id: 0x797
use_extended_id: false
#################################################
# Script to test if the otamode switch is on or off
# script:
- id: test_ota
mode: queued
then:
- logger.log: "Checking OTA Mode"
- if:
condition:
binary_sensor.is_on: otamode
then:
- logger.log: 'OTA Mode ON'
- deep_sleep.prevent: deep_sleep_handler
else:
- logger.log: 'OTA Mode OFF'
- deep_sleep.allow: deep_sleep_handler
- delay: 10s
- script.execute: test_ota
#################################################
#Deep Sleep
deep_sleep:
id: deep_sleep_handler
run_duration: 20s
sleep_duration: 15min
################################################
#Make a button to reboot the ESP device
button:
- platform: restart
name: ${device_name} Restart
Result in Home Assistant
![](https://blog.jingo.uk/content/images/2024/12/image-6.png)
![](https://blog.jingo.uk/content/images/2024/12/image-5.png)
AliExpress links - just things I have used which work well
ESP32-C6 (smaller board): https://www.aliexpress.com/item/1005007399157637.html
ESP32-C6 (larger board): https://www.aliexpress.com/item/1005007171241033.html
CAN transceiver: https://www.aliexpress.com/item/1005006734943258.html (there's a smaller board without a screw terminal sold, I've never got those to work reliably, and in one case it appeared to badly overheat - this one has worked very well)
3A step-down converter: https://www.aliexpress.com/item/1005006245122273.html
CANable USB to CAN converter - useful for in-car recording using SavvyCAN: https://www.aliexpress.com/item/1005005721849902.html
Prototyping PCBs: https://www.aliexpress.com/item/1005006665029598.html
ESP32 touch screen ("CYD"): https://www.aliexpress.com/item/1005006160645147.html