Hi all, I’m having my esphome running on a NodeMCU ESP32 to control a smart lock for auto-lock & remote unlock. Everything works well up until I added an interval HTTP request to allow for remote unlock.
Problem: The ESP32 would always show a barebone web server layout once every few hours, which I guess is a safe mode? In this state, I can’t do any OTA either from a PC (error timeout) or from the web interface (success, but still in barebone mode). Instead, the firmware can get back on its own if I reboot the board.
For the remote unlock part, I’m using Telegram bot to access an “/open” command via polling. I have an interval check to retrieve the latest message.
The strange thing is that I already disabled safe_mode component just to make sure, but it still happens regardless, so I’m not sure if the condition here is “safe mode” or not. Here’s the screenshot of the web server:
One thing I noticed is that the HTTP request inside the polling interval can take a while, it would always warn about ~2000ms execution time. Maybe that’s related?
10:29:21 [W] [component:453] interval took a long time for an operation (3964 ms)
10:29:21 [W] [component:456] Components should block for at most 30 ms
10:29:21 [W] [component:453] api took a long time for an operation (3970 ms)
10:29:21 [W] [component:456] Components should block for at most 30 ms
In any case, my code is below. Any help is appreciated, thanks! Do note that I’ve switched my board around and the issue is consistent across all of them.
substitutions:
telegram_token: !secret telegram_bot_token
telegram_chat: !secret telegram_chat_id
use_battery: true
# No battery: Auto-close on power.
# Battery: Close on door sensor.
door_open_notify_time: 15
# In seconds
esphome:
name: smart-door
on_boot:
- priority: 550
then: # No-battery mode: Power connected = closed
- if:
condition:
lambda: return ${0 if use_battery else 1};
then:
- button.press: door_close
esp32:
board: nodemcu-32s
framework:
type: esp-idf
sdkconfig_options:
CONFIG_LWIP_MAX_SOCKETS: '16'
safe_mode:
disabled: true
# Enable logging
logger:
logs:
# Prevent component from spamming the logs
http_request.idf: ERROR
# Enable Home Assistant API
api:
password: ""
ota:
- platform: esphome
password: ""
wifi:
ssid: "Ab-Apart"
password: !secret wifi_password
fast_connect: true
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Smart-Door Fallback Hotspot"
password: !secret ap_password
captive_portal:
http_request:
time:
- platform: sntp
id: time_now
timezone: Europe/Berlin
on_time_sync:
- if:
condition:
lambda: return id(pending_close_time) == true;
then:
# Update the previously pending close time
- globals.set:
id: global_close_time
value: !lambda |-
time_t currTime = id(time_now).now().timestamp;
return currTime;
- globals.set:
id: pending_close_time
value: "false"
globals:
- id: global_open_time
type: time_t
restore_value: true
initial_value: ""
- id: global_close_time
type: time_t
restore_value: true
initial_value: ""
- id: last_update_id
type: int
restore_value: true
initial_value: "0"
- id: pending_close_time
type: bool
restore_value: false
initial_value: "false"
- id: door_open_notify_sent
type: bool
restore_value: false
initial_value: "false"
- id: telegram_poll_counter
type: int
restore_value: false
initial_value: "0"
# UI Controls
number:
- platform: template
id: servo_duration
name: Servo Rotation Duration
min_value: 1
restore_value: true
initial_value: 4.5
max_value: 6
step: 0.5
optimistic: true
web_server:
sorting_group_id: sorting_group_servo_settings
- platform: template
id: servo_number
name: Servo Control
min_value: -100
initial_value: 0
max_value: 100
step: 1
optimistic: true
web_server:
sorting_group_id: sorting_group_servo_settings
set_action:
# Detach if given value is 0
then:
- servo.write:
id: servo_hw
level: !lambda 'return x / 100.0;'
- if:
condition:
lambda: 'return x == 0;'
then:
- delay: !lambda 'return 50;'
- servo.detach:
id: servo_hw
- logger.log:
format: "Servo detached"
button:
# Most components have REST API, read more: https://esphome.io/web-api/#api-rest
- platform: template
name: Open
id: door_open
# Equivalent: http://smart-door.local/button/open/press (using name, not id)
web_server:
sorting_group_id: sorting_group_door_actions
on_press:
# Move servo until the door is opened, or timeout
if:
condition: # Only if not currently moving
lambda: 'return id(servo_number).state == 0;'
then:
- globals.set:
id: global_open_time
value: !lambda |-
time_t currTime = id(time_now).now().timestamp;
return currTime;
- number.set:
id: servo_number
value: -100
- wait_until:
condition:
binary_sensor.is_on: door_is_open
timeout: !lambda 'return id(servo_duration).state * 1000 + 1000;'
# Continue opening a bit after the door is opened
- delay: !lambda 'return 1500;'
- number.set:
id: servo_number
value: 0
- platform: template
name: Close
id: door_close
web_server:
sorting_group_id: sorting_group_door_actions
on_press:
# Only trigger closing if the door is not currently opening
- if:
condition:
time.has_time:
then:
- globals.set:
id: global_close_time
value: !lambda |-
time_t currTime = id(time_now).now().timestamp;
return currTime;
else:
- globals.set:
id: pending_close_time
value: "true"
- if:
condition:
lambda: 'return id(servo_number).state >= 0;'
then:
- number.set:
id: servo_number
value: 100
- delay: !lambda 'return id(servo_duration).state * 1000;'
- number.set:
id: servo_number
value: 0
- platform: template
name: Zero
id: servo_zero
web_server:
sorting_group_id: sorting_group_servo_settings
on_press:
then:
- number.set:
id: servo_number
value: 0
# Hidden button to trigger status message (called from interval)
- platform: template
id: send_status_message
internal: true
on_press:
then:
- http_request.post:
url: "https://api.telegram.org/bot${telegram_token}/sendMessage"
request_headers:
Content-Type: application/json
json: |-
std::string status_emoji = id(door_is_open).state ? "🟥" : "🟩";
std::string status_text = id(door_is_open).state ? "OPEN" : "CLOSED";
// ... etc.
interval:
- interval: 1s
then:
# Check if door still opens: Door is open and more than 5 seconds since
- if:
condition:
lambda: return id(door_is_open).state && !id(door_open_notify_sent) && id(time_now).now().timestamp - id(global_open_time) > ${door_open_notify_time};
then:
- globals.set:
id: door_open_notify_sent
value: "true"
- logger.log:
format: "Door is opened for more than ${door_open_notify_time} seconds"
- http_request.post:
url: "https://api.telegram.org/bot${telegram_token}/sendMessage"
request_headers:
Content-Type: application/json
json: |-
std::string message = "*Warning: Door is OPEN 🟥* \n";
root["chat_id"] = ${telegram_chat};
root["text"] = message;
root["parse_mode"] = "Markdown";
# Poll Telegram for new messages every 1 second (or 5s when door is closed). This allows faster detection of open => close.
- lambda: |-
// Increment counter
id(telegram_poll_counter) = id(telegram_poll_counter) + 1;
// If door is open, only poll every 5 seconds (every 5th trigger)
// If door is closed, poll every second
bool should_poll = (!id(door_is_open).state) || (id(telegram_poll_counter) >= 5);
if (should_poll) {
id(telegram_poll_counter) = 0; // Reset counter
}
- if:
condition:
lambda: return id(telegram_poll_counter) == 0;
then:
- http_request.get:
url: "https://api.telegram.org/bot${telegram_token}/getUpdates?offset=-1&limit=1"
capture_response: true
on_response:
- if:
condition:
lambda: return response->status_code == 200;
then:
- lambda: |-
json::parse_json(body, [](JsonObject root) -> bool {
// code here
});
else:
- logger.log:
format: "Telegram API error: %d"
args: ['response->status_code']
text_sensor:
# Just for displaying globals in web UI
- platform: template
id: time_close
name: Door Last Closed
lambda: |-
char str[30];
time_t currTime = id(global_close_time);
strftime(str, sizeof(str), "%Y-%m-%d %H:%M", localtime(&currTime));
return std::string(str);
update_interval: 30s
- platform: template
id: time_open
name: Door Last Opened
lambda: |-
char str[30];
time_t currTime = id(global_open_time);
strftime(str, sizeof(str), "%Y-%m-%d %H:%M", localtime(&currTime));
return std::string(str);
update_interval: 30s
# Hardwares
binary_sensor:
# Optional, only when using a battery (always on)
- platform: gpio
id: door_is_open
name: Door is Open
device_class: door
pin:
number: GPIO12
mode:
input: true
pullup: true
# Means door is closed on release
filters:
- delayed_off: 100ms
on_release:
then: # Low = closed. Auto-lock when door is closed
- if:
# If previously we sent open warning, and not it's closed, then send new status
condition:
lambda: return id(door_open_notify_sent);
then:
- globals.set:
id: door_open_notify_sent
value: "false"
- button.press: send_status_message
- if:
condition:
lambda: return ${1 if use_battery else 0};
then:
- button.press: door_close
- platform: gpio
id: door_button
name: Door Button
device_class: door
pin:
number: GPIO14
inverted: true
mode:
input: true
pullup: true
filters:
- delayed_on_off: 100ms
on_press:
then: # High = pressed. Open the door
- button.press: door_open
servo:
- id: servo_hw
output: servo_pwm_output
output:
- platform: ledc
id: servo_pwm_output
pin: GPIO13
frequency: 50Hz
web_server:
version: 3
port: 80
sorting_groups:
- id: sorting_group_door_actions
name: "Door Actions"
sorting_weight: 10
- id: sorting_group_servo_settings
name: "Servo Settings"
sorting_weight: 20
