I just finished writing about my ESP8266+ESPHome Ping Tester – and realised that, as I was only using 43% of RAM and 48% of FLASH memory – it would be a crime to stop there – hence this short article.
The device reads pings from the network and thanks to an ESPHome WebUI (i.e. web page on the device, accessible via a browser) it is easy to select both internal and external IP addresses to ping. You get visible ping time, IP adress being pinged and a fail-count which again can be reset at any time on the WebUI.
And all of this from a device that costs less than many ESP32s, even including the WS2812 LED and standard I2c SSD1306 128*64 OLED display. I as expecting some review items to arrive about now but when they didn’t turn up I started to wonder if this device could be improved in the meantime.
Sure enough, right now I’ve added a 200-ping history, held in the device (time, IP, ping-time for each ping). The pings are held in RAM and have taken the used-RAM up from around 43% to 47%, the FLASH usage increase being hardly noticeable at 48% up from just over 47%. The information is retrieved from any old MQTT client.
This of course demands a new control on the WebUI to pause queue build-up while it is being read hence a new photo of the WebUI. I figured I’d better leave the original project as-is and put the changes and additions in here. Bear in mind, once set up, this doesn’t need a PC with ESPHome – it runs all by itself.
I’ve no doubt at some point I’ll get the urge to add more – all that spare memory doing nothing…
Meanwhile – the device is set up to use my MQTT broker at 192.168.1.20 and with my login credentials. When you compile this – check out your secrets file in ESPHome (not Home Assistant, the secrets file in the ESPHome folder).
Just as an example, this is my secrets file (details altered to protect the innocent) – I only have one password to serve multiple SSIDs as it’s a large house.
wifi_ssid: "myoffice" wifi_password: "xxxxxxxx" uk_wifi_ssid: "office" uk_wifi_password: "xxxxxxxxy" wifi_ssid2: "black" wifi_ssid3: "mlo" wifi_ssid4: "outside" wifi_ssid5: "wififorus" wifi_ssid6: "petes" wifi_ssid7: "comfast" mqtt_broker: 192.168.1.20 mqtt_username: aaaaa mqtt_password: bbbbbb abrobot_ota_password: ccccccccccccccccc ttgo_ota_password: ccccccccccc car_data_write_key: "blah1231blahblah" car_data_read_key: "moreblah123blahblah"
And now, the YAML code for the latest Ping Tester
substitutions:
device_name: oled-test
friendly_name: ESP8266 Ping Tester with ESPHome
device_version: "0.14"
code_source: "https://tech.scargill.net"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
libraries:
- ESPping@1.0.5
includes:
- <ESPping.h>
on_boot:
priority: -100
then:
- light.turn_on:
id: rgb_led
red: 100%
green: 40%
blue: 0%
esp8266:
board: nodemcuv2
restore_from_flash: true
logger:
level: DEBUG
api:
reboot_timeout: 0s
mqtt:
broker: !secret mqtt_broker
username: !secret mqtt_username
password: !secret mqtt_password
id: mqtt_client
discovery: false
discover_ip: false
web_server:
version: 3
ota:
- platform: esphome
wifi:
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
- ssid: !secret wifi_ssid2
password: !secret wifi_password
- ssid: !secret wifi_ssid3
password: !secret wifi_password
- ssid: !secret wifi_ssid4
password: !secret wifi_password
- ssid: !secret wifi_ssid5
password: !secret wifi_password
power_save_mode: none
post_connect_roaming: true
ap:
ssid: "${device_name} Fallback"
password: "12345678"
captive_portal:
preferences:
flash_write_interval: 10s
# ==================================================
# GLOBALS
# ==================================================
globals:
- id: ping_state
type: int
restore_value: no
initial_value: "0"
- id: ping_ms
type: int
restore_value: no
initial_value: "0"
- id: heartbeat
type: bool
restore_value: no
initial_value: "false"
# Persistent ping target IP
- id: ip1
type: int
restore_value: yes
initial_value: "192"
- id: ip2
type: int
restore_value: yes
initial_value: "168"
- id: ip3
type: int
restore_value: yes
initial_value: "1"
- id: ip4
type: int
restore_value: yes
initial_value: "1"
# Persistent failure counter
- id: failed_ping_count
type: uint32_t
restore_value: yes
initial_value: "0"
# Reset switch state
- id: reset_switch_state
type: bool
initial_value: 'false'
# In-RAM ping history. These arrays are deliberately not restored to flash.
- id: history_sequence
type: uint32_t[200]
restore_value: no
- id: history_ms
type: uint16_t[200]
restore_value: no
- id: history_status
type: uint8_t[200]
restore_value: no
- id: history_ip1
type: uint8_t[200]
restore_value: no
- id: history_ip2
type: uint8_t[200]
restore_value: no
- id: history_ip3
type: uint8_t[200]
restore_value: no
- id: history_ip4
type: uint8_t[200]
restore_value: no
- id: history_index
type: uint8_t
restore_value: no
initial_value: '0'
- id: history_count
type: uint16_t
restore_value: no
initial_value: '0'
- id: history_sequence_number
type: uint32_t
restore_value: no
initial_value: '0'
- id: history_paused
type: bool
restore_value: no
initial_value: 'false'
# ==================================================
# FONT
# ==================================================
font:
- file: "gfonts://Roboto"
id: font_small
size: 12
- file: "gfonts://Roboto"
id: font_heading
size: 14
# ==================================================
# I2C OLED
# ==================================================
i2c:
sda: D2
scl: D1
# ==================================================
# OLED DISPLAY
# ==================================================
display:
- platform: ssd1306_i2c
model: "SSD1306 128x64"
address: 0x3C
update_interval: 2s
lambda: |-
// OLED disabled
if (!id(oled_display).state) {
it.fill(COLOR_OFF);
return;
}
// Header
it.print(0, 0, id(font_heading), "PING TEST v${device_version}");
it.horizontal_line(0,15,128);
// Heartbeat
if (id(heartbeat)) {
it.print(118, 0, id(font_small), "*");
}
// Ping status
if (id(ping_state) == 0) {
it.print(0, 20, id(font_small), "SETTING UP");
} else if (id(ping_state) == 1) {
it.printf(
0, 20,
id(font_small),
"OK %d ms",
id(ping_ms)
);
} else {
if (id(ping_ms) > 500) {
it.printf(
0, 20,
id(font_small),
"FAILED %d ms",
id(ping_ms)
);
} else {
it.print(0, 20, id(font_small), "** FAILED **");
}
}
// Target IP
it.printf(
0, 36,
id(font_small),
"%d.%d.%d.%d",
id(ip1),
id(ip2),
id(ip3),
id(ip4)
);
// Persistent failure counter
char fail_text[20];
sprintf(
fail_text,
"Failures: %d",
(int) id(failed_ping_count)
);
int fail_width;
int x_offset;
int baseline;
int height;
id(font_small).measure(
fail_text,
&fail_width,
&x_offset,
&baseline,
&height
);
it.print(
128 - fail_width,
50,
id(font_small),
fail_text
);
# ==================================================
# RGB LED
# ==================================================
light:
- platform: neopixelbus
id: rgb_led
type: GRB
variant: WS2812
pin: D6
num_leds: 1
name: "RGB LED"
gamma_correct: 1.0
initial_state:
brightness: 26%
restore_state: all
# ==================================================
# TEXT SENSORS
# ==================================================
text_sensor:
- platform: template
name: "Firmware Version"
lambda: |-
return {"${device_version}"};
update_interval: 60s
- platform: template
name: "code_source"
lambda: |-
return {"${code_source}"};
update_interval: 60s
# ==================================================
# PING TARGET DROPDOWN
# ==================================================
select:
- platform: template
name: "Ping Target"
id: ping_target
optimistic: true
restore_value: true
initial_option: "Manual / Custom"
options:
- "Manual / Custom"
- "Google DNS - 8.8.8.8"
- "Google DNS - 8.8.4.4"
- "Cloudflare - 1.1.1.1"
- "Cloudflare - 1.0.0.1"
- "Quad9 - 9.9.9.9"
- "Quad9 - 149.112.112.112"
- "OpenDNS - 208.67.222.222"
- "OpenDNS - 208.67.220.220"
- "AdGuard - 94.140.14.14"
- "AdGuard - 94.140.15.15"
- "Router - 192.168.1.1"
- "Router - 192.168.0.1"
- "Router - 192.168.1.254"
- "Router - 192.168.0.254"
- "Local - 10.0.0.1"
- "Local - 10.10.0.1"
- "Local - 10.10.10.1"
- "Local - 172.16.0.1"
- "Local - 172.16.1.1"
on_value:
then:
- lambda: |-
if (x == "Google DNS - 8.8.8.8") {
id(ip1) = 8;
id(ip2) = 8;
id(ip3) = 8;
id(ip4) = 8;
} else if (x == "Google DNS - 8.8.4.4") {
id(ip1) = 8;
id(ip2) = 8;
id(ip3) = 4;
id(ip4) = 4;
} else if (x == "Cloudflare - 1.1.1.1") {
id(ip1) = 1;
id(ip2) = 1;
id(ip3) = 1;
id(ip4) = 1;
} else if (x == "Cloudflare - 1.0.0.1") {
id(ip1) = 1;
id(ip2) = 0;
id(ip3) = 0;
id(ip4) = 1;
} else if (x == "Quad9 - 9.9.9.9") {
id(ip1) = 9;
id(ip2) = 9;
id(ip3) = 9;
id(ip4) = 9;
} else if (x == "Quad9 - 149.112.112.112") {
id(ip1) = 149;
id(ip2) = 112;
id(ip3) = 112;
id(ip4) = 112;
} else if (x == "OpenDNS - 208.67.222.222") {
id(ip1) = 208;
id(ip2) = 67;
id(ip3) = 222;
id(ip4) = 222;
} else if (x == "OpenDNS - 208.67.220.220") {
id(ip1) = 208;
id(ip2) = 67;
id(ip3) = 220;
id(ip4) = 220;
} else if (x == "AdGuard - 94.140.14.14") {
id(ip1) = 94;
id(ip2) = 140;
id(ip3) = 14;
id(ip4) = 14;
} else if (x == "AdGuard - 94.140.15.15") {
id(ip1) = 94;
id(ip2) = 140;
id(ip3) = 15;
id(ip4) = 15;
} else if (x == "Router - 192.168.1.1") {
id(ip1) = 192;
id(ip2) = 168;
id(ip3) = 1;
id(ip4) = 1;
} else if (x == "Router - 192.168.0.1") {
id(ip1) = 192;
id(ip2) = 168;
id(ip3) = 0;
id(ip4) = 1;
} else if (x == "Router - 192.168.1.254") {
id(ip1) = 192;
id(ip2) = 168;
id(ip3) = 1;
id(ip4) = 254;
} else if (x == "Router - 192.168.0.254") {
id(ip1) = 192;
id(ip2) = 168;
id(ip3) = 0;
id(ip4) = 254;
} else if (x == "Local - 10.0.0.1") {
id(ip1) = 10;
id(ip2) = 0;
id(ip3) = 0;
id(ip4) = 1;
} else if (x == "Local - 10.10.0.1") {
id(ip1) = 10;
id(ip2) = 10;
id(ip3) = 0;
id(ip4) = 1;
} else if (x == "Local - 10.10.10.1") {
id(ip1) = 10;
id(ip2) = 10;
id(ip3) = 10;
id(ip4) = 1;
} else if (x == "Local - 172.16.0.1") {
id(ip1) = 172;
id(ip2) = 16;
id(ip3) = 0;
id(ip4) = 1;
} else if (x == "Local - 172.16.1.1") {
id(ip1) = 172;
id(ip2) = 16;
id(ip3) = 1;
id(ip4) = 1;
}
- component.update: ping_ip1
- component.update: ping_ip2
- component.update: ping_ip3
- component.update: ping_ip4
# ==================================================
# PING IP ADDRESS
# ==================================================
number:
- platform: template
name: "Ping IP 1"
id: ping_ip1
min_value: 0
max_value: 255
step: 1
mode: box
lambda: |-
return id(ip1);
set_action:
- lambda: |-
id(ip1) = (int)x;
- select.set:
id: ping_target
option: "Manual / Custom"
- platform: template
name: "Ping IP 2"
id: ping_ip2
min_value: 0
max_value: 255
step: 1
mode: box
lambda: |-
return id(ip2);
set_action:
- lambda: |-
id(ip2) = (int)x;
- select.set:
id: ping_target
option: "Manual / Custom"
- platform: template
name: "Ping IP 3"
id: ping_ip3
min_value: 0
max_value: 255
step: 1
mode: box
lambda: |-
return id(ip3);
set_action:
- lambda: |-
id(ip3) = (int)x;
- select.set:
id: ping_target
option: "Manual / Custom"
- platform: template
name: "Ping IP 4"
id: ping_ip4
min_value: 0
max_value: 255
step: 1
mode: box
lambda: |-
return id(ip4);
set_action:
- lambda: |-
id(ip4) = (int)x;
- select.set:
id: ping_target
option: "Manual / Custom"
# ==================================================
# PING FAILURE COUNTER SENSOR
# ==================================================
sensor:
- platform: template
name: "Ping Failures"
id: failed_ping_counter_sensor
accuracy_decimals: 0
update_interval: 5s
lambda: |-
return (float) id(failed_ping_count);
- platform: template
name: "Ping History Length"
id: ping_history_length_sensor
accuracy_decimals: 0
update_interval: 5s
lambda: |-
return (float) id(history_count);
# ==================================================
# OLED ON/OFF SWITCH
# ==================================================
switch:
- platform: template
name: "OLED Display"
id: oled_display
restore_mode: RESTORE_DEFAULT_ON
optimistic: true
turn_on_action:
- lambda: |-
// OLED enabled
turn_off_action:
- lambda: |-
// OLED disabled
- platform: template
name: "Reset Ping Counter"
icon: "mdi:gesture-tap"
id: this_dummy_switch_id
lambda: |-
return id(reset_switch_state);
turn_on_action:
- globals.set:
id: reset_switch_state
value: 'true'
- lambda: |-
id(failed_ping_count) = 0;
- component.update: failed_ping_counter_sensor
- delay: 150ms
- switch.turn_off: this_dummy_switch_id
turn_off_action:
- globals.set:
id: reset_switch_state
value: 'false'
- platform: template
name: "Clear Ping History"
icon: "mdi:delete-sweep"
id: clear_ping_history
optimistic: true
turn_on_action:
- lambda: |-
id(history_index) = 0;
id(history_count) = 0;
id(history_sequence_number) = 0;
char topic[24];
for (int n = 0; n < 200; n++) {
snprintf(topic, sizeof(topic), "pinger/%d", n + 1);
id(mqtt_client).publish(topic, "", 0, 0, true);
}
id(mqtt_client).publish("pinger/count", "0", 1, 0, true);
if (id(history_paused)) {
id(mqtt_client).publish("pinger/status", "PAUSED", 6, 0, true);
} else {
id(mqtt_client).publish("pinger/status", "RUNNING", 7, 0, true);
}
- component.update: ping_history_length_sensor
- delay: 150ms
- switch.turn_off: clear_ping_history
turn_off_action:
- lambda: |-
// Momentary switch.
- platform: template
name: "Pause Ping History"
id: pause_ping_history
optimistic: true
turn_on_action:
- lambda: |-
id(history_paused) = true;
char topic[24];
char payload[96];
// Publish a retained snapshot: newest entry is pinger/1.
for (int n = 0; n < id(history_count); n++) {
int idx = ((int)id(history_index) + 200 - 1 - n) % 200;
snprintf(topic, sizeof(topic), "pinger/%d", n + 1);
if (id(history_status)[idx] == 1) {
snprintf(payload, sizeof(payload),
"#%lu | %d.%d.%d.%d | %d ms | OK",
(unsigned long)id(history_sequence)[idx],
(int)id(history_ip1)[idx], (int)id(history_ip2)[idx],
(int)id(history_ip3)[idx], (int)id(history_ip4)[idx],
(int)id(history_ms)[idx]);
} else if (id(history_status)[idx] == 2) {
snprintf(payload, sizeof(payload),
"#%lu | %d.%d.%d.%d | %d ms | SLOW",
(unsigned long)id(history_sequence)[idx],
(int)id(history_ip1)[idx], (int)id(history_ip2)[idx],
(int)id(history_ip3)[idx], (int)id(history_ip4)[idx],
(int)id(history_ms)[idx]);
} else {
snprintf(payload, sizeof(payload),
"#%lu | %d.%d.%d.%d | FAILED",
(unsigned long)id(history_sequence)[idx],
(int)id(history_ip1)[idx], (int)id(history_ip2)[idx],
(int)id(history_ip3)[idx], (int)id(history_ip4)[idx]);
}
id(mqtt_client).publish(topic, payload, strlen(payload), 0, true);
}
// Clear any old retained entries beyond the current history length.
for (int n = id(history_count); n < 200; n++) {
snprintf(topic, sizeof(topic), "pinger/%d", n + 1);
id(mqtt_client).publish(topic, "", 0, 0, true);
}
snprintf(payload, sizeof(payload), "%u", id(history_count));
id(mqtt_client).publish("pinger/count", payload, strlen(payload), 0, true);
id(mqtt_client).publish("pinger/status", "PAUSED", 6, 0, true);
turn_off_action:
- lambda: |-
id(history_paused) = false;
char topic[24];
char payload[96];
// Publish the current history snapshot and resume live updates.
for (int n = 0; n < id(history_count); n++) {
int idx = ((int)id(history_index) + 200 - 1 - n) % 200;
snprintf(topic, sizeof(topic), "pinger/%d", n + 1);
if (id(history_status)[idx] == 1) {
snprintf(payload, sizeof(payload),
"#%lu | %d.%d.%d.%d | %d ms | OK",
(unsigned long)id(history_sequence)[idx],
(int)id(history_ip1)[idx], (int)id(history_ip2)[idx],
(int)id(history_ip3)[idx], (int)id(history_ip4)[idx],
(int)id(history_ms)[idx]);
} else if (id(history_status)[idx] == 2) {
snprintf(payload, sizeof(payload),
"#%lu | %d.%d.%d.%d | %d ms | SLOW",
(unsigned long)id(history_sequence)[idx],
(int)id(history_ip1)[idx], (int)id(history_ip2)[idx],
(int)id(history_ip3)[idx], (int)id(history_ip4)[idx],
(int)id(history_ms)[idx]);
} else {
snprintf(payload, sizeof(payload),
"#%lu | %d.%d.%d.%d | FAILED",
(unsigned long)id(history_sequence)[idx],
(int)id(history_ip1)[idx], (int)id(history_ip2)[idx],
(int)id(history_ip3)[idx], (int)id(history_ip4)[idx]);
}
id(mqtt_client).publish(topic, payload, strlen(payload), 0, true);
}
for (int n = id(history_count); n < 200; n++) {
snprintf(topic, sizeof(topic), "pinger/%d", n + 1);
id(mqtt_client).publish(topic, "", 0, 0, true);
}
snprintf(payload, sizeof(payload), "%u", id(history_count));
id(mqtt_client).publish("pinger/count", payload, strlen(payload), 0, true);
id(mqtt_client).publish("pinger/status", "RUNNING", 7, 0, true);
# ==================================================
# PING TEST
# ==================================================
interval:
- interval: 5s
then:
- lambda: |-
id(heartbeat) = !id(heartbeat);
IPAddress target(
id(ip1),
id(ip2),
id(ip3),
id(ip4)
);
// History status: 1=OK, 2=SLOW, 3=FAILED.
uint8_t history_result = 3;
if (Ping.ping(target, 3)) {
id(ping_ms) = (int) Ping.averageTime();
bool was_running = (id(ping_state) != 0);
// Count successful pings taking longer than 500ms.
// Do not count the first successful ping after startup.
if (was_running && id(ping_ms) > 500) {
id(failed_ping_count)++;
}
if (id(ping_ms) > 500) {
id(ping_state) = 2;
history_result = 2;
} else {
id(ping_state) = 1;
history_result = 1;
}
auto call = id(rgb_led).turn_on();
bool local_target =
(id(ip1) == 192 &&
id(ip2) == 168);
if (local_target) {
if (id(ping_ms) < 20) {
call.set_rgb(0.0, 1.0, 0.0);
} else if (id(ping_ms) < 50) {
call.set_rgb(0.5, 1.0, 0.0);
} else if (id(ping_ms) <= 100) {
call.set_rgb(1.0, 1.0, 0.0);
} else {
call.set_rgb(1.0, 0.4, 0.0);
}
} else {
if (id(ping_ms) < 75) {
call.set_rgb(0.0, 1.0, 0.0);
} else if (id(ping_ms) < 125) {
call.set_rgb(0.5, 1.0, 0.0);
} else if (id(ping_ms) <= 175) {
call.set_rgb(1.0, 1.0, 0.0);
} else {
call.set_rgb(1.0, 0.4, 0.0);
}
}
call.perform();
} else {
// Complete ping failure.
if (id(ping_state) == 0) {
// First ping failed.
// Establish that the tester is now running,
// but don't count this initial failure.
id(ping_state) = 2;
id(ping_ms) = 0;
auto call = id(rgb_led).turn_on();
call.set_rgb(1.0, 0.0, 0.0);
call.perform();
} else {
// Subsequent complete ping failure.
// Count it as a failure.
id(failed_ping_count)++;
id(ping_state) = 2;
id(ping_ms) = 0;
auto call = id(rgb_led).turn_on();
call.set_rgb(1.0, 0.0, 0.0);
call.perform();
}
}
// Store this result in the fixed-size RAM ring buffer.
uint8_t h = id(history_index);
id(history_sequence_number)++;
id(history_sequence)[h] = id(history_sequence_number);
id(history_ms)[h] = (id(ping_ms) < 65535) ? id(ping_ms) : 65535;
id(history_status)[h] = history_result;
id(history_ip1)[h] = id(ip1);
id(history_ip2)[h] = id(ip2);
id(history_ip3)[h] = id(ip3);
id(history_ip4)[h] = id(ip4);
id(history_index) = (h + 1) % 200;
if (id(history_count) < 200) {
id(history_count)++;
}
// While running, publish only the newest result and small status values.
// The full 200-entry retained snapshot is published when paused.
if (!id(history_paused)) {
char latest[96];
if (history_result == 1) {
snprintf(latest, sizeof(latest),
"#%lu | %d.%d.%d.%d | %d ms | OK",
(unsigned long)id(history_sequence_number),
id(ip1), id(ip2), id(ip3), id(ip4), id(ping_ms));
} else if (history_result == 2) {
snprintf(latest, sizeof(latest),
"#%lu | %d.%d.%d.%d | %d ms | SLOW",
(unsigned long)id(history_sequence_number),
id(ip1), id(ip2), id(ip3), id(ip4), id(ping_ms));
} else {
snprintf(latest, sizeof(latest),
"#%lu | %d.%d.%d.%d | FAILED",
(unsigned long)id(history_sequence_number),
id(ip1), id(ip2), id(ip3), id(ip4));
}
id(mqtt_client).publish("pinger/latest", latest, strlen(latest), 0, false);
char count_text[12];
snprintf(count_text, sizeof(count_text), "%u", id(history_count));
id(mqtt_client).publish("pinger/count", count_text, strlen(count_text), 0, true);
id(mqtt_client).publish("pinger/status", "RUNNING", 7, 0, true);
}
To get the history info from MQTT – the following subscribes are available:
pinger/1 pinger/2 pinger/3 … pinger/200
pinger/latest pinger/count pinger/status
For a quick check just subscribe to pinger/#


