Skip to content

Instantly share code, notes, and snippets.

@jnimmo
Forked from mukw-labs/wican.yaml
Last active August 1, 2026 20:44
Show Gist options
  • Select an option

  • Save jnimmo/5182c59011a16c94293935d06c7857a9 to your computer and use it in GitHub Desktop.

Select an option

Save jnimmo/5182c59011a16c94293935d06c7857a9 to your computer and use it in GitHub Desktop.
ESPHome BThome V2 configuration for the Ioniq EV 28kw with the MeatPi WiCAN interface
## Telemetry setup
# This ESPHome config broadcasts car telemetry (SoC, battery power, speed, charging status, odometer, etc.)
# as [BTHome](https://bthome.io/) BLE advertisements — no WiFi/App required, just passive BLE beacons.
# On the receiving end, the [BTHome Monitor](https://apps.apple.com/app/bthome-monitor) iOS app
# listens for these advertisements and uses its webhook support to forward the decoded sensor
# data to [ABRP](https://abrp.com/) (A Better Route Planner), acting as a simple BLE-to-HTTP bridge
# so ABRP gets live vehicle data without any direct integration on the ESPHome side
# last_sleep_reason codes:
# 1 = quick check, no activity detected
# 2 = was awake, activity ended (wake sensor off)
# 5 = low voltage protection
#
substitutions:
device_name: car
charging_voltage_threshold: "14" # 12V voltage at/above this implies DC-DC/charger active
low_voltage_threshold: "12.0"
low_voltage_sleep_duration: 60min
quick_check_interval: 15min # deep sleep between 12V voltage checks
wifi_first_boot_duration: 5min # WiFi/OTA window after a power-plug boot
beacon_every_n_checks: "16" # BLE broadcast of last-known values every Nth quick check
beacon_duration: 30s
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: esp-idf
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
- id: pending_status_update
type: "bool"
restore_value: no
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"
- id: trip_startsoc
type: "float"
restore_value: no
initial_value: "0"
- id: trip_startodo
type: "float"
restore_value: no
initial_value: "0"
- id: last_canbus_packet
type: "std::string"
restore_value: no
initial_value: '""'
- id: is_manual_request
type: "bool"
restore_value: no
initial_value: "false"
- id: last_sleep_reason
type: "int"
restore_value: yes
initial_value: "0"
- id: bthome_packet_id
type: uint8_t
restore_value: no
initial_value: "0"
- id: car_was_on
type: "bool"
restore_value: yes
initial_value: "false"
- id: decision_voltage
type: "float"
restore_value: no
initial_value: "0"
- id: checks_since_beacon
type: "int"
restore_value: yes
initial_value: "0"
- id: last_soc_display
type: uint8_t
restore_value: yes
initial_value: "0"
external_components:
- source:
type: git
url: https://github.com/jnimmo/esphome-bthome
ref: main
components: [bthome]
esphome:
name: car-wican
on_shutdown:
then:
# Transceiver off in sleep, mirroring wican-fw's can_disable() before sleep.
# GPIOs float in deep sleep on the C3, so hold the pin or the transceiver
# enable is indeterminate while sleeping.
- switch.turn_off: can_enabled
- lambda: |-
gpio_hold_en(GPIO_NUM_6);
gpio_deep_sleep_hold_en();
on_boot:
priority: 210
then:
# Release the deep-sleep hold on the CAN transceiver enable pin so the
# can_enabled switch controls it again this wake cycle.
- lambda: |-
gpio_deep_sleep_hold_dis();
gpio_hold_dis(GPIO_NUM_6);
- script.execute: restore_sensor_values
- script.execute: read_decision_voltage
- script.wait: read_decision_voltage
- if:
condition:
binary_sensor.is_on: first_boot
then:
- text_sensor.template.publish:
id: boot_reason
state: "First boot"
- script.execute:
id: full_wake
open_wifi: true
else:
- if:
condition:
lambda: return id(decision_voltage) < ${low_voltage_threshold};
then:
- logger.log: "Low voltage, going back to sleep."
- globals.set:
id: last_sleep_reason
value: "5"
- deep_sleep.enter:
id: deep_sleep_1
sleep_duration: ${low_voltage_sleep_duration}
else:
- if:
condition:
lambda: return id(decision_voltage) >= ${charging_voltage_threshold} || id(car_was_on);
then:
- script.execute:
id: full_wake
open_wifi: false
else:
# Quick check: no activity. No CAN, no WiFi, minimal awake time.
- globals.set:
id: last_sleep_reason
value: "1"
- lambda: id(checks_since_beacon) += 1;
- if:
condition:
lambda: return id(checks_since_beacon) >= ${beacon_every_n_checks};
then:
- globals.set:
id: checks_since_beacon
value: "0"
- script.execute: ble_beacon
- script.wait: ble_beacon
- deep_sleep.enter:
id: deep_sleep_1
sleep_duration: ${quick_check_interval}
logger:
level: DEBUG #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: 50s # failsafe only; paths either prevent sleep or enter it explicitly
sleep_duration: ${quick_check_interval}
# car ap is set to channel 6
api:
reboot_timeout: 0s
encryption:
key:
on_client_connected:
- logger.log:
format: "Client %s connected to API with IP %s"
args: ["client_info.c_str()", "client_address.c_str()"]
services:
- service: send_manual_canbus_request
variables:
can_id: string
data: string
then:
- logger.log:
format: "Sending manual CAN bus request: can_id=0x%s, data=%s"
args: ["can_id.c_str()", "data.c_str()"]
- globals.set:
id: is_manual_request
value: "true"
- globals.set:
id: frame_received
value: "false"
- lambda: |-
std::vector<uint8_t> data_vector;
for (size_t i = 0; i < data.length(); i += 2) {
uint8_t byte = strtol(data.substr(i, 2).c_str(), NULL, 16);
data_vector.push_back(byte);
}
int can_id_int = strtol(can_id.c_str(), NULL, 16);
id(can0)->send_data(can_id_int, false, data_vector);
- wait_until:
condition:
lambda: return id(frame_received);
timeout: 1sec
- globals.set:
id: is_manual_request
value: "false"
text_sensor:
- platform: template
id: boot_reason
name: "Wake reason"
- platform: wifi_info
ssid:
name: Connected SSID
id: connected_ssid
- platform: template
id: last_canbus_packet_sensor
name: "Last CANBus packet"
update_interval: 10s
wifi:
id: wifi_component
use_address: 192.168.178.210
reboot_timeout: 0s
enable_on_boot: false
on_connect:
- light.turn_on:
id: green_led
effect: "None"
on_disconnect:
- light.turn_on:
id: green_led
effect: "searching"
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
priority: 0
manual_ip:
static_ip: 192.168.178.210
gateway: 192.168.178.1
subnet: 255.255.255.0
dns1: 192.168.178.1
- ssid: !secret hotspot1_ssid
password: !secret hotspot1_password
priority: 1
- ssid: !secret hotspot2_ssid
password: !secret hotspot2_password
priority: 2
- ssid: !secret hotspot3_ssid
password: !secret hotspot3_password
priority: 3
- ssid: !secret hotspot4_ssid
password: !secret hotspot4_password
priority: 4
ota:
platform: esphome
password: "be45eb97911e8d734f302a1045defb9f"
output:
- id: blue_led_output
platform: gpio
pin: 7
# GPIO8/9 are strapping pins, but the WiCAN board hardwires its LEDs to them
- id: green_led_output
platform: gpio
pin: { number: 8, inverted: true, ignore_strapping_warning: true }
- id: yellow_led_output
platform: gpio
pin: { number: 9, inverted: true, ignore_strapping_warning: 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_OFF # stays dark on quick checks; full_wake turns it on
- platform: binary
id: green_led
output: green_led_output
effects:
- strobe:
name: "searching"
colors:
- state: True
duration: 100ms
- state: False
duration: 3000ms
- 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 (id(is_manual_request)) {
id(last_canbus_packet) = assembled_data.c_str();
id(last_canbus_packet_sensor).publish_state(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);
if (id(trip_startsoc) == 0) {
id(trip_startodo) = 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;
// Battery temperatures
int battery_max_temp = (id(received_data)[16] < 128) ? id(received_data)[16] : id(received_data)[16] - 256;
int battery_min_temp = (id(received_data)[17] < 128) ? id(received_data)[17] : id(received_data)[17] - 256;
id(battery_temperature).publish_state((battery_min_temp + battery_max_temp)/2);
// 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 (id(state_of_charge_bms).state != 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;
id(pending_status_update_sensor).publish_state(true);
id(last_state_of_charge) = id(state_of_charge_bms).state;
}
}
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] == 0x80 && 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);
if (id(trip_startodo) == 0) {
id(trip_startodo) = odometer_value;
}
}
id(next_frame_seq) = 0x01; // Reset for next multi-frame message
id(frame_received) = true;
}
}
} else {
ESP_LOGI("eup_dump", "Unexpected frame");
}
switch:
- platform: gpio
id: can_enabled
name: "Enable CAN Interface"
pin:
number: 6
inverted: true
restore_mode: ALWAYS_OFF # enabled by full_wake only
- 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"
interval:
# every ~10 seconds while fully awake (never during a quick check)
- interval: 9s
then:
- if:
condition:
switch.is_on: can_enabled
then:
- script.execute: update_car_sensors
- script.wait: update_car_sensors
sensor:
- platform: template
id: state_of_health
name: "State of Health"
unit_of_measurement: "%"
accuracy_decimals: 2
state_class: measurement
filters:
- filter_out: nan
- 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
on_value:
then:
- lambda: if (x > 0) id(last_soc_display) = (uint8_t) x;
- platform: template
id: state_of_charge_bms
name: "State of Charge (BMS)"
unit_of_measurement: "%"
accuracy_decimals: 2
device_class: battery
state_class: measurement
filters:
- filter_out: nan
- delta: 0.01
- clamp:
min_value: -5
max_value: 105
ignore_out_of_range: true
on_value:
then:
- lambda: if (x > 0) id(last_state_of_charge) = (uint8_t) x;
- 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.05
- 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:
- filter_out: nan
- clamp:
min_value: 0
max_value: 140
ignore_out_of_range: true
- 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:
- filter_out: nan
# Capture immediately so the periodic preferences sync persists it while
# driving — waiting for on_shutdown loses the value on power cuts and is
# subject to shutdown-hook ordering.
on_value:
then:
- lambda: if (x > 0) id(last_odometer_value) = x;
- platform: template
id: ambient_temperature
name: "Ambient temperature"
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
update_interval: 1min
filters:
- filter_out: nan
- clamp:
min_value: -10
max_value: 50
ignore_out_of_range: true
- platform: adc
id: starter_battery_voltage
name: "${device_name} Battery Voltage"
pin: 4
attenuation: 12db # (was 11db, renamed upstream) 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
- platform: template
id: battery_temperature
name: "Average battery temperature"
unit_of_measurement: "°C"
device_class: temperature
accuracy_decimals: 1
state_class: measurement
filters:
- filter_out: nan
- platform: template
id: last_state_of_charge_sensor
name: "Last state of charge value"
lambda: |-
return id(last_state_of_charge);
filters:
- delta: 1
- platform: template
id: battery_power_watts
internal: true
unit_of_measurement: "W"
device_class: power
state_class: measurement
- platform: template
id: speed_ms
internal: true
unit_of_measurement: "m/s"
- platform: template
id: bthome_packet_id_sensor
internal: true
binary_sensor:
# - platform: wireguard
# status:
# name: 'WireGuard Status'
- platform: template
id: pending_status_update_on_wake
name: "Pending Status Update on Wakeup"
- platform: template
id: first_boot
name: "First boot"
- platform: template
id: pending_status_update_sensor
name: "Pending Status Update"
lambda: |-
return id(pending_status_update);
- platform: template
id: wake
name: "Wake sensor"
filters:
- delayed_off: 1min
# Ignition alone keeps the board awake (DC-DC doesn't always hold >=13V while
# driving). hv_charging covers active charge sessions; ac_present deliberately
# excluded so a completed charge with the cable still plugged in doesn't hold
# the board awake while the DC-DC is off.
lambda: |-
bool ign = id(ignition).has_state() && id(ignition).state;
bool charging = (id(hv_charging).has_state() && id(hv_charging).state) ||
(id(fast_charging).has_state() && id(fast_charging).state);
bool v_charging = id(starter_battery_voltage).has_state() &&
id(starter_battery_voltage).state >= ${charging_voltage_threshold};
return ign || charging || v_charging || !id(sleep_mode).state;
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:
- if:
condition:
and:
- binary_sensor.is_on: first_boot
- script.is_running: wifi_auto_off
then:
# Don't let the 50s run_duration failsafe cut the first-boot
# WiFi/OTA window short; wifi_auto_off re-checks when it closes.
- logger.log: "Holding sleep until first-boot WiFi window closes"
else:
- globals.set:
id: last_sleep_reason
value: "2"
- lambda: id(car_was_on) = id(ignition).has_state() && id(ignition).state;
- deep_sleep.allow: deep_sleep_1
- platform: status
id: statussensor
- platform: homeassistant
entity_id: input_boolean.disable_car_sleep
id: disable_sleep
trigger_on_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:
- globals.set:
id: last_hv_charging_state
value: !lambda "return x;"
- platform: template
id: ac_present
name: "AC charger present"
on_state:
then:
- globals.set:
id: last_ac_present_state
value: !lambda "return x;"
- platform: template
id: fast_charging
name: "DC charger present"
# Combined "any charger connected" (AC or DC) for the BTHome plug broadcast
- platform: template
id: charger_connected
name: "Charger connected"
lambda: |-
if (!id(ac_present).has_state() && !id(fast_charging).has_state())
return {};
return (id(ac_present).has_state() && id(ac_present).state) ||
(id(fast_charging).has_state() && id(fast_charging).state);
- platform: template
id: ignition
name: "Ignition"
on_state:
then:
- globals.set:
id: car_was_on
value: !lambda "return x;"
- 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(hv_charging).publish_state(id(last_hv_charging_state));
- lambda: id(state_of_charge_bms).publish_state(id(last_state_of_charge));
- lambda: id(state_of_charge).publish_state(id(last_soc_display));
# 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));
# Fast 12V read for the boot decision, mirroring wican-fw sleep_mode.c:
# burst-sample the ADC directly (bypassing the sensor's 5s interval and
# sliding-window filter) so we can decide in ~150ms instead of ~25s.
- id: read_decision_voltage
mode: single
then:
- lambda: |-
float sum = 0;
int valid = 0;
for (int i = 0; i < 16; i++) {
float v = id(starter_battery_voltage).sample();
if (!std::isnan(v)) {
sum += v;
valid++;
}
delay(2);
}
id(decision_voltage) = valid > 0 ? (sum / valid) * 116.0f / 16.0f : 0.0f;
ESP_LOGI("boot", "Decision voltage: %.2fV (%d samples, car_was_on=%d)",
id(decision_voltage), valid, id(car_was_on));
- id: full_wake
mode: single
parameters:
open_wifi: bool
then:
- deep_sleep.prevent: deep_sleep_1
- light.turn_on:
id: blue_led
effect: "None"
- switch.turn_on: can_enabled
- delay: 100ms # transceiver wake-up
# - ble.enable:
- if:
condition:
lambda: return open_wifi;
then:
- wifi.enable:
- light.turn_on:
id: green_led
effect: "searching"
- script.execute: wifi_auto_off
- script.execute: update_car_sensors
- script.wait: update_car_sensors
- lambda: id(car_was_on) = id(ignition).has_state() && id(ignition).state;
# The wake sensor now decides whether we stay awake or go back to sleep.
# BLE-only broadcast of last-known values so Home Assistant's BTHome
# sensors stay fresh (and prove the device alive) during long parks.
- id: ble_beacon
mode: single
then:
- logger.log: "Broadcasting BLE beacon with last-known values"
# - ble.enable:
- lambda: |-
id(bthome_packet_id)++;
id(bthome_packet_id_sensor).publish_state((float)id(bthome_packet_id));
id(state_of_charge).publish_state(id(last_soc_display));
id(state_of_charge_bms).publish_state(id(last_state_of_charge));
id(hv_charging).publish_state(id(last_hv_charging_state));
id(odometer).publish_state(id(last_odometer_value));
id(battery_power_watts).publish_state(0.0f);
id(speed_ms).publish_state(0.0f);
- delay: ${beacon_duration}
# - ble.disable:
- id: update_car_sensors
mode: queued
then:
- logger.log:
format: "Sending canbus status request"
level: INFO
- 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;
- light.turn_on:
id: yellow_led
effect: "None"
- 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
- lambda: |-
// Update sensors
id(ignition).publish_state(id(can_ignition_status));
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));
- 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 returns on 7EE
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
- lambda: |-
id(real_vehicle_speed).publish_state(id(can_speed_mph) * 1.60934);
- light.turn_off: yellow_led
- lambda: |-
id(bthome_packet_id)++;
id(bthome_packet_id_sensor).publish_state((float)id(bthome_packet_id));
id(battery_power_watts).publish_state(
id(battery_power).has_state() ? id(battery_power).state * 1000.0f : 0.0f);
id(speed_ms).publish_state(
id(real_vehicle_speed).has_state() ? id(real_vehicle_speed).state / 3.6f : 0.0f);
- id: wifi_auto_off
mode: single
then:
- delay: ${wifi_first_boot_duration}
- wifi.disable:
- logger.log: "WiFi disabled after first-boot window"
- light.turn_off: green_led
# The wake sensor may have gone off while the window was open (sleep was
# held, see wake on_state) — re-evaluate now that the window has closed.
- if:
condition:
binary_sensor.is_off: wake
then:
- globals.set:
id: last_sleep_reason
value: "2"
- lambda: id(car_was_on) = id(ignition).has_state() && id(ignition).state;
- deep_sleep.allow: deep_sleep_1
bthome:
ble_stack: nimble
min_interval: 2s
max_interval: 5s
tx_power: 6
sensors:
# - type: packet_id
# id: bthome_packet_id_sensor
- type: battery
id: state_of_charge
- type: power_sint32
id: battery_power_watts
- type: speed
id: speed_ms
- type: count_uint32
id: odometer
- type: temperature_sint8
id: battery_temperature
binary_sensors:
- type: plug
id: charger_connected
- type: battery_charging
id: hv_charging
@djungelola

djungelola commented Apr 9, 2024

Copy link
Copy Markdown

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

@spiderdijon

Copy link
Copy Markdown

@djungelola would you be able to share your esphome yaml for the Kona?

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