Skip to content

Instantly share code, notes, and snippets.

@jnimmo
Forked from mukowman/wican.yaml
Last active April 14, 2024 13:29
Show Gist options
  • Save jnimmo/5182c59011a16c94293935d06c7857a9 to your computer and use it in GitHub Desktop.
Save jnimmo/5182c59011a16c94293935d06c7857a9 to your computer and use it in GitHub Desktop.
WiCAN Esphome
substitutions:
device_name: car
charging_voltage_threshold: '13.4'
low_voltage_threshold: '12.0'
low_voltage_sleep_duration: 1hours
deep_sleep_duration: 3min # 5 * 60000
# 5 min * 60000 = 300000
first_boot_run_duration: '300000'
abrp_key: !secret abrp_key
abrp_token: !secret abrp_token
hotspot1_ssid: !secret hotspot1_ssid
hotspot2_ssid: !secret hotspot2_ssid
hotspot3_ssid: !secret hotspot3_ssid
hotspot1_password: !secret hotspot1_password
hotspot2_password: !secret hotspot2_password
hotspot3_password: !secret hotspot3_password
esp32:
variant: ESP32C3
board: esp32-c3-devkitm-1
framework:
type: arduino
globals:
- id: sleep_mode_enabled
type: 'bool'
restore_value: yes
initial_value: 'true'
- id: added_wifi_hotspots
type: 'bool'
restore_value: no
initial_value: 'false'
- id: last_odometer_value
type: 'float'
restore_value: yes
initial_value: '60590'
- id: pending_status_update
type: 'bool'
restore_value: yes
initial_value: 'false'
- id: last_hv_charging_state
type: 'bool'
restore_value: yes
initial_value: 'false'
- id: last_ac_present_state
type: 'bool'
restore_value: yes
initial_value: 'false'
- id: last_state_of_charge
type: uint8_t
restore_value: yes
initial_value: '0'
- id: frame_received
type: 'bool'
restore_value: no
initial_value: 'false'
- id: received_data
type: 'std::vector<uint8_t>'
restore_value: no
initial_value: 'std::vector<uint8_t>()'
- id: expected_length
type: 'int'
restore_value: no
initial_value: '0'
- id: next_frame_seq
type: 'uint8_t'
restore_value: no
initial_value: '0x21' # Start wfith 0x21 after FF
- id: can_soh_raw
type: 'float'
restore_value: no
initial_value: 'NAN'
- id: can_soh_display
type: 'float'
restore_value: no
initial_value: 'NAN'
- id: can_soc_bms
type: 'float'
restore_value: no
initial_value: 'NAN'
- id: can_battery_dc_voltage
type: 'float'
restore_value: no
initial_value: '0'
- id: can_battery_current_signed
type: 'float'
restore_value: no
initial_value: '0'
- id: can_battery_power
type: 'float'
restore_value: no
initial_value: '0'
- id: can_operating_hours_float
type: 'float'
restore_value: no
initial_value: 'NAN'
- id: can_speed_mph
type: 'float'
restore_value: no
initial_value: 'NAN'
- id: can_ignition_status
type: 'bool'
restore_value: no
initial_value: 'NAN'
# mqtt:
# broker: ef7c6adc24294c638e47a467af6280eb.s2.eu.hivemq.cloud
# port: 8883
# username: esphome_car
# password: !secret car_mqqt_password
# skip_cert_cn_check: true
# certificate_authority: |
# -----BEGIN CERTIFICATE-----
# MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw
# TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
# cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw
# WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
# RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
# AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP
# R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx
# sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm
# NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg
# Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG
# /kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC
# AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB
# Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA
# FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw
# AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw
# Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB
# gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W
# PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl
# ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz
# CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm
# lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4
# avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2
# yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O
# yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids
# hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+
# HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv
# MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX
# nLRbwHOoq7hHwg==
# -----END CERTIFICATE-----
esphome:
name: ${device_name}
on_shutdown:
then:
- if:
condition:
- lambda: return id(state_of_charge_bms).has_state();
then:
globals.set:
id: last_state_of_charge
value: !lambda 'return round(id(state_of_charge_bms).state);'
- if:
condition:
lambda: return id(odometer).has_state();
then:
globals.set:
id: last_odometer_value
value: !lambda 'return id(odometer).state;'
on_boot:
priority: 210
then:
# Check if this is not a deep sleep wakeup
- light.turn_on:
id: blue_led
effect: "sleeping"
- script.execute: restore_sensor_values
- wait_until:
# If the battery voltage indicates the car is running, or the canbus responds that the car is charging, then wake up:
condition:
lambda: |-
return id(starter_battery_voltage).has_state();
timeout: 6s
- if:
condition:
and: # Deep sleep wakeup, check if we need to go back to sleep
- binary_sensor.is_off: first_boot
- sensor.in_range: # Low battery condition
id: starter_battery_voltage
below: '${low_voltage_threshold}'
then:
- logger.log: "Low voltage, going back to sleep."
- deep_sleep.enter:
id: deep_sleep_1
sleep_duration: ${low_voltage_sleep_duration}
- if:
condition:
binary_sensor.is_on: first_boot
then:
lambda: id(deep_sleep_1)->set_run_duration(${first_boot_run_duration});
- script.execute: update_car_sensors
- script.wait: update_car_sensors
# - wait_until: # Wait until we have a reponse back, or skip waiting if this is the first boot
# condition:
# lambda: return (id(ignition).has_state() || id(first_boot).state);
# timeout: 5s
- if: # Pending status update
condition: # If ignition is off, or has no state,
lambda: return ((!id(ignition).has_state() || !id(ignition).state) && !id(pending_status_update) && !id(first_boot).state);
then:
- logger.log: "No updates to send, going back to sleep."
- deep_sleep.enter:
id: deep_sleep_1
sleep_duration: ${deep_sleep_duration}
- wifi.enable:
- light.turn_on:
id: blue_led
effect: "None"
- script.execute: add_wifi_hotspots
logger:
level: INFO #NONE# ERROR #INFO #DEBUG #VERBOSE
baud_rate: 0 #to disable logging via UART
# logs:
# text_sensor: ERROR
# homeassistant.sensor: ERROR
# canbus: INFO
# light: INFO
deep_sleep:
id: deep_sleep_1
run_duration: 30s
sleep_duration: 2min
api:
reboot_timeout: 0s
on_client_connected:
- logger.log:
format: "Client %s connected to API with IP %s"
args: ["client_info.c_str()", "client_address.c_str()"]
- delay: 1s
- globals.set:
id: pending_status_update
value: 'false'
# - if:
# condition:
# and:
# - switch.is_on: sleep_mode
# - binary_sensor.is_off: ignition
# # and not the first boot
# - lambda: return esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_UNDEFINED;
# then:
# - delay: 5s
# - deep_sleep.enter:
# id: deep_sleep_1
# sleep_duration: ${deep_sleep_duration}
# services:
# - service: send_canbus
# variables:
# canbus_id: int
# data: string
# then:
# - lambda: |-
# // Split the input string into tokens using commas and spaces as delimiters
# const char* hexString = variables.data.c_str();
# int canbusId = variables.canbus_id;
# std::vector<uint8_t> byteList;
# // Tokenize the input string by commas and spaces and convert to bytes
# char* token;
# char* saveptr;
# token = strtok_r(const_cast<char*>(hexString), ", ", &saveptr);
# while (token != NULL) {
# byteList.push_back(strtol(token, NULL, 16));
# token = strtok_r(NULL, ", ", &saveptr);
# }
# // Send the byte list to the CAN bus using canbus.send
# id(can0).send(canbusId, byteList);
time:
# - platform: homeassistant
# id: ha_time
- platform: sntp
id: sntp_time
servers: 162.159.200.1
# web_server:
# version: 2
# ota: false
# include_internal: true
wifi:
id: wifi_component
fast_connect: true
enable_on_boot: false
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
manual_ip:
static_ip: 192.168.178.210
gateway: 192.168.178.1
subnet: 255.255.255.0
dns1: 192.168.178.1
output:
- id: blue_led_output
platform: gpio
pin: 7
- id: green_led_output
platform: gpio
pin: { number: 8, inverted: true }
- id: yellow_led_output
platform: gpio
pin: { number: 9, inverted: true }
light:
- platform: binary
id: blue_led
output: blue_led_output
internal: True
effects:
- strobe:
name: "sleeping"
colors:
- state: True
duration: 500ms
- state: False
duration: 500ms
- strobe:
name: "sleep"
colors:
- state: True
duration: 2000ms
- state: False
duration: 2000ms
restore_mode: ALWAYS_ON
- platform: binary
id: green_led
output: green_led_output
effects:
- strobe:
name: "searching"
colors:
- state: True
duration: 4000ms
- state: False
duration: 100ms
- platform: binary
id: yellow_led
output: yellow_led_output
button:
- platform: restart
name: "${device_name} Restart"
- platform: template
name: Query Car Status
id: query_status
on_press:
- script.execute: update_car_sensors
canbus:
- platform: esp32_can
id: can0
tx_pin: 0
rx_pin: 3
bit_rate: 500kbps
can_id: 0 # mandatory but we do not use it
on_frame:
- can_id: 0
can_id_mask: 0
then:
- lambda: |-
auto data_pretty = remote_transmission_request ? "n/a" : format_hex_pretty(x).c_str();
ESP_LOGD("eup_dump", "can_id: 0x%08x, rtr: %d, length: %d, content: %s", can_id, remote_transmission_request, x.size(), data_pretty);
uint8_t frame_type = (x[0] & 0xF0) >> 4; // Extract the first 4 bits
uint8_t frame_index = x[0] & 0x0F; // Extract the next 4 bits
if (frame_type == 0x00) {
// This is a single-frame message
id(expected_length) = x[0] & 0x0F; // Extract the length from the first 4 bits
id(received_data).clear();
id(frame_received) = true;
id(next_frame_seq) = 0x01; // Reset for next multi-frame message
// Append the data (excluding the first byte which contains the length)
for (int i = 1; i < x.size(); i++) {
id(received_data).push_back(x[i]);
}
// Extract any single message data here
} else if (frame_type == 0x01) {
// This is the start of a multi-frame message
// Extract the message length from the next 12 bits
id(expected_length) = ((x[0] & 0x0F) << 8) | x[1];
ESP_LOGD("eup_dump", "multi-frame message header, total frame length: %d", id(expected_length));
id(received_data).clear();
id(next_frame_seq) = 0x01;
id(frame_received) = false;
// Append the first part of the data (after the size bytes)
for (int i = 2; i < 8; i++) {
id(received_data).push_back(x[i]);
}
// Send Flow Control frame to request Consecutive Frames
id(can0)->send_data(can_id - 0x08, false, {0x30, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
} else if (frame_type == 0x02) {
ESP_LOGD("eup_dump", "Received frame_index: %02X, Expected next_frame_seq: %02X", frame_index, id(next_frame_seq));
if (frame_index == id(next_frame_seq)) {
// This is a Consecutive Frame
for (int i = 1; i < 8; i++) {
id(received_data).push_back(x[i]);
}
id(next_frame_seq) = (id(next_frame_seq) == 0x0F) ? 0x00 : id(next_frame_seq) + 1;
// Check if reassembly is complete
if (id(received_data).size() >= id(expected_length)) {
// Convert the vector to a formatted string
std::string assembled_data;
for (uint8_t byte : id(received_data)) {
char buffer[4];
sprintf(buffer, "%02X.", byte);
assembled_data += buffer;
}
// Print the formatted string
ESP_LOGI("eup_dump", "Recieved completed canbus frame");
ESP_LOGD("AssembledFrame", "Complete Frame: %s", assembled_data.c_str());
if (can_id == 0x7EC && id(received_data)[1] == 0x05) {
if (id(received_data).size() >= 45) {
float soh_raw = ((id(received_data)[27] << 8) | id(received_data)[28]) / 10.0;
id(state_of_health).publish_state(soh_raw);
}
// State of Charge Display
float soc_display = (id(received_data).size() > 33) ? id(received_data)[33] / 2 : -1;
id(state_of_charge).publish_state(soc_display);
}
else if (can_id == 0x7EC && id(received_data)[1] == 0x01) {
// State of Charge BMS
float soc_bms = (id(received_data).size() > 6) ? id(received_data)[6] / 2 : -1;
id(state_of_charge_bms).publish_state(soc_bms);
id(state_of_charge_bms_calibrated).publish_state(soc_bms);
// HV Battery Voltage
id(can_battery_dc_voltage) = ((id(received_data)[14] * 256) + id(received_data)[15]) / 10.0;
// Battery Current
int signed_byte_k = (id(received_data)[12] < 128) ? id(received_data)[12] : id(received_data)[12] - 256;
id(can_battery_current_signed) = (signed_byte_k * 256 + id(received_data)[13]) / 10.0;
// Charging Status
bool hv_charging_status = (id(received_data)[11] >> 7) & 1;
id(hv_charging).publish_state(hv_charging_status);
bool ac_present_status = (id(received_data)[11] >> 5) & 1;
id(ac_present).publish_state(ac_present_status);
// CCS charging port
bool fast_charging_status = (id(received_data)[11] >> 6) & 1;
id(fast_charging).publish_state(fast_charging_status);
// BMS Ignition (published at end of lambda)
id(can_ignition_status) = (id(received_data)[52] >> 2) & 1;
// Operating hours
float operating_hours_float = ((id(received_data)[48] << 24) | (id(received_data)[49] << 16) | (id(received_data)[50] << 8) | id(received_data)[51]) / 3600.0;
id(operating_hours).publish_state(operating_hours_float);
// Extract and publish Cumulative Energy Charged
if (id(received_data).size() > 48) {
float cumulative_energy_charged_kw = ((id(received_data)[40] << 24) | (id(received_data)[41] << 16) | (id(received_data)[42] << 8) | id(received_data)[43]) / 10.0;
float cumulative_energy_discharged_kw = ((id(received_data)[44] << 24) | (id(received_data)[45] << 16) | (id(received_data)[46] << 8) | id(received_data)[47]) / 10.0;
id(cumulative_energy_charged).publish_state(cumulative_energy_charged_kw);
id(cumulative_energy_discharged).publish_state(cumulative_energy_discharged_kw);
}
// Boot flags
if (soc_bms != id(last_state_of_charge) || hv_charging_status != id(last_hv_charging_state) || ac_present_status != id(last_ac_present_state)) {
id(pending_status_update) = true;
}
}
else if (can_id == 0x7EA && id(received_data)[1] == 0x01) {
// response from 7E2 (published end of lambda)
id(can_speed_mph) = ((signed int)(id(received_data)[16]) * 256 + id(received_data)[15]) / 100.0;
}
else if (can_id == 0x7EE && id(received_data)[1] == 0x01 && id(received_data).size() > 14) {
// response to 0x7E6, containing ambient temperature
float ambient_temperature_float = (((id(received_data)[14] < 128) ? id(received_data)[14] : id(received_data)[14] - 256) - 80) / 2.0;
id(ambient_temperature).publish_state(ambient_temperature_float);
}
else if (can_id == 0x7CE && id(received_data)[1] == 0xB0 && id(received_data).size() > 11) {
// response to 7C6, containing odometer
uint32_t odometer_value = (static_cast<uint32_t>(id(received_data)[9]) << 16) |
(static_cast<uint32_t>(id(received_data)[10]) << 8) |
static_cast<uint32_t>(id(received_data)[11]);
id(odometer).publish_state(odometer_value);
}
id(next_frame_seq) = 0x01; // Reset for next multi-frame message
id(frame_received) = true;
}
}
} else {
ESP_LOGI("eup_dump", "Unexpected frame");
}
// Update sensors
id(ignition).publish_state(id(can_ignition_status));
id(real_vehicle_speed).publish_state(id(can_speed_mph) * 1.60934);
id(battery_current).publish_state(id(can_battery_current_signed));
id(battery_power).publish_state((id(can_battery_dc_voltage) * id(can_battery_current_signed))/1000);
id(hv_battery_voltage).publish_state(id(can_battery_dc_voltage));
switch:
- platform: gpio
id: can_enabled
name: "Enable CAN Interface"
pin:
number: 6
inverted: true
restore_mode: ALWAYS_ON
- platform: template
id: sleep_mode
name: "${device_name} Sleep Mode"
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- logger.log: "Sleep mode: ON"
on_turn_off:
- logger.log: "Sleep mode: OFF"
- platform: template
id: send_abrp_telemetry
name: "Send ABRP telemetry while driving"
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
- platform: template
id: send_abrp_telemetry_charging
name: "Send ABRP telemetry while charging"
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
# - platform: template
# id: advertise_blehome
# name: "Advertise BLE Home packets"
# optimistic: true
# restore_mode: RESTORE_DEFAULT_OFF
# esp32_ble:
# id: ble
# esp32_ble_server:
# id: ble_server
interval:
# ABRP Telemetry while charging
# - interval: 5min
# then:
# if:
# condition:
# and:
# - wifi.connected:
# - switch.is_on: send_abrp_telemetry_charging
# - binary_sensor.is_on: ignition
# then:
# script.execute: send_abrp_telemetry_script
- interval: 30s
then:
- script.execute: update_car_sensors
- if:
condition:
and:
- wifi.connected:
- switch.is_on: send_abrp_telemetry
then:
- script.wait: update_car_sensors
- script.execute: send_abrp_telemetry_script
- interval: 5min
then:
- if:
condition:
- wifi.connected:
then:
- script.execute: send_home_assistant_webhook
- interval: 1s
then:
if:
condition:
wifi.connected:
then:
- light.turn_on:
id: green_led
effect: "None"
else:
- light.turn_off: green_led
- if:
condition:
wifi.enabled:
then:
light.turn_on:
id: green_led
effect: "searching"
# - interval: 30s
# then:
# if:
# condition:
# switch.is_on: advertise_blehome
# then:
# - lambda: |-
# ESP_LOGD("advertisement", "Refreshing BTHome advertisement");
# // Define the BTHome manufacturer data structure
# struct manufacturer_data_t {
# uint16_t uuid;
# uint8_t device_info;
# uint8_t battery_id;
# uint8_t battery_value;
# uint8_t power_id;
# int16_t power_value;
# uint8_t speed_id;
# uint16_t speed_value;
# uint8_t battery_charging_id;
# uint8_t battery_charging_value;
# };
# static manufacturer_data_t* bthome_data = new manufacturer_data_t;
# // Set the BTHome data
# bthome_data->uuid = 0xFCD2; // BTHome UUID
# bthome_data->device_info = 0x40; // BTHome device info
# bthome_data->battery_id = 0x01;
# bthome_data->battery_value = (uint8_t)id(state_of_charge_bms).state;
# bthome_data->power_id = 0x0B;
# int16_t power_in_kW = (int16_t)(id(battery_power).state * 100); // Adjusted for 16-bit signed integer
# bthome_data->power_value = power_in_kW;
# bthome_data->speed_id = 0x44;
# bthome_data->speed_value = (uint16_t)(id(real_vehicle_speed).state * 100);
# bthome_data->battery_charging_id = 0x16;
# bthome_data->battery_charging_value = id(hv_charging).state ? 1 : 0;
# auto ble_instance = static_cast<esphome::esp32_ble::ESP32BLE*>(id(ble));
# std::vector<uint8_t> manufacturer_data((uint8_t*) bthome_data, (uint8_t*) bthome_data + sizeof(manufacturer_data_t));
# ble_instance->get_advertising()->set_manufacturer_data(manufacturer_data);
# ble_instance->get_advertising()->start();
# ESP_LOGD("advertisement", "BTHome advertisement refreshed");
sensor:
- platform: template
id: state_of_health
name: "State of Health"
unit_of_measurement: "%"
accuracy_decimals: 0
state_class: measurement
filters:
- filter_out: nan
- delta: 1.0
- platform: template
id: state_of_charge
name: "State of Charge (Display)"
unit_of_measurement: "%"
accuracy_decimals: 0
device_class: battery
state_class: measurement
filters:
- filter_out: nan
- delta: 1.0
- platform: template
id: state_of_charge_bms
name: "State of Charge (BMS)"
unit_of_measurement: "%"
accuracy_decimals: 0
device_class: battery
state_class: measurement
filters:
- filter_out: nan
- delta: 1.0
- platform: template
id: state_of_charge_bms_calibrated
name: "State of Charge"
unit_of_measurement: "%"
accuracy_decimals: 0
device_class: battery
state_class: measurement
filters:
- filter_out: nan
- delta: 1.0
- calibrate_linear:
method: least_squares
datapoints:
# Map 0.0 (from sensor) to 1.0 (true value)
- 0.0 -> 0.0
- 95 -> 100.0
- clamp:
max_value: 100.0
- platform: template
id: cumulative_energy_charged
name: "Cumulative Energy Charged"
unit_of_measurement: "kWh"
accuracy_decimals: 0
device_class: energy
state_class: total_increasing
filters:
- filter_out: nan
- delta: 1.0
- platform: template
id: cumulative_energy_discharged
name: "Cumulative Energy Discharged"
unit_of_measurement: "kWh"
accuracy_decimals: 0
device_class: energy
state_class: total_increasing
filters:
- filter_out: nan
- delta: 1.0
- platform: template
id: battery_current
name: "Battery Current"
unit_of_measurement: "A"
accuracy_decimals: 2
device_class: current
state_class: measurement
filters:
- filter_out: nan
- delta: 0.2
- platform: template
id: battery_power
name: "Battery Power"
unit_of_measurement: "kW"
accuracy_decimals: 2
device_class: power
state_class: measurement
filters:
- filter_out: nan
- delta: 0.2
- platform: template
id: hv_battery_voltage
name: "HV Battery Voltage"
unit_of_measurement: "V"
accuracy_decimals: 0
device_class: voltage
state_class: measurement
filters:
- filter_out: nan
- delta: 1
- platform: template
id: real_vehicle_speed
name: "Real Vehicle Speed"
unit_of_measurement: "km/h"
accuracy_decimals: 0
device_class: speed
state_class: measurement
filters:
- delta: 10%
- filter_out: nan
- platform: template
id: operating_hours
name: "Operating hours"
unit_of_measurement: "h"
accuracy_decimals: 0
device_class: duration
state_class: total_increasing
update_interval: 5min
filters:
- filter_out: nan
- platform: template
id: odometer
name: "Odometer"
unit_of_measurement: "km"
accuracy_decimals: 0
device_class: distance
state_class: total_increasing
filters:
- delta: 1.0
- filter_out: nan
- platform: template
id: ambient_temperature
name: "Ambient temperature"
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
update_interval: 5min
filters:
- filter_out: nan
- platform: adc
id: starter_battery_voltage
name: "${device_name} Battery Voltage"
pin: 4
attenuation: 11db # https://github.com/meatpiHQ/wican-fw/blob/bf212132f8e506f2c520e917daf86e53a1070302/main/sleep_mode.c#L234
filters:
- lambda: return x * 116 / 16; # https://github.com/meatpiHQ/wican-fw/blob/bf212132f8e506f2c520e917daf86e53a1070302/main/sleep_mode.c#L397
- sliding_window_moving_average:
window_size: 5 # Number of samples to average
send_every: 5 # How often to send the averaged value
send_first_at: 1 # When to send the first averaged value
update_interval: 5s # How often to update the sensor reading
unit_of_measurement: V
accuracy_decimals: 1
device_class: voltage
state_class: measurement
binary_sensor:
- platform: template
id: pending_status_update_on_wake
- platform: template
id: first_boot
- platform: template
id: wake
filters:
- delayed_off: 1min
lambda: |-
if (id(ignition).has_state() || id(starter_battery_voltage).has_state() || !id(sleep_mode).state) {
return id(ignition).state || (id(starter_battery_voltage).state >= ${charging_voltage_threshold} || !id(sleep_mode).state);
} else {
return {};
}
on_state:
then:
if:
condition:
binary_sensor.is_on: wake
then:
- logger.log: "Wake sensor is on, preventing deep sleep"
- deep_sleep.prevent: deep_sleep_1
else:
- deep_sleep.allow: deep_sleep_1
- platform: status
id: statussensor
- platform: homeassistant
entity_id: input_boolean.disable_car_sleep
id: disable_sleep
publish_initial_state: true # This is important!
on_state:
then:
if:
condition:
lambda: return x;
then:
- switch.turn_off: sleep_mode
- platform: template
id: hv_charging
name: "High Voltage Battery Charging"
on_state:
then:
lambda: |-
if (id(last_hv_charging_state) != x) {
// State has changed
ESP_LOGI("sensor_on_state", "Detected state change for hv_charging");
id(last_hv_charging_state) = x;
if (!id(statussensor).state) {
id(pending_status_update) = true;
}
}
- platform: template
id: ac_present
name: "AC charger present"
on_state:
then:
lambda: |-
if (id(last_ac_present_state) != x) {
// State has changed
ESP_LOGI("sensor_on_state", "Detected state change for ac_present_state");
id(last_ac_present_state) = x;
if (!id(statussensor).state) {
id(pending_status_update) = true;
}
}
- platform: template
id: fast_charging
name: "DC charger present"
- platform: template
id: ignition
name: "Ignition"
- platform: template
id: normal_voltage
name: "Starter battery normal voltage"
lambda: |-
return id(starter_battery_voltage).has_state() && id(starter_battery_voltage).state >= ${low_voltage_threshold};
script:
- id: restore_sensor_values
mode: single
then:
- lambda: id(first_boot).publish_state(esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_UNDEFINED);
- lambda: id(odometer).publish_state(id(last_odometer_value));
- lambda: id(ac_present).publish_state(id(last_ac_present_state));
- lambda: id(state_of_charge_bms).publish_state(id(last_state_of_charge));
- lambda: id(state_of_charge_bms_calibrated).publish_state(id(last_state_of_charge));
# Restore pending status update to a sensor so we can determine the boot reason
- lambda: id(pending_status_update_on_wake).publish_state(id(pending_status_update));
- id: queue_status_update
mode: restart
then:
- if:
condition:
binary_sensor.is_off: statussensor
then:
globals.set:
id: pending_status_update
value: 'true'
else:
globals.set:
id: pending_status_update
value: 'false'
- id: add_wifi_hotspots
mode: single
then:
- lambda: |-
auto wific = id(wifi_component);
esphome::wifi::WiFiAP hotspot1;
hotspot1.set_ssid("${hotspot1_ssid}");
hotspot1.set_password("${hotspot1_password}");
hotspot1.set_priority(9.0f);
esphome::wifi::WiFiAP hotspot2;
hotspot2.set_ssid("${hotspot2_ssid}");
hotspot2.set_password("${hotspot2_password}");
hotspot2.set_priority(9.0f);
esphome::wifi::WiFiAP hotspot3;
hotspot2.set_ssid("${hotspot3_ssid}");
hotspot2.set_password("${hotspot3_password}");
hotspot2.set_priority(9.0f);
wific->add_sta(hotspot1);
wific->add_sta(hotspot2);
wific->add_sta(hotspot3);
wific->set_fast_connect(false);
- id: update_car_sensors
mode: single
then:
- logger.log:
format: "Sending canbus status request"
level: DEBUG
- globals.set:
id: frame_received
value: 'false'
- lambda: |-
id(can_battery_current_signed) = 0;
id(can_battery_power) = 0;
id(can_ignition_status) = false;
id(can_speed_mph) = 0;
id(can_battery_dc_voltage) = 0;
- canbus.send:
# 7E4 - 2101, returns on 7EC -
can_id: 0x7E4
data: [0x02, 0x21, 0x01, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA]
- wait_until:
condition:
lambda: return id(frame_received);
timeout: 1sec
- globals.set:
id: frame_received
value: 'false'
- canbus.send:
# 7E4 - 2105, returns on 7EC
can_id: 0x7E4
data: [0x02, 0x21, 0x05, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA]
- wait_until:
condition:
lambda: return id(frame_received);
timeout: 1sec
- if:
condition:
binary_sensor.is_on: ignition
then:
- globals.set:
id: frame_received
value: 'false'
- canbus.send:
# 7E2 - 2101, Real vehicle speed, returns on 7EA
can_id: 0x7E2
data: [0x02, 0x21, 0x01, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA]
- wait_until:
condition:
lambda: return id(frame_received);
timeout: 1sec
- globals.set:
id: frame_received
value: 'false'
- canbus.send:
# Ambient temperature
can_id: 0x7E6
data: [0x02, 0x21, 0x80, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA]
- wait_until:
condition:
lambda: return id(frame_received);
timeout: 1sec
- globals.set:
id: frame_received
value: 'false'
- canbus.send:
# Odometer
can_id: 0x7C6 # Returns on 7CE
data: [0x03, 0x22, 0xB0, 0x02, 0xAA, 0xAA, 0xAA, 0xAA]
- wait_until:
condition:
lambda: return id(frame_received);
timeout: 1sec
- id: send_abrp_telemetry_script
mode: single
then:
if:
condition:
and:
- wifi.connected:
- time.has_time:
then:
- logger.log:
format: "Sending ABRP telemetry"
level: INFO
- http_request.post:
url: "https://api.iternio.com/1/tlm/send"
headers:
Content-Type: application/json
Authorization: !secret abrp_key
json: |-
root["tlm"]["utc"] = id(sntp_time).now().timestamp;
root["tlm"]["soc"] = id(state_of_charge_bms_calibrated).has_state() ? id(state_of_charge_bms_calibrated).state : id(last_state_of_charge);
root["tlm"]["power"] = id(battery_power).state;
root["tlm"]["speed"] = id(real_vehicle_speed).has_state() ? id(real_vehicle_speed).state : 0;
root["tlm"]["is_charging"] = id(hv_charging).state;
root["tlm"]["is_dcfc"] = id(fast_charging).state;
root["tlm"]["kwh_charged"] = id(cumulative_energy_charged).state;
root["tlm"]["odometer"] = id(odometer).state;
root["token"] = "${abrp_token}";
verify_ssl: false
- id: send_home_assistant_webhook
mode: single
then:
if:
condition:
and:
- wifi.connected:
then:
http_request.post:
headers:
Content-Type: application/json
url: !secret webhook_url
verify_ssl: false
json: |-
root["soc"] = id(state_of_charge_bms_calibrated).has_state() ? id(state_of_charge_bms_calibrated).state : id(last_state_of_charge);
root["is_charging"] = id(hv_charging).state;
root["is_dcfc"] = id(fast_charging).state;
root["ac_present"] = id(ac_present).state;
root["power"] = id(battery_power).state;
root["odometer"] = id(odometer).state;
http_request:
useragent: esphome/wican
timeout: 5s
@jnimmo
Copy link
Author

jnimmo commented Sep 30, 2023

Todo:

  • get odometer working
  • consider disabling sleep mode while the vehicle is charging rather than just using 12V battery voltage.
  • finish ABRP telemetry integration
  • test out using a personal hotspot
  • test out broadcasting charge status over Bluetooth LE rather than relying on wifi
  • flash yellow LED on canbus activity
  • add metrics for calculate estimated range etc if available

@jnimmo
Copy link
Author

jnimmo commented Oct 12, 2023

Can't get BTHome BLE advertisements working.. seems to need to go in the service data field instead of manufacturer data.

@djungelola
Copy link

Looks very interesting. Having a Hyundai Kona, which uses other PID's, and all responses are multi-frames. Do you think it is possible to mod your code to support my car? Where shall i focus?

@djungelola
Copy link

Ported it to my Kona (some finetuning left), works fine except for the webhooks to HA. Seems like HA doesnt like webhooks outside the LAN. How did you solve this?

@jnimmo
Copy link
Author

jnimmo commented Apr 9, 2024

Glad to hear you got it working! Is your Home Assistant behind a proxy - i.e Cloudflare? I had to add some config because of that (XFF headers), but not aware of needing to make any other changes.

@djungelola
Copy link

djungelola commented Apr 9, 2024

would you like to share your config?
Edit: got it working

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