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: - [email protected] 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/#
Read More