Skip to content

Instantly share code, notes, and snippets.

@JavanXD
Last active November 24, 2025 16:30
Show Gist options
  • Select an option

  • Save JavanXD/696d026ef202a7d6455ed4745df63e39 to your computer and use it in GitHub Desktop.

Select an option

Save JavanXD/696d026ef202a7d6455ed4745df63e39 to your computer and use it in GitHub Desktop.
ESPHome/Homeassistant - Sniff CAN-Bus (MCP2515) from LEDA LUC2
substitutions:
name: esphome-ledaluc2
friendly_name: ESPHome LEDA LUC2
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2023.6.0 # Use a stable ESPHome version for compatibility
name_add_mac_suffix: false # Prevent adding MAC suffix to the device name
project:
name: esphome.web
version: dev # Version of the project
esp32:
board: esp32dev # Specify the ESP32 development board
framework:
type: arduino # Use the Arduino framework
# Enable logging for debugging purposes
logger:
# Enable Home Assistant API with encryption key from secrets.yaml
api:
encryption:
key: !secret api_key
# Enable over-the-air updates for firmware
ota:
platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Fallback hotspot in case Wi-Fi connection fails
ap:
ssid: "ESPHome-LEDALUC2"
password: !secret ap_password
# Allow Wi-Fi provisioning via serial connection
improv_serial:
# Enable captive portal for Wi-Fi provisioning via the fallback hotspot
captive_portal:
# Import specific components from an example configuration without overwriting local settings
dashboard_import:
package_import_url: github://esphome/example-configs/esphome-web/esp32.yaml@main
import_full_config: false
# Host a simple web server (e.g., for Improv Wi-Fi)
web_server:
# Configure the SPI interface for the MCP2515 CAN bus module
spi:
clk_pin: GPIO22 # Clock pin
miso_pin: GPIO17 # Master In Slave Out pin
mosi_pin: GPIO21 # Master Out Slave In pin
# Configure the CAN bus using the MCP2515 module
canbus:
- platform: mcp2515
cs_pin: GPIO16
can_id: 0x28A
bit_rate: 125KBPS
on_frame:
- can_id: 0x28A
then:
- lambda: |-
// Log all received CAN frames for debugging
if (x.size() > 0 && x.size() < 8) {
// Log the received frame data safely
std::string frame_data;
for (size_t i = 0; i < x.size(); i++) {
char byte_str[5];
snprintf(byte_str, sizeof(byte_str), "0x%02X ", x[i]);
frame_data += byte_str;
}
// Log the frame data in a single line
ESP_LOGD("CAN", "Received CAN Frame (Size: %d bytes): %s", x.size(), frame_data.c_str());
// Check for specific sizes and log states
if (x.size() == 2) {
ESP_LOGI("CAN", "Interpreted State: Ventilation turned OFF via Display");
} else if (x.size() == 1) {
ESP_LOGI("CAN", "Interpreted State: Ventilation turned ON via Display");
} else {
ESP_LOGW("CAN", "Interpreted State: Unknown or Additional Data");
}
}
else if (x.size() == 8) {
uint8_t frame_type = x[0]; // First byte determines the frame type
// Frame type 0x00: Pressure difference and exhaust temperature
if (frame_type == 0x00) {
// Extract pressure difference from Byte 2
float pressure_difference = x[1] * 0.1f; // Convert to Pascals
// Check for adjustment flag in Byte 3
if (x[2] == 0x81) {
const float PRESSURE_ADJUSTMENT_VALUE = 25.5f;
pressure_difference += PRESSURE_ADJUSTMENT_VALUE;
ESP_LOGI("CAN", "Adjustment Applied: +%.1f Pa", PRESSURE_ADJUSTMENT_VALUE);
}
// Extract exhaust temperature from Byte 4
uint16_t raw_temp = x[3] | (x[4] << 8); // Little Endian
float exhaust_temperature = static_cast<float>(raw_temp); // °C
// Log decoded values
ESP_LOGI("CAN", "Pressure: %.1f Pa, Temperature: %.1f °C", pressure_difference, exhaust_temperature);
// Publish to sensors
id(pressure_difference_sensor).publish_state(pressure_difference);
id(exhaust_temperature_sensor).publish_state(exhaust_temperature);
}
// Frame type 0x01: Ventilation status
else if (frame_type == 0x01) {
// Extract ventilation status from Byte 6
bool ventilation_active = (x[5] == 0x01);
// Log ventilation status
ESP_LOGI("CAN", "Ventilation Active: %s", ventilation_active ? "Yes" : "No");
ESP_LOGD("CAN", "Ventilation Bytes: Data=%02X %02X %02X %02X %02X %02X %02X %02X",
x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7]);
// Publish to binary sensor
id(ventilation_status_sensor).publish_state(ventilation_active);
}
// Frame type 0x09: Heartbeat signal
else if (frame_type == 0x09) {
// Log loop and counter for reference
ESP_LOGD("CAN", "Heartbeat (0x09): Loop=%02X, Counter=%02X", x[2], x[1]);
}
// Frame type 0x55: Heartbeat signal
else if (frame_type == 0x55) {
// Log loop and counter for reference
ESP_LOGD("CAN", "Heartbeat (0x55): Loop=%02X, Counter=%02X", x[2], x[1]);
}
else if (frame_type == 0x80) {
ESP_LOGW("CAN", "Error Frame (0x80): Data=%02X %02X %02X %02X %02X %02X %02X %02X",
x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7]);
}
else if (frame_type == 0x81) {
ESP_LOGW("CAN", "Error Frame (0x81): Data=%02X %02X %02X %02X %02X %02X %02X %02X",
x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7]);
}
// Handle unknown frame types
else {
ESP_LOGW("CAN", "Unknown Frame Type. Data=%02X %02X %02X %02X %02X %02X %02X %02X",
x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7]);
}
} else {
ESP_LOGW("CAN", "Unexpected frame size: %d bytes", x.size());
}
# Define template sensors to hold the values published from the CAN bus data
sensor:
- platform: template
name: "Pressure Difference"
id: pressure_difference_sensor
unit_of_measurement: "Pa"
accuracy_decimals: 1
device_class: pressure # Device class for pressure sensors
icon: "mdi:air-filter" # Optional custom icon for visual clarity
filters:
- throttle_average: 3s
- platform: template
name: "Exhaust Temperature"
id: exhaust_temperature_sensor
unit_of_measurement: "°C"
accuracy_decimals: 1
device_class: temperature # Device class for temperature sensors
icon: "mdi:thermometer"
filters:
- throttle_average: 3s
binary_sensor:
- platform: template
name: "Ventilation Status"
id: ventilation_status_sensor
device_class: running # Device class for indicating system activity
icon: "mdi:fan" # Optional custom icon for ventilation
@Hooorny
Copy link

