-
-
Save jnimmo/5182c59011a16c94293935d06c7857a9 to your computer and use it in GitHub Desktop.
WiCAN Esphome
This file contains 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.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 |
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: