-
-
Save jnimmo/5182c59011a16c94293935d06c7857a9 to your computer and use it in GitHub Desktop.
ESPHome configuration for the Ioniq EV 28kw with the MeatPi WiCAN interface
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Can't get BTHome BLE advertisements working.. seems to need to go in the service data field instead of manufacturer data.
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?
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?
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.
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
Todo: