Just figured I’d share something I put together recently.
We had an internet outage the other day and it ended up being that the modem had just locked up or something (resetting it fixed the problem).
I figured there had to be a way to have an on-esp-device way to manage this.
This was installed on a Sonoff S31.
This depends on a custom component to perform pings: GitHub - trombik/esphome-component-ping: esphome component that sends and receives ICMP
I’ll admit I am no expert with ESPHome so ChatGPT helped with some of this. I understand it, verified before using, and tested it a bit before putting it to work - but it may not necessarily be the most efficient implementation of this functionality
I also tested by blocking access to the IP I was pinging periodically and things worked as desired.
I also added an automation in HA to notify me a few minutes before a reset. I believe these notifications will work on the local network even if the internet is down, and if not at home, and I receive the notification, I’ll know its like a false positive.
esphome:
name: cable-modem-sonoff-s31
friendly_name: Cable Modem Sonoff S31
libraries:
- ESP8266WiFi
- https://github.com/akaJes/AsyncPing#95ac7e4
on_boot:
- priority: -10 # Ensures this runs later in the boot process
then:
- logger.log:
level: INFO
format: "On boot: disabling ping checks (internal) for 5 minutes!"
- delay: 5min
- lambda: |-
id(ping_check_enabled) = true;
- logger.log:
level: INFO
format: "On boot: Ping checks enabled after successful boot (with 5 min delay)!"
esp8266:
board: esp01_1m
# Enable logging
logger:
baud_rate: 0 # (UART logging interferes with cse7766)
level: DEBUG
# Enable Home Assistant API
api:
encryption:
key: XXXXX
ota:
password: XXXXX
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Cable-Modem-Sonoff-S31"
password: XXXXX
captive_portal:
external_components:
- source:
type: git
url: https://github.com/trombik/esphome-component-ping # I forked this to my own controlled repo for security reasons.
ref: main
globals:
- id: ping_check_enabled
type: bool
initial_value: 'false' # At boot we default to disable ping checks for 5 minutes.
- id: packet_loss_above_threshold_start_time
type: uint32_t
initial_value: '0'
- id: packet_loss_threshold
type: int
initial_value: '90' # Packet loss threshold percentage
- id: packet_loss_period
type: int
initial_value: '300' # Seconds before packet loss causes reset
- id: waiting_to_reenable_ping
type: bool
initial_value: 'false'
- id: ha_ping_control_enabled
type: bool
initial_value: 'true' # Assume ping checks are enabled by default from HA
uart:
rx_pin: RX
baud_rate: 4800
# Normal S31 Plug Logic
binary_sensor:
- platform: gpio
pin:
number: GPIO0
mode: INPUT_PULLUP
inverted: True
name: "Cable Modem Sonoff S31 Button"
on_press:
- switch.toggle: relay
- platform: status
name: "Cable Modem Sonoff S31 Status"
# Normal S31 Plug Logic
sensor:
- platform: wifi_signal
name: "Cable Modem Sonoff S31 WiFi Signal"
update_interval: 60s
- platform: cse7766
current:
name: "Cable Modem Sonoff S31 Current"
accuracy_decimals: 1
filters:
- throttle_average: 30s
voltage:
name: "Cable Modem Sonoff S31 Voltage"
accuracy_decimals: 1
filters:
- throttle_average: 30s
power:
name: "Cable Modem Sonoff S31 Power"
accuracy_decimals: 1
filters:
- throttle_average: 30s
id: my_power
- platform: total_daily_energy
name: "Cable Modem Sonoff S31 Daily Energy"
power_id: my_power
filters:
- throttle_average: 30s
# Ping logic
- platform: ping
ip_address: 1.1.1.1 # Can be anything, I'd recommend a reliable DNS provider though
num_attempts: 5 # number of packets to send
# the timeout. however, this is not what you usually expect from `ping`
# implementation: the timeout is also the interval to send packets. if you
# set this value to 10 sec, and the network is fine (no packet loss), then
# the component sends a packet at 10 sec interval, and the total time to
# finish would be 10 sec * num_attempts = 10 * 17 = 170 sec.
timeout: 2sec
update_interval: 60s # the interval for checking the sensors. defaults to 60s.
loss:
name: Packet loss
id: loss
- platform: uptime
name: "Cable Modem Sonoff S31 Uptime"
id: esphome_uptime
update_interval: 60s # Adjust the update interval as needed
filters:
- lambda: return x / 60; # Convert seconds to minutes
unit_of_measurement: "minutes"
interval:
# This lambda function is executed every 5 seconds to monitor packet loss.
# If packet loss exceeds a defined threshold (id(packet_loss_threshold)), it starts a timer to track how long the condition persists.
# - If the packet loss remains above this threshold for a duration exceeding id(packet_loss_period) seconds, and if ping checks are enabled,
# it logs this event, then proceeds to reset the relay: turning it off for 10 seconds before turning it back on.
# - This relay reset aims to potentially correct the high packet loss condition.
# - After the relay reset, if ping checks were not already waiting to be re-enabled, it disables ping checks and sets a flag to indicate
# it's waiting for ping checks to be re-enabled, preventing immediate re-triggering of the condition.
# - If the packet loss drops below the threshold at any check, it logs this event and resets the start time, ready to monitor the condition afresh.
# This approach ensures actions are taken only if the packet loss condition persists, avoiding unnecessary relay resets for brief spikes in packet loss.
- interval: 5s
then:
- lambda: |-
if (id(loss).state > id(packet_loss_threshold)) {
if (id(packet_loss_above_threshold_start_time) == 0) {
id(packet_loss_above_threshold_start_time) = id(sntp_time).now().timestamp;
ESP_LOGD("custom", "Packet loss exceeded threshold. Starting timer.");
} else if ((id(sntp_time).now().timestamp - id(packet_loss_above_threshold_start_time)) >= id(packet_loss_period)) {
if (id(ha_ping_control_enabled) && id(ping_check_enabled)) {
ESP_LOGI("custom", "Packet loss has been above threshold for more than 3 minutes. Triggering relay reset.");
// Reset the relay here
id(relay).turn_off();
delay(10000); // 10 seconds delay
id(relay).turn_on();
// Log relay reset
ESP_LOGI("custom", "Relay has been reset.");
if (!id(waiting_to_reenable_ping)) {
id(ping_check_enabled) = false;
id(waiting_to_reenable_ping) = true;
ESP_LOGI("custom", "Ping checks disabled, waiting to re-enable.");
}
}
}
} else {
if (id(packet_loss_above_threshold_start_time) != 0) {
ESP_LOGD("custom", "Packet loss dropped below threshold. Resetting timer.");
id(packet_loss_above_threshold_start_time) = 0;
}
}
# This lambda function is triggered every minute to manage the re-enabling of ping checks.
# It uses a static variable, disable_time, to track when the waiting period to re-enable ping checks began.
# - If the device is in the state of waiting to re-enable ping checks (id(waiting_to_reenable_ping) is true),
# and if disable_time is not yet set, it records the current time in millis() as the start of the waiting period.
# - It then checks if 5 minutes (300,000 milliseconds) have passed since disable_time was set. If so, it indicates
# the waiting period is over, and it proceeds to:
# 1. Re-enable ping checks by setting id(ping_check_enabled) to true,
# 2. Reset the flag indicating it was waiting to re-enable ping checks (id(waiting_to_reenable_ping)),
# 3. Reset disable_time and id(packet_loss_above_threshold_start_time) to 0, effectively clearing the timer and
# packet loss monitoring state.
# - A log message is generated to indicate that ping checks have been re-enabled after the waiting period.
# This approach ensures a non-blocking wait period is observed before attempting to re-enable ping checks,
# allowing other device functionalities to proceed uninterrupted during the wait.
- interval: 1min
then:
- lambda: |-
static unsigned long disable_time = 0;
if (id(waiting_to_reenable_ping)) {
if (disable_time == 0) {
// Record the time we started waiting
disable_time = millis();
} else if (millis() - disable_time > 300000) { // 5 minutes in milliseconds
// Time has passed, re-enable ping checks and reset variables
id(ping_check_enabled) = true;
id(waiting_to_reenable_ping) = false;
disable_time = 0;
id(packet_loss_above_threshold_start_time) = 0;
ESP_LOGI("custom", "Ping checks re-enabled after waiting period.");
}
}
switch:
- platform: gpio # Normal S31 Plug Switch
name: "Cable Modem Sonoff S31 Relay"
pin: GPIO12
id: relay
restore_mode: ALWAYS_ON
- platform: template # Let's us manually disable ping checks from HA - note that pings keep happening, we just ignore them.
name: "Cable Modem Sonoff S31 Ping Checks (Manual)"
id: ha_control_ping_checks
optimistic: true
restore_mode: ALWAYS_ON
turn_on_action:
- lambda: |-
id(ha_ping_control_enabled) = true;
ESP_LOGI("custom", "Ping checks enabled via Home Assistant.");
turn_off_action:
- lambda: |-
id(ha_ping_control_enabled) = false;
ESP_LOGI("custom", "Ping checks disabled via Home Assistant.");
time:
- platform: sntp
id: sntp_time
status_led:
pin: GPIO13
UPDATE (2 days later): Today was the first live-test. I noticed the internet was down 3 minutes in. 2 minutes later the S31 initiated the reset. I could have done it faster, but if I weren’t home/awake it would have taken care of it for me.
And yes, I am concerned something is up with the cable modem needing to be reset twice in less than a week.