Hooorny commented Oct 29, 2025

Ich habe diese Ausreißer mittels Filters geglättet, funktioniert sehr gut.

# Define template sensors to hold the values published from the CAN bus data
sensor:
  - platform: template
    name: "Pressure Difference"
    id: pressure_difference_sensor
    unit_of_measurement: "Pa"
    accuracy_decimals: 1
    device_class: pressure  # Device class for pressure sensors
    state_class: measurement
    icon: "mdi:air-filter"  # Optional custom icon for visual clarity
    update_interval: never
    filters:
      - median:
          window_size: 5
          send_every: 3
          send_first_at: 1
   
  - platform: template
    name: "Exhaust Temperature"
    id: exhaust_temperature_sensor
    unit_of_measurement: "°C"
    accuracy_decimals: 1
    device_class: temperature  # Device class for temperature sensors
    state_class: measurement
    icon: "mdi:thermometer"
    update_interval: never
    filters:
      - median:
          window_size: 5
          send_every: 3
          send_first_at: 1        

binary_sensor:
  - platform: template
    name: "Ventilation Status"
    id: ventilation_status_sensor    
    device_class: running  # Device class for indicating system activity
    icon: "mdi:fan"  # Optional custom icon for ventilation
    filters:
      - delayed_on_off: 1s

button:
  - platform: restart
    name: "CAN-Sniffer Restart"

@arthurichkowski
Copy link

@Hooorny vielen Dank! ...wollte zuerst meine Spannungsquelle genauer einstellen aber mit deinem Filter läuft es auch stabil.

@Hooorny
Copy link

Hooorny commented Nov 3, 2025

Bei mir bleibt der MCP immer noch ab und zu nach einigen Stunden hängen ... deswegen habe ich einen Watchdog eingebaut, der den Restart-Button auslöst, wenn 1 Minute keine CAN-Daten "empfangen" wurden. Funktioniert für mich jetzt ausreichend gut ;)

# --- Watchdog: merkt sich, wann zuletzt ein Frame kam
globals:
  - id: can_last_seen_ms
    type: uint32_t
    restore_value: no
    initial_value: '0'

