Skip to content

Instantly share code, notes, and snippets.

@jnimmo
Forked from mukowman/wican.yaml
Last active June 17, 2024 04:39
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.
ESPHome configuration for the Ioniq EV 28kw with the MeatPi WiCAN interface
substitutions:
device_name: car
charging_voltage_threshold: '13.0'
low_voltage_threshold: '12.0'
low_voltage_sleep_duration: 30min
deep_sleep_duration: 3min # 5 * 60000
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
hotspot4_ssid: !secret hotspot4_ssid
hotspot1_password: !secret hotspot1_password
hotspot2_password: !secret hotspot2_password
hotspot3_password: !secret hotspot3_password
hotspot4_password: !secret hotspot4_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
- 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'
esphome:
name: car-wican
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;'
- if:
condition:
lambda: return id(ac_present).has_state();
then:
globals.set:
id: last_ac_present_state
value: !lambda 'return id(ac_present).state;'
- if:
condition:
lambda: return id(hv_charging).has_state();
then:
globals.set:
id: last_hv_charging_state
value: !lambda 'return id(hv_charging).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
# - wireguard.disable
- 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});
- text_sensor.template.publish:
id: boot_reason
state: "First boot"
- script.execute: update_car_sensors
- script.wait: update_car_sensors
- if: # Pending status update
condition: # If ignition is off, or has no state,
or:
- lambda: return (id(ignition).has_state() && id(ignition).state);
- lambda: return id(pending_status_update);
- lambda: return id(first_boot).state;
then:
- wifi.enable:
- light.turn_on:
id: blue_led
effect: "None"
- light.turn_on:
id: green_led
effect: "searching"
- script.execute: add_wifi_hotspots
# - script.execute: update_bthome_broadcast
else:
- logger.log: "No updates to send, going back to sleep."
- deep_sleep.enter:
id: deep_sleep_1
sleep_duration: ${deep_sleep_duration}
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
sleep_duration: ${deep_sleep_duration}
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()"]
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'
# esp32_ble:
# enable_on_boot: false
# esp32_ble_server:
# id: bleserver
# manufacturer: "ESPHome"
# model: "WiCAN"
# manufacturer_data: [0x4C, 0, 0x23, 77, 0xF0]
time:
- platform: homeassistant
id: ha_time
- platform: sntp
id: sntp_time
servers:
- 162.159.200.1
- 192.168.178.1
- 202.37.101.1
timezone: Pacific/Auckland
on_time_sync:
then:
- script.execute: send_abrp_telemetry_script
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
reboot_timeout: 0s
fast_connect: true
enable_on_boot: false
on_connect:
- light.turn_on:
id: green_led
effect: "None"
- if:
condition:
and:
- switch.is_on: send_abrp_telemetry
then:
- script.execute: send_abrp_telemetry_script
on_disconnect:
- light.turn_on:
id: green_led
effect: "searching"
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
priority: 9
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: 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);
id(state_of_charge_bms_calibrated).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 (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;
id(pending_status_update_sensor).publish_state(true);
id(last_state_of_charge) = soc_bms;
}
}
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_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"
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
- platform: template
id: advertise_bthome
name: "Advertise BLE Home packets"
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
# on_turn_off:
# then:
# if:
# condition:
# ble.enabled
# then:
# ble.disable
interval:
# every 10 seconds while driving
- interval: 9s
then:
- if:
condition:
and:
- wifi.connected:
- binary_sensor.is_on: ignition
- switch.is_on: send_abrp_telemetry
- lambda: 'return !id(is_manual_request);'
then:
- script.execute: update_car_sensors
- script.wait: update_car_sensors
- script.execute: send_abrp_telemetry_script
- interval: 60s
then:
- script.execute: update_car_sensors
- script.wait: update_car_sensors
# - script.execute: update_bthome_broadcast
- if:
condition:
and:
- wifi.connected:
- switch.is_on: send_abrp_telemetry
then:
- script.execute: send_abrp_telemetry_script
- interval: 5min
then:
# - script.execute: update_bthome_broadcast
- if:
condition:
- wifi.connected:
then:
- script.execute: send_home_assistant_webhook
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
- platform: template
id: state_of_charge_bms
name: "State of Charge (BMS)"
unit_of_measurement: "%"
accuracy_decimals: 1
device_class: battery
state_class: measurement
filters:
- filter_out: nan
- delta: 0.5
- 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: 0.5
- calibrate_linear:
method: least_squares
datapoints:
# Map 0.0 (from sensor) to 1.0 (true value)
- 12 -> 11
- 95 -> 99
- 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
- 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
# on_value:
# then:
# lambda: |-
# if (id(trip_start) >= 1) {
# return 1;
# }
- 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
- 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
- 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);
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: 2min
lambda: |-
if (id(ignition).has_state() || id(fast_charging).has_state() || !id(sleep_mode).state) {
return (id(ignition).state || id(fast_charging).state || !id(sleep_mode).state);
} else {
return {};
}
on_state:
then:
if:
condition:
and:
- binary_sensor.is_on: wake
- or:
- lambda: return (id(starter_battery_voltage).state >= ${charging_voltage_threshold});
- switch.is_off: sleep_mode
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"
- platform: template
id: ac_present
name: "AC charger present"
- 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: 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(5.0f);
esphome::wifi::WiFiAP hotspot2;
hotspot2.set_ssid("${hotspot2_ssid}");
hotspot2.set_password("${hotspot2_password}");
hotspot2.set_priority(3.0f);
esphome::wifi::WiFiAP hotspot3;
hotspot3.set_ssid("${hotspot3_ssid}");
hotspot3.set_password("${hotspot3_password}");
hotspot3.set_priority(1.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: 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;
- 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);
- 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().is_valid() ? id(sntp_time).now().timestamp : id(ha_time).now().timestamp;
root["tlm"]["soc"] = id(state_of_charge_bms_calibrated).has_state() ? id(state_of_charge_bms_calibrated).state : 0;
root["tlm"]["power"] = id(battery_power).state;
root["tlm"]["speed"] = id(real_vehicle_speed).state;
root["tlm"]["is_charging"] = id(hv_charging).state;
root["tlm"]["batt_temp"] = id(battery_temperature).state;
root["tlm"]["is_dcfc"] = id(fast_charging).state;
root["tlm"]["kwh_charged"] = id(cumulative_energy_charged).state;
root["tlm"]["odometer"] = id(odometer).has_state() ? id(odometer).state : id(last_odometer_value);
root["tlm"]["ext_temp"] = id(ambient_temperature).state;
root["tlm"]["voltage"] = id(hv_battery_voltage).state;
root["tlm"]["current"] = id(battery_current).state;
root["tlm"]["soh"] = id(state_of_health).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 car_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["speed"] = id(real_vehicle_speed).has_state() ? id(real_vehicle_speed).state : 0;
root["ac_present"] = id(ac_present).state;
root["power"] = id(battery_power).state;
root["odometer"] = id(odometer).has_state() ? id(odometer).state : id(last_odometer_value);
http_request:
useragent: esphome-wican
timeout: 3s
@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