Introduction
See the newer LTE version based on the LilyGo T-A7670SA here.
This is a simple and highly customizable vehicle GPS tracker, Immobilizer and Alarm using esphome. The device and code is highly customizable to suit your needs and allows for expansion.
This device uses the GSM network and requires credit to send SMS messages.
When installing, keep the GPS antenna positioned towards the sky. Also try to place in an area with minimal obstruction above it.
Below is a list of available features.
| Features |
|---|
| Program up to 3 central phone numbers for management. Notifications are only sent to phone_number_1 |
| Fuel Shutoff Relay – Manually activated fuel shutoff relay. This relay will NOT activate if the ignition state is ON regardless of any automations for safety. (relay_on, relay_off) |
| Query Status for multiple sensors such as GPS Fix, GPS Satellite’s, Ignition Status, Uptime, Relay Status, and AP status and more. (status) |
| Alarm Mode – When armed, turns the fuel shutoff relay on and enables notifications for the Ignition, when the ignition is turned on, a notification will be sent to all phone numbers. Also enables the speed alarm and lock and unlocks the doors. (arm, disarm). |
| Speed Alarm – When armed in alarm mode, enables a Speed alarm, if the vehicle exceeds 10km/h while armed, a notification is sent to phone_number_1. This is useful if you are getting towed. |
| Query Device Location – Provides coordinates and a link to google maps, also provides the Ignition status and GPS Speed. (where) |
| Query, Add and Delete the current central phone numbers with phone_number_x, new_number_x, remove_number_x commands (phone_number_x, new_number_x, remove_number_x) |
| Reboot Notification – Sends a notification to phone_number_1 when the device is rebooted. The device can also be rebooted via SMS. (reboot) |
| Checks and confirms that messages received match that set from phone_number_1, phone_number_2 or phone_number_3, otherwise an “Unauthorized” command is sent back to the sender. |
| Central Locking Control – lock and unlock the doors (lock, unlock). |
| Battery voltage monitor with low battery notifications. An alert is sent to phone_number_1 when the battery voltage is below the set value, The battery voltage can be dynamically set (battery_voltage 12.00) |
| Door Open Alarm – When armed, activates the alarm and sends a notification to phone_number_1when the doors are opened. |
| Ignition On Alarm – When armed, activates the alarm and sends a notification to phone_number_1 when the ignition is turned on. |
| Find your Car! If you lose your car, send the find command to activate the horn. This pulses the horn for 200ms, 3 times per query (find). |
| Enable door open notifications. Sends a notification every time the doors are opened. Handy if you forgot to arm the alarm, or just leaving the car for a short time. (door_alerts_on, door_alerts_off). |
| Enable or Disable feedback notifications for the lock, unlock, arm and disarm commands. This cuts down on sending SMS messages. When sending the lock, unlock, arm or disarm commands, you will not receive a response when notifications are set to off. (notifications_on, notifications_off) |
| Horn activates and pulses for 1 second on alarm activation. This is turned off with the disarm command. |
| Horn feedback for lock and unlock. When the car locks (lock or arm) the horn will sound once, when the car unlocks (unlock or disarm) the horn sounds twice. (horn_feedback_on, horn_feedback_off) |
| Dynamically set the horn_feedback realy bounce time. This is useful for different vehicles as the horns respond differently to different pulse times. (horn_pulsetime 250ms) |
| Auto-Arm function automatically arms the alarm after the ignition is turned off after x minutes. The alarm will not auto arm if the doors are open. (auto_arm_on, auto_arm_off, auto_arm_time 10) |
| Add up to 3 schedules for arming and disarming the alarm. Schedules will not ARM when the ignition is ON for safety. SMS arming and disarming alerts are disabled when armed and disarmed via schedules. (schedule_x, new_schedule_x, delete_schedule_x) |
| Query the SIM800L for the network signal strength. Displayed in dBm and %. (status) |
| Watchdog – This will send a SMS every x days to confirm the device is online (watchdog_on, watchdog_off, watchdog_days x) |
| Tripple Axis Gyro + Accelerometer – Monitors the vehicle for any movement and activates the alarm when roll or pitch angle is exceeded. (calibrate_level, imu_status) |
| Impact Alarm – Alarm activates when impact is detected. Impact sensitivity can be dynamically adjusted via Web UI or SMS. This can detect a broken window. |
| Deep Sleep cuts power consumption by 80% (deep_sleep_enabled, deep_sleep_disabled, sleep_time 22:00, wake_time 07:00) |
| Functions can be manually overridden and viewed from the Web UI over Wi-Fi, |
| Parts List |
|---|
| LillyGo T-Call ESP32 SIM800L |
| RS232 GPS/GNSS 5v |
| 5x HF46F-005-HS1 Relays |
| 2x HF46F-012-HS1 Relays |
| INA219 Battery Monitor |
| Pololu 12V to 5V Regulator 5V |
| 8 Way Terminal Pin Headers 2.54mm |
| TBS M10Q GLONASS Module (or any GLONASS GPS module.) |
| GY521 – MPU6050 Tripple Axis Gyro + Accelerometer |
| Enclosure |
| Nano SIM Card |
Media
ESPhome YAML
Replace the intiail_value for phone_number_1, phone_number_2 and phone_number_3 with your phone numbers in the globals section. Also replace the Access Point password and SSID in the AP section, and also in the # Handle the “ap_on” and “ap_off” commands section. All field requiring changes are highlighted in GREEN
esphome:
name: "car-gps"
friendly_name: "car-gps"
on_boot:
- priority: 0
then:
- lambda: |-
id(sms_message).publish_state("");
ESP_LOGD("on_boot", "SMS message cleared on boot.");
- lambda: |-
ESP_LOGD("on_boot", "Device has rebooted. Waiting for GPS lock...");
- lambda: |-
ESP_LOGD("on_boot", "Phone Number 1: %s", id(phone_number_1).c_str());
ESP_LOGD("on_boot", "Phone Number 2: %s", id(phone_number_2).c_str());
ESP_LOGD("on_boot", "Phone Number 3: %s", id(phone_number_3).c_str());
- lambda: |-
ESP_LOGD("on_boot", "Stored schedules after reboot:");
ESP_LOGD("on_boot", "Schedule 1: %s", id(schedule_1).c_str());
ESP_LOGD("on_boot", "Schedule 2: %s", id(schedule_2).c_str());
ESP_LOGD("on_boot", "Schedule 3: %s", id(schedule_3).c_str());
- lambda: |-
id(watchdog_last_sent) = 0;
ESP_LOGI("watchdog", "Manually reset watchdog_last_sent to 0 for testing.");
- delay: 5s
- uart.write:
id: sim
data: "AT+CMGD=1,4\r\n"
- delay: 2s
- uart.write:
id: sim
data: "AT+CMGF=1\r\n"
- delay: 1s
- lambda: |-
ESP_LOGD("on_boot", "Cleared all stored SMS messages on boot.");
- priority: -100
then:
- switch.turn_off: unlock_switch
- switch.turn_off: lock_switch
globals:
- id: sms_sent
type: bool
initial_value: "false"
- id: alarm_armed
type: bool
initial_value: "false"
- id: speed_alarm_triggered
type: bool
initial_value: "false"
- id: battery_alert_sent
type: bool
restore_value: no
initial_value: "false"
- id: critical_battery_alert_sent
type: bool
restore_value: no
initial_value: "false"
- id: door_alert_triggered
type: bool
restore_value: no
initial_value: "false"
- id: schedule_lock_triggered
type: bool
restore_value: no
initial_value: "false"
- id: schedule_unlock_triggered
type: bool
restore_value: no
initial_value: "false"
- id: phone_number_1
type: std::string
restore_value: yes
initial_value: '"+61234567890"'
- id: phone_number_2
type: std::string
restore_value: yes
initial_value: '"+61234567890"'
- id: phone_number_3
type: std::string
restore_value: yes
initial_value: '"+61234567890"'
- id: schedule_1
type: std::string
restore_value: yes
initial_value: '""'
- id: schedule_2
type: std::string
restore_value: yes
initial_value: '""'
- id: schedule_3
type: std::string
restore_value: yes
initial_value: '""'
- id: override_relay_on
type: bool
restore_value: no
initial_value: "false"
- id: alarm_start_time
type: int
restore_value: no
initial_value: "0"
- id: alarm_triggered
type: bool
initial_value: "false"
- id: horn_feedback_enabled
type: bool
restore_value: yes
initial_value: "false"
- id: door_alerts_enabled
type: bool
restore_value: yes
initial_value: "false"
- id: notifications_enabled
type: bool
restore_value: yes
initial_value: "true"
- id: battery_voltage_threshold
type: float
restore_value: yes
initial_value: '12.00'
- id: battery_voltage_below_threshold_sent
type: bool
restore_value: no
initial_value: "false"
- id: horn_pulse_time_ms
type: int
restore_value: yes
initial_value: "250"
- id: watchdog_enabled
type: bool
restore_value: true
initial_value: "true"
- id: watchdog_interval_days
type: int
restore_value: true
initial_value: "1"
- id: watchdog_last_sent
type: int
restore_value: true
initial_value: "0"
- id: siren_runtime_seconds
type: int
restore_value: yes
initial_value: '30'
- id: ax0
type: float
restore_value: yes
initial_value: "0.0"
- id: ay0
type: float
restore_value: yes
initial_value: "0.0"
- id: az0
type: float
restore_value: yes
initial_value: "0.0"
- id: pitch_offset_deg
type: float
restore_value: yes
initial_value: "0.0"
- id: roll_offset_deg
type: float
restore_value: yes
initial_value: "0.0"
- id: gyro_z_bias
type: float
restore_value: yes
initial_value: "0.0"
- id: yaw_offset_deg
type: float
restore_value: yes
initial_value: "0.0"
- id: level_tol_deg
type: float
restore_value: yes
initial_value: "1.5"
- id: yaw_integrator_deg
type: float
restore_value: no
initial_value: "0.0"
- id: last_yaw_update_ms
type: uint32_t
restore_value: no
initial_value: "0"
- id: auto_arm_enabled
type: bool
restore_value: yes
initial_value: "false"
- id: auto_arm_delay_minutes
type: int
restore_value: yes
initial_value: "10"
- id: ignition_off_timestamp
type: int
restore_value: no
initial_value: "0"
- id: tilt_alarm_threshold_deg
type: float
restore_value: yes
initial_value: '3.0'
- id: tilt_over_start_ms
type: uint32_t
restore_value: no
initial_value: "0"
- id: calibrate_on_arm
type: bool
restore_value: yes
initial_value: "false"
- id: siren_enabled
type: bool
restore_value: yes
initial_value: "true"
interval:
- interval: 10s
then:
- lambda: |-
if (!id(sms_sent) && id(gps_time).now().is_valid() && id(gps_satellites).state > 3) {
auto now = id(gps_time).now();
char buffer[100];
snprintf(
buffer, sizeof(buffer),
"Device rebooted successfully at (GPS time): %04d-%02d-%02d %02d:%02d:%02d UTC",
now.year, now.month, now.day_of_month, now.hour, now.minute, now.second
);
std::vector<std::string> nums = {
id(phone_number_1)
};
for (auto &pn : nums) {
if (!pn.empty()) {
id(gsm).send_sms(pn.c_str(), buffer);
}
}
id(sms_sent) = true;
ESP_LOGD("interval", "Reboot notification sent.");
}
- interval: 1s
then:
- lambda: |-
if (id(alarm_armed) && id(speed).has_state()) {
float car_speed = id(speed).state;
if (car_speed > 10.0 && !id(speed_alarm_triggered)) {
std::vector<std::string> nums = {
id(phone_number_1)
};
for (auto &pn : nums) {
if (!pn.empty()) {
id(gsm).send_sms(pn.c_str(), "Car speed alarm activated. Car is travelling more than 10km/h.");
}
}
id(speed_alarm_triggered) = true;
ESP_LOGD("speed_alarm", "Car speed exceeds 10 km/h. Alarm triggered.");
} else if (car_speed <= 10.0 && id(speed_alarm_triggered)) {
id(speed_alarm_triggered) = false;
ESP_LOGD("speed_alarm", "Car speed is below 10 km/h. No alarm.");
}
}
- interval: 10s
then:
- lambda: |-
static int last_minute = -1;
if (!id(gps_time).now().is_valid()) return;
time_t now = id(gps_time).now().timestamp;
struct tm *current_time = localtime(&now);
int current_minute = current_time->tm_min;
if (current_minute == last_minute) return;
last_minute = current_minute;
char time_buffer[6];
strftime(time_buffer, sizeof(time_buffer), "%H:%M", current_time);
std::string current_time_str(time_buffer);
char day_buffer[4];
strftime(day_buffer, sizeof(day_buffer), "%a", current_time);
std::string current_day(day_buffer);
std::transform(current_day.begin(), current_day.end(), current_day.begin(), ::tolower);
std::vector<std::string> schedules = { id(schedule_1), id(schedule_2), id(schedule_3) };
for (const std::string &schedule : schedules) {
if (schedule.empty()) continue;
size_t first_sep = schedule.find("|");
size_t second_sep = schedule.find("|", first_sep + 1);
if (first_sep == std::string::npos || second_sep == std::string::npos) continue;
std::string days = schedule.substr(0, first_sep);
std::string arm_time = schedule.substr(first_sep + 1, second_sep - (first_sep + 1));
std::string disarm_time = schedule.substr(second_sep + 1);
if (days.find(current_day) == std::string::npos) continue;
// ARM SCHEDULE
if (arm_time == current_time_str && !id(schedule_lock_triggered)) {
bool original_feedback = id(horn_feedback_enabled);
id(horn_feedback_enabled) = false; // silence any honks
ESP_LOGI("schedule", "Scheduled ARM at %s", arm_time.c_str());
id(set_alarm_state).execute(true, true); // armed, silent
id(horn_feedback_enabled) = original_feedback;
id(schedule_lock_triggered) = true;
id(schedule_unlock_triggered) = false;
}
// DISARM SCHEDULE
if (disarm_time == current_time_str && !id(schedule_unlock_triggered)) {
bool original_feedback = id(horn_feedback_enabled);
id(horn_feedback_enabled) = false; // silence any honks
ESP_LOGI("schedule", "Scheduled DISARM at %s", disarm_time.c_str());
id(set_alarm_state).execute(false, true); // disarmed, silent
id(horn_feedback_enabled) = original_feedback;
id(schedule_unlock_triggered) = true;
id(schedule_lock_triggered) = false;
}
}
- interval: 120s
then:
- lambda: |-
if (id(ina219_bus_voltage).has_state()) {
float battery_voltage = id(ina219_bus_voltage).state;
if (battery_voltage < id(battery_voltage_threshold) && !id(battery_voltage_below_threshold_sent)) {
char msg[64];
snprintf(msg, sizeof(msg), "Battery voltage low: %.2fV", battery_voltage);
id(gsm).send_sms(id(phone_number_1).c_str(), msg);
id(battery_voltage_below_threshold_sent) = true;
ESP_LOGD("battery", "Low voltage alert sent: %.2fV", battery_voltage);
} else if (battery_voltage >= id(battery_voltage_threshold)) {
id(battery_voltage_below_threshold_sent) = false;
}
}
- interval: 1s
id: alarm_pulse
then:
- lambda: |-
if (id(alarm_siren_switch).state) {
if (id(gps_time).now().is_valid()) {
int elapsed = id(gps_time).now().timestamp - id(alarm_start_time);
if (elapsed >= id(siren_runtime_seconds)) {
ESP_LOGD("alarm", "Siren timed out after %d seconds.", id(siren_runtime_seconds));
id(alarm_siren_switch).turn_off();
id(alarm_triggered) = false;
return;
}
}
id(horn_output).turn_on();
delay(500);
id(horn_output).turn_off();
}
- interval: 200ms
id: find_pulse
then:
- lambda: |-
static int find_count = 0;
if (id(find_switch).state) {
if (find_count < 3) {
id(horn_output).turn_on();
delay(200);
id(horn_output).turn_off();
find_count++;
} else {
id(find_switch).turn_off();
find_count = 0;
}
} else {
find_count = 0;
}
- interval: 1s
then:
- lambda: |-
if (id(alarm_armed)) {
id(status_led).turn_on();
} else {
id(status_led).turn_off();
}
- interval: 1min
then:
- lambda: |-
if (!id(watchdog_enabled)) {
ESP_LOGD("watchdog", "Watchdog disabled.");
return;
}
if (!id(gps_time).now().is_valid()) {
ESP_LOGW("watchdog", "GPS time not valid yet.");
return;
}
auto now = id(gps_time).now();
int now_day = now.timestamp / 86400;
int now_hour = now.hour;
int now_minute = now.minute;
ESP_LOGD("watchdog", "Now: %04d-%02d-%02d %02d:%02d:%02d (UTC)", now.year, now.month, now.day_of_month, now.hour, now.minute, now.second);
ESP_LOGD("watchdog", "Last sent: %d | Today: %d | Interval: %d", id(watchdog_last_sent), now_day, id(watchdog_interval_days));
if (now_hour == 12 && now_minute == 00 && (now_day - id(watchdog_last_sent)) >= id(watchdog_interval_days)) {
id(watchdog_last_sent) = now_day;
std::string msg = "Watchdog: Device is online and functioning.";
id(gsm).send_sms(id(phone_number_1).c_str(), msg.c_str());
ESP_LOGI("watchdog", "Watchdog SMS sent to phone_number_1.");
} else {
ESP_LOGD("watchdog", "Conditions not met - no SMS sent.");
}
- interval: 1min
then:
- lambda: |-
if (!id(auto_arm_enabled)) return;
if (!id(gps_time).now().is_valid()) return;
if (id(ignition_off_timestamp) <= 0) return;
if (id(ignition).state) return; // ignition currently ON
if (id(doors).state) return; // doors currently OPEN
if (id(alarm_armed)) return; // already armed
int elapsed = id(gps_time).now().timestamp - id(ignition_off_timestamp);
int needed = id(auto_arm_delay_minutes) * 60;
if (elapsed >= needed) {
ESP_LOGI("auto_arm", "Auto-arming after %d minute(s) of ignition OFF.", id(auto_arm_delay_minutes));
bool original_feedback = id(horn_feedback_enabled);
id(horn_feedback_enabled) = false; // suppress honk on auto-arm
id(set_alarm_state).execute(true, true); // armed, silent
id(horn_feedback_enabled) = original_feedback;
id(ignition_off_timestamp) = 0;
}
- interval: 250ms
then:
- lambda: |-
if (!id(alarm_armed)) { id(tilt_over_start_ms) = 0; return; }
if (!id(tilt_magnitude_deg).has_state()) { id(tilt_over_start_ms) = 0; return; }
float effective_thresh = std::max(id(tilt_alarm_threshold_deg), id(level_tol_deg));
float mag = id(tilt_magnitude_deg).state;
uint32_t now_ms = millis();
if (mag >= effective_thresh) {
if (id(tilt_over_start_ms) == 0) {
id(tilt_over_start_ms) = now_ms;
} else if ((now_ms - id(tilt_over_start_ms)) >= 3000 && !id(alarm_triggered)) {
// 3 seconds exceeded -> trigger alarm
id(alarm_triggered) = true;
id(alarm_siren_switch).turn_on();
if (id(notifications_enabled)) {
char msg[80];
snprintf(msg, sizeof(msg), "ALERT: Tilt exceeded %.1f deg for 3s.", effective_thresh);
id(gsm).send_sms(id(phone_number_1).c_str(), msg);
}
ESP_LOGI("alarm", "Tilt alarm fired: mag=%.2f deg, thresh=%.2f deg", mag, effective_thresh);
}
} else {
id(tilt_over_start_ms) = 0;
}
esp32:
board: esp32dev
framework:
type: arduino
logger:
level: INFO
baud_rate: 0
logs:
mpu6050: NONE
sensor: INFO
component: INFO
wifi: INFO
api: WARN
alarm: INFO
sms: INFO
sleep: INFO
imu: INFO
boot: INFO
ota:
- platform: esphome
password: ""
- platform: web_server
wifi:
# networks:
# - ssid: !secret wifi_ssid
# password: !secret wifi_password
# bssid:
# fast_connect: true
ap:
ssid: "car-gps"
password: "password"
id: wifi_ap
i2c:
sda: GPIO25
scl: GPIO14
scan: true
id: bus_a
frequency: 400kHz
web_server:
port: 80
log: true
local: True
version: 3
include_internal: true
text_sensor:
- platform: template
id: sms_sender
name: "Sms Sender"
- platform: template
id: sms_message
name: "Sms Message"
- platform: template
id: alarm_state
name: "Alarm State"
lambda: |-
if (id(alarm_armed)) {
return {"Armed"};
} else {
return {"Disarmed"};
}
update_interval: 10s
- platform: template
id: phone_number_sms
name: "Phone Number"
- platform: template
id: gps_time_text
name: "GPS Time"
update_interval: 10s
lambda: |-
if (!id(gps_time).now().is_valid()) {
return {"GPS Time: N/A"};
}
time_t now = id(gps_time).now().timestamp;
struct tm *timeinfo = localtime(&now);
char time_buffer[20];
strftime(time_buffer, sizeof(time_buffer), "%Y-%m-%d %H:%M:%S", timeinfo);
return {std::string(time_buffer)};
- platform: template
name: "Phone Number 1"
lambda: |-
return {id(phone_number_1)};
- platform: template
name: "Phone Number 2"
lambda: |-
return {id(phone_number_2)};
- platform: template
name: "Phone Number 3"
lambda: |-
return {id(phone_number_3)};
- platform: template
name: "Schedule 1"
lambda: |-
return {id(schedule_1)};
- platform: template
name: "Schedule 2"
lambda: |-
return {id(schedule_2)};
- platform: template
name: "Schedule 3"
lambda: |-
return {id(schedule_3)};
- platform: template
name: "Battery Voltage Threshold"
lambda: |-
char buffer[10];
snprintf(buffer, sizeof(buffer), "%.2f", id(battery_voltage_threshold));
return {std::string(buffer)};
- platform: template
name: "Notifications Enabled"
lambda: |-
return {id(notifications_enabled) ? "ON" : "OFF"};
- platform: template
name: "Door Alerts Enabled"
lambda: |-
return {id(door_alerts_enabled) ? "ON" : "OFF"};
- platform: template
name: "Horn Feedback Enabled"
lambda: |-
return {id(horn_feedback_enabled) ? "ON" : "OFF"};
- platform: template
name: "Watchdog Enabled"
lambda: |-
return {id(watchdog_enabled) ? "ON" : "OFF"};
- platform: template
name: "Watchdog Interval Days"
lambda: |-
return {std::to_string(id(watchdog_interval_days))};
- platform: template
name: "Siren Runtime (sec)"
lambda: |-
return {std::to_string(id(siren_runtime_seconds))};
- platform: template
name: "Horn Pulse Time (ms)"
lambda: |-
return {std::to_string(id(horn_pulse_time_ms))};
- platform: template
name: "Watchdog Last Sent"
lambda: |-
if (id(watchdog_last_sent) <= 0 || !id(gps_time).now().is_valid()) {
return {"N/A"};
}
time_t ts = id(watchdog_last_sent) * 86400;
struct tm *timeinfo = localtime(&ts);
char buffer[20];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M", timeinfo);
return {std::string(buffer)};
update_interval: 60s
- platform: template
name: "Tilt Threshold (deg)"
lambda: |-
char b[16];
snprintf(b, sizeof(b), "%.1f", id(tilt_alarm_threshold_deg));
return {std::string(b)};
sensor:
- platform: uptime
name: "Uptime"
id: uptime_sensor
- platform: ina219
address: 0x41
shunt_resistance: 0.1 ohm
current:
name: "INA219 Current"
id: ina219_current
power:
name: "INA219 Power"
bus_voltage:
name: "INA219 Bus Voltage"
id: ina219_bus_voltage
shunt_voltage:
name: "INA219 Shunt Voltage"
max_voltage: 32.0V
max_current: 3.2A
update_interval: 60s
- platform: template
name: "SIM800L Signal Strength (dBm)"
id: sim800l_rssi
unit_of_measurement: "dBm"
icon: "mdi:signal"
accuracy_decimals: 0
lambda: |-
int rssi_val = id(sim800l_rssi_raw).state;
if (rssi_val == 99) return NAN; // No signal
return (rssi_val * 2) - 113; // Convert to dBm
- platform: template
name: "SIM800L Signal Strength (%)"
id: sim800l_signal_percent
unit_of_measurement: "%"
icon: "mdi:signal-cellular-outline"
accuracy_decimals: 0
lambda: |-
int rssi_val = id(sim800l_rssi_raw).state;
if (rssi_val == 99) return 0; // No signal
int percent = (rssi_val * 100) / 31; // Convert RSSI (0-31) to 0-100%
return std::max(0, std::min(100, percent)); // Ensure it's within 0-100%
- platform: mpu6050
address: 0x68
update_interval: 50ms
accel_x:
id: ax_raw
name: "MPU6050 Accel X (raw)"
accel_y:
id: ay_raw
name: "MPU6050 Accel Y (raw)"
accel_z:
id: az_raw
name: "MPU6050 Accel Z (raw)"
gyro_x:
id: gx_raw_dps
name: "MPU6050 Gyro X (deg/s raw)"
gyro_y:
id: gy_raw_dps
name: "MPU6050 Gyro Y (deg/s raw)"
gyro_z:
id: gz_raw_dps
name: "MPU6050 Gyro Z (deg/s raw)"
temperature:
id: mpu_temp
name: "MPU6050 Temperature"
- platform: template
id: ax
name: "Accel X (cal)"
unit_of_measurement: "m/s^2"
update_interval: 100ms
lambda: |-
return id(ax_raw).state - id(ax0);
filters:
- median:
window_size: 7
send_every: 1
send_first_at: 1
- platform: template
id: ay
name: "Accel Y (cal)"
unit_of_measurement: "m/s^2"
update_interval: 100ms
lambda: |-
return id(ay_raw).state - id(ay0);
filters:
- median:
window_size: 7
send_every: 1
send_first_at: 1
- platform: template
id: az
name: "Accel Z (cal)"
unit_of_measurement: "m/s^2"
update_interval: 100ms
lambda: |-
return id(az_raw).state - id(az0);
filters:
- median:
window_size: 7
send_every: 1
send_first_at: 1
- platform: template
id: gz_dps
name: "Gyro Z (deg/s, cal)"
unit_of_measurement: "deg/s"
update_interval: 100ms
lambda: |-
// gz_raw_dps is deg/s; subtract learned bias
return id(gz_raw_dps).state - id(gyro_z_bias);
filters:
- median:
window_size: 7
send_every: 1
send_first_at: 1
- platform: template
id: pitch_deg
name: "Pitch (deg)"
unit_of_measurement: "deg"
update_interval: 150ms
lambda: |-
if (!id(ax).has_state() || !id(ay).has_state() || !id(az).has_state()) return NAN;
const float RAD2DEG = 57.2957795f;
float axv = id(ax).state;
float ayv = id(ay).state;
float azv = id(az).state;
float pitch_raw = atan2f(-axv, sqrtf(ayv*ayv + azv*azv)) * RAD2DEG;
float out = pitch_raw - id(pitch_offset_deg);
return out;
filters:
- median:
window_size: 7
send_every: 1
send_first_at: 1
- lambda: |-
return (fabsf(x) < id(level_tol_deg)) ? 0.0f : x;
- platform: template
id: roll_deg
name: "Roll (deg)"
unit_of_measurement: "deg"
update_interval: 150ms
lambda: |-
if (!id(ax).has_state() || !id(ay).has_state() || !id(az).has_state()) return NAN;
const float RAD2DEG = 57.2957795f;
float ayv = id(ay).state;
float azv = id(az).state;
float roll_raw = atan2f(ayv, azv) * RAD2DEG;
float out = roll_raw - id(roll_offset_deg);
return out;
filters:
- median:
window_size: 7
send_every: 1
send_first_at: 1
- lambda: |-
return (fabsf(x) < id(level_tol_deg)) ? 0.0f : x;
- platform: template
id: yaw_deg
name: "Yaw (deg)"
unit_of_measurement: "deg"
update_interval: 100ms
lambda: |-
// Integrate gz_dps over time, store in globals
uint32_t now_ms = millis();
if (id(last_yaw_update_ms) == 0) {
id(last_yaw_update_ms) = now_ms;
return id(yaw_integrator_deg) - id(yaw_offset_deg);
}
float dt = (now_ms - id(last_yaw_update_ms)) / 1000.0f;
id(last_yaw_update_ms) = now_ms;
float rate = id(gz_dps).state;
id(yaw_integrator_deg) += rate * dt;
if (id(yaw_integrator_deg) > 180.0f) id(yaw_integrator_deg) -= 360.0f;
if (id(yaw_integrator_deg) < -180.0f) id(yaw_integrator_deg) += 360.0f;
return id(yaw_integrator_deg) - id(yaw_offset_deg);
filters:
- median:
window_size: 5
send_every: 1
send_first_at: 1
- platform: template
id: tilt_magnitude_deg
name: "Tilt Magnitude (deg)"
unit_of_measurement: "deg"
update_interval: 150ms
lambda: |-
float p = fabsf(id(pitch_deg).state);
float r = fabsf(id(roll_deg).state);
return sqrtf(p*p + r*r);
filters:
- median:
window_size: 5
send_every: 1
send_first_at: 1
- platform: sim800l
rssi:
id: sim800l_rssi_raw
uart:
- id: sim
baud_rate: 9600
tx_pin: 27
rx_pin: 26
- id: gpsmodule
baud_rate: 38400
rx_pin: 34
sim800l:
id: gsm
uart_id: sim
on_sms_received:
- lambda: |-
id(sms_sender).publish_state(sender);
id(sms_message).publish_state(message);
std::string sender_str(sender);
std::string message_str(message);
ESP_LOGD("sms", "Received SMS from '%s': '%s'", sender_str.c_str(), message_str.c_str());
if (
sender_str != id(phone_number_1) &&
sender_str != id(phone_number_2) &&
sender_str != id(phone_number_3)
) {
id(gsm).send_sms(sender_str.c_str(), "Unauthorized");
return;
}
auto to_lower = [](std::string s){
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
return s;
};
std::string msg_lower = to_lower(message_str);
// Query stored numbers
if (msg_lower == "phone_number_1") { id(gsm).send_sms(sender_str.c_str(), ("Phone Number 1: " + id(phone_number_1)).c_str()); return; }
if (msg_lower == "phone_number_2") { id(gsm).send_sms(sender_str.c_str(), ("Phone Number 2: " + id(phone_number_2)).c_str()); return; }
if (msg_lower == "phone_number_3") { id(gsm).send_sms(sender_str.c_str(), ("Phone Number 3: " + id(phone_number_3)).c_str()); return; }
// Update numbers (persistent)
if (msg_lower.rfind("new_number_1 ", 0) == 0) { std::string n = message_str.substr(13); id(phone_number_1)=n; id(gsm).send_sms(sender_str.c_str(), ("Phone Number 1 updated to: " + n).c_str()); return; }
if (msg_lower.rfind("new_number_2 ", 0) == 0) { std::string n = message_str.substr(13); id(phone_number_2)=n; id(gsm).send_sms(sender_str.c_str(), ("Phone Number 2 updated to: " + n).c_str()); return; }
if (msg_lower.rfind("new_number_3 ", 0) == 0) { std::string n = message_str.substr(13); id(phone_number_3)=n; id(gsm).send_sms(sender_str.c_str(), ("Phone Number 3 updated to: " + n).c_str()); return; }
// Remove numbers
if (msg_lower == "remove_number_1") { id(phone_number_1).clear(); id(gsm).send_sms(sender_str.c_str(), "Phone Number 1 removed."); return; }
if (msg_lower == "remove_number_2") { id(phone_number_2).clear(); id(gsm).send_sms(sender_str.c_str(), "Phone Number 2 removed."); return; }
if (msg_lower == "remove_number_3") { id(phone_number_3).clear(); id(gsm).send_sms(sender_str.c_str(), "Phone Number 3 removed."); return; }
// Relay on/off
if (msg_lower == "relay_on") { id(override_relay_on)=true; id(shutoff).turn_on(); delay(100); id(gsm).send_sms(sender_str.c_str(), id(shutoff).state ? "Fuel Shutoff Relay turned on." : "Fuel Shutoff Relay failed to turn on."); return; }
if (msg_lower == "relay_off") { id(override_relay_on)=false; id(shutoff).turn_off(); delay(100); id(gsm).send_sms(sender_str.c_str(), !id(shutoff).state ? "Fuel Shutoff Relay turned off." : "Fuel Shutoff Relay failed to turn off."); return; }
// Where
if (msg_lower == "where") {
std::string response;
if (id(latitude).has_state() && id(longitude).has_state()) {
float lat = id(latitude).state, lon = id(longitude).state;
response += "Device location:\n";
response += "Lat: " + std::to_string(lat) + "\n";
response += "Lon: " + std::to_string(lon) + "\n";
response += "Google Maps: https://www.google.com/maps?q=" + std::to_string(lat) + "," + std::to_string(lon) + "\n";
} else {
response += "GPS coordinates are not available yet.\n";
}
response += std::string("Ignition: ") + (id(ignition).state ? "ON\n" : "OFF\n");
response += std::string("Doors: ") + (id(doors).state ? "OPEN\n" : "CLOSED\n");
if (id(speed).has_state()) response += "Speed: " + std::to_string(id(speed).state) + " km/h\n"; else response += "Speed: N/A\n";
id(gsm).send_sms(sender_str.c_str(), response.c_str());
return;
}
// ARM/DISARM (unified)
if (msg_lower == "arm") { id(set_alarm_state).execute(true, false); if (id(notifications_enabled)) id(gsm).send_sms(sender_str.c_str(), "Alarm armed. Notifications enabled, fuel shutoff is ON and doors are locked."); return; }
if (msg_lower == "disarm") { id(set_alarm_state).execute(false, false); if (id(notifications_enabled)) id(gsm).send_sms(sender_str.c_str(), "Alarm disarmed. Notifications disabled, fuel shutoff is OFF and doors are unlocked."); return; }
// Status
if (msg_lower == "status") {
std::string response;
if (id(gps_time).now().is_valid()) {
response += "GPS Fix: True\n";
time_t now = id(gps_time).now().timestamp; struct tm *ti = localtime(&now);
char tb[20]; strftime(tb, sizeof(tb), "%Y-%m-%d %H:%M:%S", ti);
response += std::string("GPS Time: ") + tb + "\n";
} else { response += "GPS Fix: False\nGPS Time: N/A\n"; }
if (id(gps_satellites).has_state()) response += "Satellites: " + std::to_string((int)id(gps_satellites).state) + "\n"; else response += "Satellites: N/A\n";
if (id(uptime_sensor).has_state()) {
int ts = (int)id(uptime_sensor).state; int d=ts/86400, h=(ts%86400)/3600, m=(ts%3600)/60;
response += "Uptime: " + std::to_string(d) + "d " + std::to_string(h) + "h " + std::to_string(m) + "m\n";
} else response += "Uptime: N/A\n";
response += std::string("Ignition: ") + (id(ignition).state ? "ON\n":"OFF\n");
response += std::string("Doors: ") + (id(doors).state ? "OPEN\n":"CLOSED\n");
response += std::string("Siren: ") + (id(siren_enabled) ? "ON\n" : "OFF\n");
response += std::string("Relay: ") + (id(shutoff).state ? "ON\n":"OFF\n");
response += std::string("Alarm State: ") + (id(alarm_armed) ? "Armed\n":"Disarmed\n");
response += std::string("Horn Feedback: ") + (id(horn_feedback_enabled) ? "ON\n":"OFF\n");
response += std::string("Door Alerts: ") + (id(door_alerts_enabled) ? "ON\n":"OFF\n");
response += std::string("Notifications: ") + (id(notifications_enabled) ? "ON\n":"OFF\n");
if (id(sim800l_rssi).has_state()) response += "SIM800L RSSI: " + std::to_string((int)id(sim800l_rssi).state) + " dBm\n"; else response += "SIM800L RSSI: N/A\n";
if (id(sim800l_signal_percent).has_state()) response += "SIM800L Signal: " + std::to_string((int)id(sim800l_signal_percent).state) + "%\n"; else response += "SIM800L Signal: N/A\n";
response += "Horn Pulsetime: " + std::to_string(id(horn_pulse_time_ms)) + " ms\n";
response += "Siren Runtime: " + std::to_string(id(siren_runtime_seconds)) + " sec\n";
response += std::string("Auto-arm: ") + (id(auto_arm_enabled) ? "ON\n" : "OFF\n");
response += "Auto-arm Delay: " + std::to_string(id(auto_arm_delay_minutes)) + " min\n";
char th[10]; snprintf(th, sizeof(th), "%.2fV", id(battery_voltage_threshold)); response += "Voltage Alarm: " + std::string(th) + "\n";
if (id(ina219_bus_voltage).has_state() && id(ina219_current).has_state()) {
char vs[10], cs[10]; snprintf(vs, sizeof(vs), "%.2fV", id(ina219_bus_voltage).state); snprintf(cs, sizeof(cs), "%.2fA", id(ina219_current).state);
response += "Battery Voltage: " + std::string(vs) + "\n";
response += "Current Draw: " + std::string(cs) + "\n";
} else response += "Battery: N/A\n";
response += std::string("Watchdog: ") + (id(watchdog_enabled) ? "ON\n":"OFF\n");
response += "Watchdog Interval: " + std::to_string(id(watchdog_interval_days)) + " day(s)\n";
id(gsm).send_sms(sender_str.c_str(), response.c_str());
return;
}
// Add schedule (flexible)
if (msg_lower.rfind("new_schedule_", 0) == 0) {
auto trim = [](std::string s)->std::string { auto issp=[](unsigned char c){return std::isspace(c);}; while(!s.empty()&&issp(s.front())) s.erase(s.begin()); while(!s.empty()&&issp(s.back())) s.pop_back(); return s; };
auto split_ws = [](const std::string& s){ std::vector<std::string> out; std::string cur; for(char c: s){ if (std::isspace((unsigned char)c)){ if(!cur.empty()){ out.push_back(cur); cur.clear(); } } else { cur.push_back(c);} } if(!cur.empty()) out.push_back(cur); return out; };
auto to_hhmm = [](const std::string& t)->std::string { int h=-1,m=-1,s=0; if (sscanf(t.c_str(), "%d:%d:%d", &h, &m, &s) < 2) return ""; if (h<0||h>23||m<0||m>59) return ""; char buf[6]; snprintf(buf,sizeof(buf),"%02d:%02d",h,m); return std::string(buf); };
auto add_days = [](std::array<bool,7>& d, std::string tok){ if (!tok.empty()&&(tok.back()==','||tok.back()==';')) tok.pop_back(); if (tok=="mon"||tok=="monday") d[0]=true; else if (tok=="tue"||tok=="tues"||tok=="tuesday") d[1]=true; else if (tok=="wed"||tok=="weds"||tok=="wednesday") d[2]=true; else if (tok=="thu"||tok=="thur"||tok=="thurs"||tok=="thursday") d[3]=true; else if (tok=="fri"||tok=="friday") d[4]=true; else if (tok=="sat"||tok=="saturday") d[5]=true; else if (tok=="sun"||tok=="sunday") d[6]=true; };
size_t pref = std::string("new_schedule_").size(); size_t i = pref; while (i < msg_lower.size() && std::isdigit((unsigned char)msg_lower[i])) i++;
if (i==pref) { id(gsm).send_sms(sender_str.c_str(), "Invalid schedule index."); return; }
int idx = atoi(msg_lower.substr(pref, i-pref).c_str()); if (idx<1||idx>3) { id(gsm).send_sms(sender_str.c_str(), "Schedule index must be 1-3."); return; }
std::string rest = trim(msg_lower.substr(i));
for (char& c : rest) { if (c==',' || c=='\n' || c=='\t' || c==';') c=' '; }
std::array<bool,7> days{}; bool saw=false, wkd=false, wke=false, all=false;
std::string arm_hhmm, disarm_hhmm;
std::vector<std::string> toks = split_ws(rest);
for (size_t k=0;k<toks.size();++k) {
std::string t = toks[k];
if (t=="arm" && (k+1)<toks.size()) { std::string cand = to_hhmm(toks[k+1]); if (cand.empty()){ id(gsm).send_sms(sender_str.c_str(), "Invalid arm time. Use HH:MM."); return; } arm_hhmm=cand; k++; continue; }
if (t=="disarm" && (k+1)<toks.size()) { std::string cand = to_hhmm(toks[k+1]); if (cand.empty()){ id(gsm).send_sms(sender_str.c_str(), "Invalid disarm time. Use HH:MM."); return; } disarm_hhmm=cand; k++; continue; }
if (t=="everyday"||t=="daily"||t=="all") { all=true; continue; }
if (t=="weekdays"||t=="weekday") { wkd=true; continue; }
if (t=="weekends"||t=="weekend") { wke=true; continue; }
size_t before = std::count(days.begin(), days.end(), true);
add_days(days, t);
size_t after = std::count(days.begin(), days.end(), true);
if (after>before) saw=true;
}
if (all) { for(int d=0; d<7; ++d) days[d]=true; saw=true; }
if (wkd) { for(int d=0; d<=4; ++d) days[d]=true; saw=true; }
if (wke) { days[5]=true; days[6]=true; saw=true; }
if (arm_hhmm.empty() || disarm_hhmm.empty()) { id(gsm).send_sms(sender_str.c_str(), "Missing arm/disarm time. Example: arm 21:00 disarm 07:00"); return; }
if (!saw) { id(gsm).send_sms(sender_str.c_str(), "No days specified. Use mon..sun or weekdays/weekends/everyday."); return; }
const char* shortn[7] = {"mon","tue","wed","thu","fri","sat","sun"};
std::string days_str; for (int d=0; d<7; ++d) if (days[d]) { if (!days_str.empty()) days_str += ","; days_str += shortn[d]; }
std::string schedule_data = days_str + "|" + arm_hhmm + "|" + disarm_hhmm;
if (idx==1) id(schedule_1)=schedule_data; else if (idx==2) id(schedule_2)=schedule_data; else id(schedule_3)=schedule_data;
std::string conf = "Schedule " + std::to_string(idx) + " set: " + days_str + " arm " + arm_hhmm + " disarm " + disarm_hhmm;
id(gsm).send_sms(sender_str.c_str(), conf.c_str());
return;
}
// Remove schedule
if (msg_lower.find("remove_schedule_") == 0) {
std::string id_part = msg_lower.substr(16);
bool deleted=false;
if (id_part=="1" && !id(schedule_1).empty()) { id(schedule_1)=""; deleted=true; }
else if (id_part=="2" && !id(schedule_2).empty()) { id(schedule_2)=""; deleted=true; }
else if (id_part=="3" && !id(schedule_3).empty()) { id(schedule_3)=""; deleted=true; }
id(gsm).send_sms(sender_str.c_str(), deleted ? ("Schedule " + id_part + " removed successfully.").c_str() : ("Schedule " + id_part + " not found.").c_str());
return;
}
// Query schedules
if (msg_lower == "schedule_1" || msg_lower == "schedule_2" || msg_lower == "schedule_3") {
std::string val = (msg_lower=="schedule_1")?id(schedule_1):(msg_lower=="schedule_2")?id(schedule_2):id(schedule_3);
if (val.empty()) { id(gsm).send_sms(sender_str.c_str(), msg_lower + " is not set."); }
else {
size_t a = val.find("|"), b = val.find("|", a+1);
if (a!=std::string::npos && b!=std::string::npos) {
std::string days = val.substr(0,a), at = val.substr(a+1, b-(a+1)), dt = val.substr(b+1);
id(gsm).send_sms(sender_str.c_str(), (msg_lower + ":\nDays: " + days + "\nArm: " + at + "\nDisarm: " + dt).c_str());
} else id(gsm).send_sms(sender_str.c_str(), (msg_lower + " contains invalid data.").c_str());
}
return;
}
// Lock/Unlock (unified)
if (msg_lower == "lock") { id(do_lock).execute(false); if (id(notifications_enabled)) id(gsm).send_sms(sender_str.c_str(), "Doors Locked."); return; }
if (msg_lower == "unlock") { id(do_unlock).execute(false); if (id(notifications_enabled)) id(gsm).send_sms(sender_str.c_str(), "Doors Unlocked."); return; }
// Time
if (msg_lower == "time") {
std::string response;
if (id(gps_time).now().is_valid()) {
time_t now = id(gps_time).now().timestamp; struct tm *ti = localtime(&now);
char tb[20]; strftime(tb, sizeof(tb), "%Y-%m-%d %H:%M:%S", ti);
response = std::string("GPS Time: ") + tb;
} else response = "GPS Time: N/A";
id(gsm).send_sms(sender_str.c_str(), response.c_str());
return;
}
// Reboot
if (msg_lower == "reboot") { id(gsm).send_sms(sender_str.c_str(), "Device is rebooting..."); id(reboot_trigger).press(); return; }
// Horn feedback on/off
if (msg_lower == "horn_feedback_on") { id(horn_feedback_enabled)=true; id(gsm).send_sms(sender_str.c_str(), "Horn feedback enabled."); return; }
if (msg_lower == "horn_feedback_off") { id(horn_feedback_enabled)=false; id(gsm).send_sms(sender_str.c_str(), "Horn feedback disabled."); return; }
// Find
if (msg_lower == "find") { id(find_switch).turn_on(); id(gsm).send_sms(sender_str.c_str(), "Find command activated. Horn will sound 3 times."); return; }
// Battery voltage threshold
if (msg_lower.rfind("battery_voltage ", 0) == 0) {
float val = strtof(message_str.substr(16).c_str(), nullptr);
if (val > 0.0f && val < 20.0f) {
id(battery_voltage_threshold) = val; id(battery_voltage_below_threshold_sent)=false;
char buf[64]; snprintf(buf, sizeof(buf), "Battery alert voltage set to %.2fV", val);
id(gsm).send_sms(sender_str.c_str(), buf);
} else id(gsm).send_sms(sender_str.c_str(), "Invalid battery voltage threshold.");
return;
}
// Notifications on/off
if (msg_lower == "notifications_on") { id(notifications_enabled)=true; id(gsm).send_sms(sender_str.c_str(), "Notifications have been enabled."); return; }
if (msg_lower == "notifications_off") { id(notifications_enabled)=false; id(gsm).send_sms(sender_str.c_str(), "Notifications have been disabled."); return; }
// Door alerts on/off
if (msg_lower == "door_alerts_on") { id(door_alerts_enabled)=true; id(gsm).send_sms(sender_str.c_str(), "Door alerts enabled."); return; }
if (msg_lower == "door_alerts_off") { id(door_alerts_enabled)=false; id(gsm).send_sms(sender_str.c_str(), "Door alerts disabled."); return; }
// Battery snapshot
if (msg_lower == "battery") {
std::string response;
if (id(ina219_bus_voltage).has_state() && id(ina219_current).has_state()) {
char vs[10], cs[10]; snprintf(vs, sizeof(vs), "%.2fV", id(ina219_bus_voltage).state); snprintf(cs, sizeof(cs), "%.2fA", id(ina219_current).state);
response += "Battery Voltage: " + std::string(vs) + "\n";
response += "Current Draw: " + std::string(cs);
} else response += "Battery data not available.";
id(gsm).send_sms(sender_str.c_str(), response.c_str());
return;
}
// Watchdog on/off
if (msg_lower == "watchdog_on") { id(watchdog_enabled)=true; id(gsm).send_sms(sender_str.c_str(), "Watchdog enabled."); return; }
if (msg_lower == "watchdog_off") { id(watchdog_enabled)=false; id(gsm).send_sms(sender_str.c_str(), "Watchdog disabled."); return; }
// Watchdog days
if (msg_lower.rfind("watchdog_days ", 0) == 0) {
int days = atoi(msg_lower.substr(14).c_str());
if (days > 0) { id(watchdog_interval_days)=days; id(gsm).send_sms(sender_str.c_str(), ("Watchdog interval set to " + std::to_string(days) + " day(s).").c_str()); id(watchdog_last_sent)=id(gps_time).now().timestamp; }
else { id(gsm).send_sms(sender_str.c_str(), "Invalid watchdog_days value."); }
return;
}
// Horn pulse time
if (msg_lower.rfind("horn_pulsetime ", 0) == 0) {
int pulse = atoi(message_str.substr(15).c_str());
if (pulse >= 50 && pulse <= 2000) {
id(horn_pulse_time_ms)=pulse; char buf[64]; snprintf(buf, sizeof(buf), "Horn pulse time set to %d ms", pulse);
id(gsm).send_sms(sender_str.c_str(), buf);
} else id(gsm).send_sms(sender_str.c_str(), "Invalid pulse time. Range: 50-2000 ms.");
return;
}
// Siren runtime
if (msg_lower.rfind("siren_runtime ", 0) == 0) {
int runtime = atoi(message_str.substr(14).c_str());
if (runtime > 0 && runtime <= 300) { id(siren_runtime_seconds)=runtime; std::string c="Siren runtime updated to "+std::to_string(runtime)+" seconds."; id(gsm).send_sms(sender_str.c_str(), c.c_str()); }
else id(gsm).send_sms(sender_str.c_str(), "Invalid siren runtime. Must be between 1 and 300 seconds.");
return;
}
// IMU calibration
if (msg_lower == "calibrate_level") {
const int N = 200; float sax=0, say=0, saz=0, sgz=0; int n_acc=0, n_gz=0;
for (int i = 0; i < N; i++) {
if (id(ax).has_state() && id(ay).has_state() && id(az).has_state()) { sax+=id(ax).state; say+=id(ay).state; saz+=id(az).state; n_acc++; }
if (id(gz_dps).has_state()) { sgz+=id(gz_dps).state; n_gz++; }
delay(10);
}
if (n_acc == 0 || n_gz == 0) { id(gsm).send_sms(sender_str.c_str(), "IMU cal failed: no samples."); return; }
const float rad2deg = 57.2957795f;
float axm=sax/n_acc, aym=say/n_acc, azm=saz/n_acc, gzm=sgz/n_gz;
float pitch_raw = atan2f(-axm, sqrtf(aym*aym + azm*azm)) * rad2deg;
float roll_raw = atan2f(aym, azm) * rad2deg;
float pitch_now = pitch_raw - id(pitch_offset_deg);
float roll_now = roll_raw - id(roll_offset_deg);
bool already_level = (fabsf(pitch_now) <= id(level_tol_deg)) && (fabsf(roll_now) <= id(level_tol_deg));
id(pitch_offset_deg)=pitch_raw; id(roll_offset_deg)=roll_raw; id(gyro_z_bias)=gzm;
if (id(yaw_deg).has_state()) id(yaw_offset_deg)=id(yaw_deg).state + id(yaw_offset_deg); else id(yaw_offset_deg)=0.0f;
char msg[128]; snprintf(msg, sizeof(msg), "IMU calibrated. PitchOff=%.2f deg, RollOff=%.2f deg, GzBias=%.3f deg/s. Level: %s",
id(pitch_offset_deg), id(roll_offset_deg), id(gyro_z_bias), (already_level?"YES":"NO"));
id(gsm).send_sms(sender_str.c_str(), msg);
return;
}
// ------------------------------
// "imu_status" - report pitch and roll (deg)
// ------------------------------
if (msg_lower == "imu_status") {
if (id(pitch_deg).has_state() && id(roll_deg).has_state()) {
char pbuf[16], rbuf[16];
snprintf(pbuf, sizeof(pbuf), "%.2f", id(pitch_deg).state);
snprintf(rbuf, sizeof(rbuf), "%.2f", id(roll_deg).state);
std::string resp = "Pitch: " + std::string(pbuf) + " deg\n"
"Roll: " + std::string(rbuf) + " deg";
id(gsm).send_sms(sender_str.c_str(), resp.c_str());
} else {
id(gsm).send_sms(sender_str.c_str(), "Pitch/Roll not available yet.");
}
return;
}
// Auto-arm on/off
if (msg_lower == "auto_arm_on") { id(auto_arm_enabled)=true; id(gsm).send_sms(sender_str.c_str(), "Auto-arm enabled."); return; }
if (msg_lower == "auto_arm_off") { id(auto_arm_enabled)=false; id(gsm).send_sms(sender_str.c_str(), "Auto-arm disabled."); return; }
// Auto-arm time
if (msg_lower.rfind("auto_arm_time ", 0) == 0) {
int mins = atoi(msg_lower.substr(14).c_str());
if (mins >= 1 && mins <= 1440) { id(auto_arm_delay_minutes)=mins; char b[64]; snprintf(b,sizeof(b),"Auto-arm time set to %d minute(s).", mins); id(gsm).send_sms(sender_str.c_str(), b); }
else { id(gsm).send_sms(sender_str.c_str(), "Invalid auto_arm_time. Range: 1-1440 minutes."); }
return;
}
// ------------------------------
// Siren enable/disable (persisted)
// ------------------------------
if (msg_lower == "siren_on") {
id(siren_enabled) = true;
id(gsm).send_sms(sender_str.c_str(), "Siren enabled. Alerts + horn will sound on alarm.");
ESP_LOGI("sms", "Siren enabled via SMS.");
return;
}
if (msg_lower == "siren_off") {
id(siren_enabled) = false;
id(gsm).send_sms(sender_str.c_str(), "Siren disabled. Alerts will send, horn will stay silent.");
ESP_LOGI("sms", "Siren disabled via SMS.");
return;
}
// Fallback
id(gsm).send_sms(sender_str.c_str(), "Unrecognized Command");
binary_sensor:
- platform: gpio
id: doors
pin:
number: 13
mode: INPUT_PULLUP
inverted: True
name: "Doors"
filters:
- delayed_on: 500ms
- delayed_off: 500ms
device_class: door
on_press:
then:
- lambda: |-
ESP_LOGD("binary_sensor", "Doors OPENED");
id(doors).publish_state(true);
- lambda: |-
if (id(alarm_armed) && !id(alarm_triggered)) {
id(alarm_triggered) = true;
id(alarm_siren_switch).turn_on();
id(gsm).send_sms(id(phone_number_1).c_str(), "ALERT: Door opened while alarm is armed!");
ESP_LOGD("alarm", "Alarm triggered by DOOR sensor.");
}
- lambda: |-
if (id(door_alerts_enabled)) {
id(gsm).send_sms(id(phone_number_1).c_str(), "NOTICE: Door opened.");
ESP_LOGD("door_alert", "Door opened alert sent.");
}
on_release:
then:
- lambda: |-
ESP_LOGD("binary_sensor", "Doors CLOSED");
id(doors).publish_state(false);
- platform: gpio
id: ignition
pin:
number: 21
mode: INPUT_PULLUP
inverted: True
name: "Ignition"
filters:
- delayed_on: 100ms
- delayed_off: 100ms
device_class: power
on_press:
then:
- lambda: |-
ESP_LOGD("binary_sensor", "Ignition TURNED ON");
id(ignition).publish_state(true);
id(ignition_off_timestamp) = 0;
- lambda: |-
if (id(alarm_armed) && !id(alarm_triggered)) {
id(alarm_triggered) = true;
id(alarm_siren_switch).turn_on();
id(gsm).send_sms(id(phone_number_1).c_str(), "ALERT: Ignition turned ON while alarm is armed!");
ESP_LOGD("alarm", "Alarm triggered by IGNITION sensor.");
}
on_release:
then:
- lambda: |-
ESP_LOGD("binary_sensor", "Ignition TURNED OFF");
id(ignition).publish_state(false);
if (id(gps_time).now().is_valid()) {
id(ignition_off_timestamp) = id(gps_time).now().timestamp;
ESP_LOGD("auto_arm", "Ignition off timestamp recorded: %d", id(ignition_off_timestamp));
}
- logger.log: "Ignition OFF -> running IMU calibration"
- lambda: |-
id(calibrate_imu_all).execute();
switch:
- platform: gpio
name: "SIM800_PWKEY"
pin: 4
restore_mode: ALWAYS_OFF
- platform: gpio
name: "SIM800_RST"
pin: 5
restore_mode: ALWAYS_ON
- platform: gpio
name: "SIM800_POWER"
pin: 23
restore_mode: ALWAYS_ON
- platform: gpio
id: shutoff
pin: 18
restore_mode: ALWAYS_OFF
name: "Fuel Shutoff"
inverted: false
on_turn_on:
- lambda: |-
if (id(ignition).state && !id(override_relay_on)) {
ESP_LOGD("relay", "Fuel shutoff relay blocked due to ignition being ON.");
id(shutoff).turn_off();
}
- platform: gpio
id: lock_switch
name: "Lock Switch"
pin: GPIO32
inverted: false
restore_mode: ALWAYS_OFF
on_turn_on:
- lambda: |-
if (id(horn_feedback_enabled)) {
int pulse = id(horn_pulse_time_ms);
id(horn_output).turn_on();
delay(pulse);
id(horn_output).turn_off();
}
- delay: 100ms
- switch.turn_off: lock_switch
- platform: gpio
id: unlock_switch
name: "Unlock Switch"
pin:
number: GPIO22
mode: OUTPUT
inverted: false
restore_mode: ALWAYS_OFF
on_turn_on:
- lambda: |-
if (id(horn_feedback_enabled)) {
int pulse = id(horn_pulse_time_ms);
id(horn_output).turn_on();
delay(pulse);
id(horn_output).turn_off();
delay(pulse);
id(horn_output).turn_on();
delay(pulse);
id(horn_output).turn_off();
}
- delay: 100ms
- switch.turn_off: unlock_switch
- platform: template
name: "Alarm Siren"
id: alarm_siren_switch
optimistic: true
restore_mode: ALWAYS_OFF
turn_on_action:
- lambda: |-
if (!id(siren_enabled)) {
ESP_LOGI("siren", "Siren blocked: siren_enabled=false. Keeping horn silent.");
// Revert optimistic ON immediately so alarm_pulse won't run
id(alarm_siren_switch).turn_off();
return;
}
if (id(gps_time).now().is_valid()) {
id(alarm_start_time) = id(gps_time).now().timestamp;
}
turn_off_action:
- lambda: |-
id(alarm_start_time) = 0;
- platform: gpio
pin: GPIO15
id: status_led
name: "Status LED"
restore_mode: ALWAYS_OFF
on_turn_on:
then:
- lambda: |-
ESP_LOGD("status_led", "LED turned ON (alarm armed)");
on_turn_off:
then:
- lambda: |-
ESP_LOGD("status_led", "LED turned OFF (alarm disarmed)");
- platform: template
name: "Find Horn"
id: find_switch
restore_mode: ALWAYS_OFF
optimistic: true
- platform: template
name: "Alarm Control"
id: alarm_web_switch
restore_mode: ALWAYS_OFF
lambda: |-
return id(alarm_armed);
turn_on_action:
- logger.log: "Web UI -> ARM"
- lambda: |-
id(set_alarm_state).execute(true, false) ;
turn_off_action:
- logger.log: "Web UI -> DISARM"
- lambda: |-
id(set_alarm_state).execute(false, false);
script:
- id: do_lock
parameters:
silent: bool
mode: restart
then:
- if:
condition:
lambda: 'return silent || !id(horn_feedback_enabled);'
then:
- switch.turn_on: lock_switch
else:
- lambda: |-
id(lock_switch).turn_on();
- id: do_unlock
parameters:
silent: bool
mode: restart
then:
- if:
condition:
lambda: 'return silent || !id(horn_feedback_enabled);'
then:
- switch.turn_on: unlock_switch
else:
- lambda: |-
id(unlock_switch).turn_on();
- id: do_arm
parameters:
silent: bool
mode: restart
then:
- lambda: |-
static uint32_t last = 0;
uint32_t now = millis();
if (now - last < 1000) { ESP_LOGW("arm","Throttled arming."); return; }
last = now;
if (id(ignition).state) { ESP_LOGW("arm","Ignition is ON; arming anyway."); }
id(alarm_armed) = true;
id(alarm_triggered) = false;
- switch.turn_on: shutoff
- logger.log: "Arming -> running IMU calibration..."
- lambda: |-
id(calibrate_imu_all).execute();
- lambda: |-
id(do_lock).execute(silent);
- id: do_disarm
parameters:
silent: bool
mode: restart
then:
- lambda: |-
static uint32_t last = 0;
uint32_t now = millis();
if (now - last < 1000) { ESP_LOGW("arm","Throttled disarming."); return; }
last = now;
id(alarm_armed) = false;
id(alarm_triggered) = false;
- switch.turn_off: alarm_siren_switch
- switch.turn_off: shutoff
- lambda: |-
id(do_unlock).execute(silent);
- id: set_alarm_state
parameters:
armed: bool
silent: bool
mode: restart
then:
- if:
condition:
lambda: 'return armed;'
then:
- lambda: |-
id(do_arm).execute(silent);
else:
- lambda: |-
id(do_disarm).execute(silent);
# IMU calibration
- id: calibrate_imu_all
mode: restart
then:
- lambda: |-
ESP_LOGI("imu", "Starting IMU calibration. Keep the vehicle perfectly still...");
const int N = 200;
float sax=0, say=0, saz=0, sgz=0;
int n_acc=0, n_gz=0;
for (int i = 0; i < N; i++) {
if (id(ax).has_state() && id(ay).has_state() && id(az).has_state()) {
sax += id(ax).state; say += id(ay).state; saz += id(az).state; n_acc++;
}
if (id(gz_dps).has_state()) { sgz += id(gz_dps).state; n_gz++; }
delay(10);
}
if (n_acc == 0 || n_gz == 0) {
ESP_LOGW("imu", "Calibration failed: missing IMU samples (acc:%d, gz:%d).", n_acc, n_gz);
return;
}
float axm = sax / n_acc, aym = say / n_acc, azm = saz / n_acc;
float gzm = sgz / n_gz;
const float rad2deg = 57.2957795f;
float pitch_raw = atan2f(-axm, sqrtf(aym*aym + azm*azm)) * rad2deg;
float roll_raw = atan2f(aym, azm) * rad2deg;
float pitch_now = pitch_raw - id(pitch_offset_deg);
float roll_now = roll_raw - id(roll_offset_deg);
bool already_level = (fabsf(pitch_now) <= id(level_tol_deg)) && (fabsf(roll_now) <= id(level_tol_deg));
id(pitch_offset_deg) = pitch_raw;
id(roll_offset_deg) = roll_raw;
id(gyro_z_bias) = gzm;
if (id(yaw_deg).has_state()) {
id(yaw_offset_deg) = id(yaw_deg).state + id(yaw_offset_deg); // align to 0
} else {
id(yaw_offset_deg) = 0.0f;
}
ESP_LOGI("imu", "Calibration done. pitch_off=%.2f deg, roll_off=%.2f deg, gz_bias=%.3f deg/s. Already level: %s",
id(pitch_offset_deg), id(roll_offset_deg), id(gyro_z_bias),
already_level ? "YES" : "NO");
lock:
- platform: template
name: "Car Lock"
id: car_lock
optimistic: false
lambda: |-
if (id(alarm_armed)) {
return esphome::lock::LOCK_STATE_LOCKED;
} else {
return esphome::lock::LOCK_STATE_UNLOCKED;
}
lock_action:
- logger.log: "Locking via car_lock"
- lambda: |-
id(set_alarm_state).execute(true, false);
unlock_action:
- logger.log: "Unlocking via car_lock"
- lambda: |-
id(set_alarm_state).execute(false, false);
number:
- platform: template
name: "Tilt Alarm Threshold (deg)"
id: tilt_threshold_number
optimistic: true
min_value: 1.0
max_value: 45.0
step: 0.5
initial_value: 3.0
set_action:
- lambda: |-
id(tilt_alarm_threshold_deg) = x;
ESP_LOGI("tilt", "Tilt threshold set to %.1f deg", id(tilt_alarm_threshold_deg));
button:
- platform: restart
name: "Restart"
id: reboot_trigger
- platform: template
name: "Send Test SMS"
on_press:
then:
- lambda: |-
id(gsm)->send_sms("+61234567890", "Test SMS from ESPHome!");
- platform: template
id: imu_cal_button
name: "Calibrate IMU"
on_press:
then:
- lambda: |-
id(calibrate_imu_all).execute();
output:
- platform: gpio
pin: GPIO19
id: horn_output
inverted: false
gps:
uart_id: gpsmodule
id: gps_sensor
update_interval: 60s
latitude:
name: "GPS Latitude"
id: latitude
longitude:
name: "GPS Longitude"
id: longitude
speed:
name: "GPS Speed"
id: speed
satellites:
name: "GPS Satellites"
id: gps_satellites
time:
- platform: gps
id: gps_time
Commands
The following commands will be accepted, and replies will be sent accordingly.
relay_on
relay_off
lock
unlock
where
status
arm
disarm
ap_on
ap_off
time
reboot
help
find
battery_voltage
notifications_on
notifications_off
horn_feedback_on
horn_feedback_off
horn_pulsetime 100ms (in milliseconds)
door_alerts_on
door_alerts_off
auto_arm_on
auto_arm_off
auto_arm_time 10 (in minutes)
phone_number_1
phone_number_2
phone_number_3
new_number_1
new_number_2
new_number_3
remove_number_1
remove_number_2
remove_number_3
schedule_1
schedule_2
schedule_3
new_schedule_1
new_schedule_2
new_schedule_3
remove_schedule_1
remove_schedule_2
remove_schedule_3
watchdog_on
watchdog_off
watchdog_days 3
siren_runtime 30
siren_on
siren_off
deep_sleep_enable
deep_sleep_disable
sleep_time 23:00
wake_time 07:00
calibrate_level
imu_status
The following messages will be sent. The replies for the relay activation will be sent only when the state of the relay is set correctly. The unit will send a conformation message on boot when powered up or rebooted only when an active GPS lock and GPS time is acquired.
Example commands are shown in bold below.
(reboot)
Device booted successfully at (GPS time) 2025-01-01 22:00:00 UTC
(arm)
Alarm armed. Notifications enabled, fuel shutoff is ON and doors are locked.
(disarm) - Defaults to DISARMED when rebooted.
Alarm disarmed. Notifications disabled, fuel shutoff is OFF and doors are unlocked.
(relay_on, relay_off) - Defaults to OFF when rebooted.
Fuel Shutoff Relay has been turned on.
Fuel Shutoff Relay has been turned off.
(status)
GPS Fix: True
GPS Time: 2025-02-03 20:00:00
Satellites: 12
Uptime: 8d 1h 32m
Ignition: OFF/ON
Doors: Closed/Open
Relay: OFF
Alarm State: Armed
WiFi AP: Enabled
Horn Feedback: ON
Door Alerts: ON
Notifications: ON
SIM800L RSSI: -77dBm
SIM800L Signal: 58%
Horn Pulsetime: 250ms
Siren Runtime: 30 sec
Auto-arm: ON/OFF
Auto-arm Delay: 10 min
Voltage Alarm: 12.00v
Battery Voltage: 12.34v
Current Draw: -0.07A
Watchdog: ON
Watchdog Interval: 1 day(s)
Deep Sleep (UTC):
Deep Sleep: ENABLED
Sleep: 23:00
Wake: 09:00
(where)
Device Location:
Lat: xxx.xxxxxx
Long: xxx.xxxxxxx
Google Maps: Google maps link will be displayed here with co-ordinates
Ignition: On/Off
Doors: Closed/Open
Speed: 5 kkm/h
(phone_number_x)
Phone Number 1: +1234567890
Phone Number 2: +2212345678
Phone Number 3: +2412345567
(new_number_x +1234567890)
Phone number updated to: +1234567890
(remove_number_x)
Phone Number 2 removed
(lock, unlock)
Doors Locked.
Doors Unlocked.
(schedule_x)
schedule_1:
Days: mon,tues,wed,thurs,fri,sat,sun
Arm: 23:00
Disarm: 07:00
(remove_schedule_x)
Schedule x deleted successfully.
(new_schedule_x mon,tues,wed,thurs,fri,sat,sun arm 22:00:00 disarm 23:00:00)
Schedule added successfully.
(horn_feedback_on, horn_feedback_off)
Horn Feedback Enabled
Horn Feedback Disabled
(horn_pulsetime 250ms)
Horn pulse time set to 250ms.
(notifications_on, notifications_off)
Notifications have been enabled
Notifications have been disabled
(door_alerts_on, door_alerts_off)
Door alerts enabled.
Door alerts disabled.
(time)
GPS Time: 2025-02-03 22:00:00
(battery)
Battery Voltage: 12.41v
Current Draw: -0.07A
(battery_voltage 12.00)
Battery alert voltage set to 12.00V
(find)
Find command activated. Horn will sound 3 times.
(watchdog_on, watchdog_off)
Watchdog enabled.
Watchdog disabled.
(auto_arm_on, auto_arm_off)
Auto-arm Enabled.
Auto-arm Disabled.
(auto_arm_time 10)
Auto-arm time set to 10 minute(s).
(watchdog_days x)
Watchdog interval set to 1 day(s).
(siren_runtime 30)
Siren runtime updated to 30 seconds.
(deep_sleep_enabled, deep_sleep_disabled)
Deep Sleep ENABLED. Schedule: 23:00 -> 07:00 (UTC)
Deep Sleep DISABLED.
(calibrae_level)
IMU Calibrated. PitchOff=4.03@@,RollOff=-2.23@@,GzBias=0.610@@/s. Level: YES
(imu_status)
Pitch: 0.00 deg
Roll: 0.00 deg
Car speed alarm activated. Car is travelling more than 10km/h.
ALERT: Tilt 8.7@@ (Pitch 8.7@@, Roll 0.0@@)
Unauthorized
Unrecognized Command
This will look like the following.
1 2