interval:
  - interval: 10s
    then:
      - lambda: |-
          const uint32_t now = millis();
          if (id(can_last_seen_ms) != 0 && (now - id(can_last_seen_ms)) > 60000) {  // > 1 min ohne Frames
            ESP_LOGW("CAN", "No CAN frames for 1 minute -> Restarting node");
            id(restart_btn).press();
          }

# Configure the CAN bus using the MCP2515 module
canbus:
  - platform: mcp2515
    cs_pin: GPIO16   
    id: ledaluc2_canbus 
    can_id: 0x28A
    clock: 8MHZ
    bit_rate: 125KBPS
    on_frame:
      can_id: 0x28A
      then:
        - lambda: |-
              // Zeitpunkt merken (für Watchdog)
              id(can_last_seen_ms) = millis();

              // Kompakter, allokationsarmer Dump statt eigener String-Bastelei
              ESP_LOGD("CAN", "RX: %s", format_hex_pretty(x).c_str());

              // Kleiner Bounds-Helper
              auto b = [&](size_t i)->uint8_t { return i < x.size() ? x[i] : 0; };

              if (x.size() > 0 && x.size() < 8) {
                if (x.size() == 2) {
                  ESP_LOGI("CAN", "Interpreted State: Ventilation turned OFF via Display");
                } else if (x.size() == 1) {
                  ESP_LOGI("CAN", "Interpreted State: Ventilation turned ON via Display");
                } else {
                  ESP_LOGW("CAN", "Interpreted State: Unknown or Additional Data");
                }
              }
              else if (x.size() == 8) {
                uint8_t frame_type = b(0);

                if (frame_type == 0x00) {
                  // Byte1: Druck *0.1 Pa
                  float pressure_difference = b(1) * 0.1f;
                  if (b(2) == 0x81) {
                    const float PRESSURE_ADJUSTMENT_VALUE = 25.5f;
                    pressure_difference += PRESSURE_ADJUSTMENT_VALUE;
                    ESP_LOGI("CAN", "Adjustment Applied: +%.1f Pa", PRESSURE_ADJUSTMENT_VALUE);
                  }
                  uint16_t raw_temp = b(3) | (uint16_t(b(4)) << 8);
                  float exhaust_temperature = static_cast<float>(raw_temp);

                  ESP_LOGI("CAN", "Pressure: %.1f Pa, Temperature: %.1f °C",
                          pressure_difference, exhaust_temperature);

                  id(pressure_difference_sensor).publish_state(pressure_difference);
                  id(exhaust_temperature_sensor).publish_state(exhaust_temperature);
                }
                else if (frame_type == 0x01) {
                  bool ventilation_active = (b(5) == 0x01);
                  ESP_LOGI("CAN", "Ventilation Active: %s", ventilation_active ? "Yes" : "No");
                  ESP_LOGD("CAN", "Ventilation Bytes: %s", format_hex_pretty(x).c_str());
                  id(ventilation_status_sensor).publish_state(ventilation_active);
                }
                else if (frame_type == 0x09 || frame_type == 0x55) {
                  ESP_LOGD("CAN", "Heartbeat (0x%02X): Loop=%02X, Counter=%02X", frame_type, b(2), b(1));
                }
                else if (frame_type == 0x80 || frame_type == 0x81) {
                  ESP_LOGW("CAN", "Error Frame (0x%02X): %s", frame_type, format_hex_pretty(x).c_str());
                }
                else {
                  ESP_LOGW("CAN", "Unknown Frame Type: %s", format_hex_pretty(x).c_str());
                }
              } else {
                ESP_LOGW("CAN", "Unexpected frame size: %d bytes", x.size());
              }

# Define template sensors to hold the values published from the CAN bus data
sensor:
  - platform: template
    name: "Pressure Difference"
    id: pressure_difference_sensor
    unit_of_measurement: "Pa"
    accuracy_decimals: 1
    device_class: pressure  # Device class for pressure sensors
    state_class: measurement
    icon: "mdi:air-filter"  # Optional custom icon for visual clarity
    update_interval: never
    filters:
      - timeout: 20s  # sent value will be NaN
      - median:
          window_size: 5
          send_every: 3
          send_first_at: 1
   
  - platform: template
    name: "Exhaust Temperature"
    id: exhaust_temperature_sensor
    unit_of_measurement: "°C"
    accuracy_decimals: 1
    device_class: temperature  # Device class for temperature sensors
    state_class: measurement
    icon: "mdi:thermometer"
    update_interval: never
    filters:
      - timeout: 20s  # sent value will be NaN
      - median:
          window_size: 5
          send_every: 3
          send_first_at: 1        

binary_sensor:
  - platform: template
    name: "Ventilation Status"
    id: ventilation_status_sensor    
    device_class: running  # Device class for indicating system activity
    icon: "mdi:fan"  # Optional custom icon for ventilation
    filters:
      - delayed_on_off: 1s

button:
  - platform: restart
    id: restart_btn
    name: "CAN-Sniffer Restart"

@Blaumicha
Copy link

Blaumicha commented Nov 3, 2025

Auch ich kann endlich Erfolg vermelden. Bei mir lag es auch an der Spannungsversorgung, es dauerte etwas länger da auch das getestete USB Netzteil nicht geholfen hat. Ich habe noch 1yF Tantalelko auf die Platine gelötet und mittel Labornetzteil alles zum laufen bekommen. Die Schaltung kommt jetzt in den Keller und wird an ein Hutschienen Netzteil angeschlossen. Wenn auch das läuft poste ich gern noch mal den Netzteiltyp.

Vielen Dank an alle die hier gepostet haben, hat mir sehr geholfen!

HG Michael

@Blaumicha
Copy link

Ist nun in den Keller umgezogen und läuft mit einem MEAN WELL Netzteil 12W 5V 2,4A ; MeanWell HDR-15-5 ; DIN-Rail Trafo, auf anhieb.

@ChristophWeigert
Copy link

ChristophWeigert commented Nov 8, 2025

Könnt ihr nochmal Fotos machen wie was angelötet/verbunden ist? Ich bekomme es nicht hin Daten zu empfangen 😢

Hier Bilder wie es aktuell aussieht: https://immich.weigert.cc/s/ledalux2

Log:


Time | Level | Tag | Message
-- | -- | -- | --
18:10:17 | [I] | [safe_mode:042] | Boot seems successful; resetting boot loop counter
18:10:17 | [D] | [esp32.preferences:149] | Writing 1 items: 0 cached, 1 written, 0 failed
18:13:42 | [D] | [api:160] | Accept 192.168.1.108
18:13:42 | [D] | [api.connection:1383] | ESPHome Logs 2025.10.4 (192.168.1.108) connected
18:13:43 | [I] | [app:185] | ESPHome version 2025.10.4 compiled on Nov  8 2025, 17:05:29
18:13:43 | [I] | [app:187] | Project esphome.web version dev
18:13:43 | [C] | [wifi:679] | WiFi:
18:13:43 | [C] | [wifi:458] | Local MAC: 8C:4F:00:28:33:58
18:13:43 | [C] | [wifi:465] | IP Address: c0a8:647e:61c:403f:98f3:d80:80ec:fb3f
18:13:43 | [C] | [wifi:469] | SSID: �[5m'W-IoT'
18:13:43 | [C] | [logger:261] | Log
18:13:43 | [C] | [logger:267] | Log Baud Rate: 11
18:13:43 | [C] | [logger:274] | Task Log Buffer Size: 768
18:13:43 | [C] | [spi:067] | SPI bus:
18:13:43 | [C] | [spi:068] | CLK Pin: GPIO22
18:13:43 | [C] | [spi:069] | SDI Pin: GPIO17
18:13:43 | [C] | [spi:070] | SDO Pin: GPIO21
18:13:43 | [C] | [spi:075] | Using HW SPI: SPI
18:13:43 | [C] | [canbus:020] | config standard id=0x28a
18:13:43 | [E] | [component:154] | canbus is marked FAILED: unspecified
18:13:43 | [C] | [captive_portal:116] | Captive Portal:
18:13:43 | [C] | [web_server:317] | Web Ser
18:13:43 | [C] | [esphome.ota:093] | Over-The-Air upda
18:13:43 | [C] | [esphome.ota:100] | Password configured
18:13:43 | [C] | [safe_mode:018] | Safe M
18:13:43 | [C] | [web_server.ota:241] | Web Server OTA
18:13:43 | [C] | [api:222] | Ser
18:13:43 | [C] | [api:229] | Noise encryption: YES
18:13:43 | [C] | [improv_serial:031] | Improv Serial:
18:13:43 | [C] | [mdns:179] | m

@Maigus2510
Copy link

Hey, könnte mir einer von euch so ein Teio zusammenbauen und zuschicken? Bin selbst zu dumm dazu. Bezahlung natürlich nach eurem Preis ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment