Skip to content

Instantly share code, notes, and snippets.

@skylord123
Last active February 2, 2026 17:31
Show Gist options
  • Select an option

  • Save skylord123/567b466876da8b5ebdbe6ab465380dab to your computer and use it in GitHub Desktop.

Select an option

Save skylord123/567b466876da8b5ebdbe6ab465380dab to your computer and use it in GitHub Desktop.
Auto reboot modem

Auto Power Cycle Modem

image

Overview

This Node-RED flow provides automated modem power cycling when internet connectivity issues are detected. It monitors multiple network health indicators and triggers smart modem reboots based on configurable thresholds, helping maintain reliable internet connectivity without manual intervention.

Read the article for this here

Companion Flow: This flow complements the Internet Connectivity Monitor by providing automated remediation when issues are detected.

Features

This flow monitors four distinct conditions that can trigger an automatic modem reboot:

1. Sustained Internet Outage

  • Triggers when internet is completely down for more than 2 minutes
  • Ensures the modem has been powered on for at least 3 minutes before rebooting (prevents rapid cycling)

2. Intermittent Connectivity Issues

  • Monitors for frequent connection drops over a 5-minute window
  • Configurable threshold (default: 3 outages in 5 minutes)
  • Requires modem to be powered on for at least 5 minutes before triggering
  • Analyzes historical state changes to identify unstable connections

3. Remote Host Packet Loss Detection

  • Monitors packet loss across multiple remote hosts (Google, Cloudflare DNS)
  • Intelligent multi-host verification prevents false positives from single-endpoint issues
  • Configurable parameters in the Packet Loss Tracker function node:
    const THRESHOLD_PERCENT = 10;        // Packet loss percentage threshold
    const REQUIRED_FAILED_HOSTS = 2;     // Number of hosts that must exceed threshold
    const DURATION_SECONDS = 60;         // How long ALL hosts must be failing
  • Example: With default settings, requires 2 remote hosts with ≥10% packet loss for 60 seconds
  • Prevents unnecessary reboots when a single remote service (e.g., Google or Cloudflare) has independent issues

4. Router-to-Modem Packet Loss

  • Monitors packet loss on the local link between router and modem using sensor.modem_packet_loss
  • Detects issues with the physical connection or modem performance
  • Triggers on ≥10% packet loss sustained for 2 minutes
  • Requires modem powered on for at least 5 minutes

Control Features

Auto-Reboot Switch

  • Global on/off control via Home Assistant entity: switch.auto_reboot_modem
  • When disabled, all automatic reboot triggers are suppressed
  • Separate switch available for packet loss monitoring: switch.auto_reboot_modem_for_packet_loss

Reboot Flow Protection

  • Prevents multiple simultaneous reboot attempts using flow context variable
  • Automatic cooldown period between reboot cycles
  • Sequential reboot logic:
    1. Turn off modem power (smart plug)
    2. Wait 5 seconds
    3. Turn on modem power
    4. Wait 20 seconds for modem to stabilize
    5. Monitor for internet restoration (2-minute timeout)
    6. Send appropriate notification (success or retry)

Notification System

  • Centralized notification hub via link nodes
  • Customizable notification messages for different trigger scenarios:
    • "Internet outage detected. Power cycling modem."
    • "Intermittent outage detected. Power cycling modem."
    • "Packet loss detected. Power cycling modem."
    • "Internet restored" (success)
    • "Internet is still out, power cycling modem again." (retry)
  • Extensible notification delivery options:
    • Smart speaker announcements
    • Mobile app push notifications
    • Matrix/chat room messages (works during internet outages with self-hosted servers)
    • Log file recording

Prerequisites

Required Home Assistant Entities

  • binary_sensor.internet_status - Binary sensor indicating internet connectivity
  • switch.kauf_plug_two - Smart plug controlling modem power (replace with your own switch entity)
  • sensor.google_com_packet_loss - Packet loss percentage to Google
  • sensor.cloudflare_dns_packet_loss - Packet loss percentage to Cloudflare DNS
  • sensor.modem_packet_loss - Packet loss between router and modem

Required Node-RED Nodes

  • node-red-contrib-home-assistant-websocket (v0.80.3 or compatible)

Installation

  1. Import the flow JSON into Node-RED
  2. Replace all instances of switch.kauf_plug_two with your own smart switch/plug entity ID that controls your modem's power
    • Use the search function in Node-RED (Ctrl+F / Cmd+F) to find all occurrences
    • There are multiple nodes that reference this entity
  3. Verify all other Home Assistant entities are correctly configured for your setup
  4. Enable the Auto reboot modem switch in Home Assistant
  5. Configure notification endpoints as desired
  6. Deploy the flow

Configuration

Adjusting Trigger Thresholds

Sustained Outage Duration:

  • Modify the "Internet out for more than X minutes" group
  • Change the for parameter in the state-changed node (default: 2 minutes)

Intermittent Outage Detection:

  • Edit the THRESHOLD variable in the "Internet intermittently out" function node
  • Adjust TIME_WINDOW_MINUTES to change the monitoring period (default: 5 minutes)

Remote Host Packet Loss Parameters:

  • Open the Packet Loss Tracker function node
  • Modify configuration constants at the top of the code
  • Adjust REQUIRED_FAILED_HOSTS to require more/fewer failing endpoints
  • Change DURATION_SECONDS to modify how long conditions must persist

Router-to-Modem Packet Loss:

  • Modify the threshold percentage in the state-changed node (default: 10%)
  • Adjust the for duration (default: 2 minutes)

Modem Stabilization Times:

  • Delay nodes control power-off duration (default: 5 seconds) and post-boot wait time (default: 20 seconds)
  • Adjust these based on your modem's boot requirements

Notification Customization

Connect your preferred notification method to the "Modem Reboot Notifications" link input node (link in 5). The payload contains the notification message string.

How It Works

The flow uses a state machine approach with flow context variables to prevent race conditions. When a trigger condition is met:

  1. Checks if auto_reboot_modem switch is enabled
  2. Verifies modem has been powered on for minimum duration
  3. Checks if another reboot flow is already running
  4. Sets flow context flag to prevent concurrent reboots
  5. Executes power cycle sequence
  6. Monitors for internet restoration
  7. Sends appropriate notifications
  8. Clears flow context flag

If internet doesn't restore within the timeout period, the flow recursively triggers another reboot attempt.

[{"id":"23d70618471de7f9","type":"group","z":"b47ba089725ec70c","name":"Auto power cycle modem","style":{"label":true},"nodes":["7cf2da6f6cc4ad32","5661f6970db0df66","268001e2046e8b04","dae9c2ab26ac60a3","3b7b6fb56f174063"],"x":48,"y":433,"w":2504,"h":1034},{"id":"7cf2da6f6cc4ad32","type":"group","z":"b47ba089725ec70c","g":"23d70618471de7f9","name":"Internet out for more than X minutes","style":{"label":true},"nodes":["216c4c4b31b7c4b2","2a5ad0b04cd24f6a","7f656cbc9b04b6df","d8733d0984a6870a","d0dd9272018c9949","06ec5f4ea951b3a9","7f125919f0066c1f"],"x":74,"y":639,"w":1242,"h":182},{"id":"216c4c4b31b7c4b2","type":"server-state-changed","z":"b47ba089725ec70c","g":"7cf2da6f6cc4ad32","name":"","server":"233a9c63.e2baf4","version":6,"outputs":2,"exposeAsEntityConfig":"","entities":{"entity":["binary_sensor.internet_status"],"substring":[],"regex":[]},"outputInitially":false,"stateType":"str","ifState":"off","ifStateType":"str","ifStateOperator":"is","outputOnlyOnStateChange":true,"for":"2","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":true,"ignoreCurrentStateUnavailable":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":270,"y":720,"wires":[["2a5ad0b04cd24f6a"],[]]},{"id":"2a5ad0b04cd24f6a","type":"api-current-state","z":"b47ba089725ec70c","g":"7cf2da6f6cc4ad32","name":"Modem Powered on for 3 minutes","server":"233a9c63.e2baf4","version":3,"outputs":2,"halt_if":"on","halt_if_type":"str","halt_if_compare":"is","entity_id":"switch.kauf_plug_two","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"}],"for":"3","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":620,"y":720,"wires":[["d8733d0984a6870a","06ec5f4ea951b3a9"],[]]},{"id":"7f656cbc9b04b6df","type":"change","z":"b47ba089725ec70c","g":"7cf2da6f6cc4ad32","name":"Notification Message","rules":[{"t":"set","p":"payload","pt":"msg","to":"Internet outage detected. Power cycling modem.","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1132,"y":716.0000095367432,"wires":[["7f125919f0066c1f"]]},{"id":"d8733d0984a6870a","type":"ha-switch","z":"b47ba089725ec70c","g":"7cf2da6f6cc4ad32","name":"Auto reboot modem","version":0,"debugenabled":false,"inputs":1,"outputs":2,"entityConfig":"ff28e76e61713996","enableInput":true,"outputOnStateChange":false,"outputProperties":[{"property":"outputType","propertyType":"msg","value":"state change","valueType":"str"},{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"}],"x":902,"y":716.0000095367432,"wires":[["7f656cbc9b04b6df"],[]]},{"id":"d0dd9272018c9949","type":"inject","z":"b47ba089725ec70c","g":"7cf2da6f6cc4ad32","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":680,"wires":[["2a5ad0b04cd24f6a"]]},{"id":"06ec5f4ea951b3a9","type":"link out","z":"b47ba089725ec70c","g":"7cf2da6f6cc4ad32","name":"link out 2","mode":"link","links":["26e36143526b4543"],"x":835,"y":780,"wires":[]},{"id":"7f125919f0066c1f","type":"link out","z":"b47ba089725ec70c","g":"7cf2da6f6cc4ad32","name":"link out 7","mode":"link","links":["d273041df2cc0c7a"],"x":1275,"y":720,"wires":[]},{"id":"233a9c63.e2baf4","type":"server","name":"Home Assistant","version":6,"addon":false,"rejectUnauthorizedCerts":true,"ha_boolean":["y","yes","true","on","home","open"],"connectionDelay":true,"cacheJson":true,"heartbeat":false,"heartbeatInterval":"30","areaSelector":"friendlyName","deviceSelector":"friendlyName","entitySelector":"friendlyName","statusSeparator":"at: ","statusYear":"hidden","statusMonth":"short","statusDay":"numeric","statusHourCycle":"h23","statusTimeFormat":"h:m","enableGlobalContextStore":true},{"id":"ff28e76e61713996","type":"ha-entity-config","server":"233a9c63.e2baf4","deviceConfig":"c76f3feee4e3dec6","name":"Auto reboot modem","version":6,"entityType":"switch","haConfig":[{"property":"name","value":"Auto reboot modem"},{"property":"icon","value":"mdi:restart"},{"property":"entity_picture","value":""},{"property":"entity_category","value":""},{"property":"device_class","value":""}],"resend":false,"debugEnabled":false},{"id":"c76f3feee4e3dec6","type":"ha-device-config","name":"Ping Tests","hwVersion":"","manufacturer":"Node-RED","model":"","swVersion":""},{"id":"5661f6970db0df66","type":"group","z":"b47ba089725ec70c","g":"23d70618471de7f9","name":"Internet intermittently out X times in X minutes","style":{"label":true},"nodes":["fe6c091f0cd552ce","1d18761951aeea07","21a2113481e52225","eacd3b9cc6309eb6","74c6cf1c48095b02","824b3f807998037e","a9ea99ae0d1a7ce8","af32694e902f135c"],"x":74,"y":459,"w":1912,"h":142},{"id":"fe6c091f0cd552ce","type":"server-state-changed","z":"b47ba089725ec70c","g":"5661f6970db0df66","name":"","server":"233a9c63.e2baf4","version":6,"outputs":2,"exposeAsEntityConfig":"","entities":{"entity":["binary_sensor.internet_status"],"substring":[],"regex":[]},"outputInitially":false,"stateType":"str","ifState":"off","ifStateType":"str","ifStateOperator":"is","outputOnlyOnStateChange":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":true,"ignoreCurrentStateUnavailable":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":270,"y":500,"wires":[["21a2113481e52225"],[]]},{"id":"1d18761951aeea07","type":"api-get-history","z":"b47ba089725ec70c","g":"5661f6970db0df66","name":"Get internet status history for last 5 mins","server":"233a9c63.e2baf4","version":1,"startDate":"","endDate":"","entityId":"binary_sensor.internet_status","entityIdType":"equals","useRelativeTime":true,"relativeTime":"5 minutes","flatten":true,"outputType":"array","outputLocationType":"msg","outputLocation":"payload","x":1120,"y":500,"wires":[["74c6cf1c48095b02"]]},{"id":"21a2113481e52225","type":"ha-switch","z":"b47ba089725ec70c","g":"5661f6970db0df66","name":"Auto reboot modem","version":0,"debugenabled":false,"inputs":1,"outputs":2,"entityConfig":"ff28e76e61713996","enableInput":true,"outputOnStateChange":false,"outputProperties":[{"property":"outputType","propertyType":"msg","value":"state change","valueType":"str"},{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"}],"x":550,"y":500,"wires":[["eacd3b9cc6309eb6"],[]]},{"id":"eacd3b9cc6309eb6","type":"api-current-state","z":"b47ba089725ec70c","g":"5661f6970db0df66","name":"Modem power on for >= 5 mins","server":"233a9c63.e2baf4","version":3,"outputs":2,"halt_if":"on","halt_if_type":"str","halt_if_compare":"is","entity_id":"switch.kauf_plug_two","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"}],"for":"5","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":810,"y":500,"wires":[["1d18761951aeea07"],[]]},{"id":"74c6cf1c48095b02","type":"function","z":"b47ba089725ec70c","g":"5661f6970db0df66","name":"Internet intermittently out 3 times in 5 minutes","func":"// Configuration\nconst THRESHOLD = 3; // Number of \"off\" transitions that trigger a restart\nconst TIME_WINDOW_MINUTES = 5; // Time window to analyze\n\n// Get the history array\nconst history = msg.payload;\n\n// Validate input\nif (!Array.isArray(history) || history.length === 0) {\n node.status({\n fill: \"grey\",\n shape: \"ring\",\n text: \"No history data\"\n });\n return null;\n}\n\n// Count transitions to \"off\" state\nlet offCount = 0;\nlet lastState = null;\n\n// Sort by timestamp to ensure chronological order\nconst sortedHistory = history.sort((a, b) => {\n const dateA = new Date(a.last_changed).getTime();\n const dateB = new Date(b.last_changed).getTime();\n return dateA - dateB;\n});\n\n// Iterate through history and count transitions to \"off\"\nfor (let i = 0; i < sortedHistory.length; i++) {\n const currentState = sortedHistory[i].state;\n\n // Count transition to \"off\" state\n if (currentState === \"off\" && lastState !== \"off\") {\n offCount++;\n }\n\n lastState = currentState;\n}\n\n// Check if threshold is exceeded\nconst thresholdExceeded = offCount >= THRESHOLD;\n\n// Update node status\nif (thresholdExceeded) {\n node.status({\n fill: \"red\",\n shape: \"dot\",\n text: `Threshold exceeded: ${offCount} outages in ${TIME_WINDOW_MINUTES} mins`\n });\n} else {\n node.status({\n fill: \"green\",\n shape: \"ring\",\n text: `${offCount} of ${THRESHOLD} outages (OK)`\n });\n}\n\n// Prepare output message with details\nmsg.outageCount = offCount;\nmsg.threshold = THRESHOLD;\nmsg.thresholdExceeded = thresholdExceeded;\nmsg.timeWindow = TIME_WINDOW_MINUTES;\n\n// Only pass message through if threshold is exceeded\nif (thresholdExceeded) {\n return msg;\n} else {\n return null;\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1490,"y":500,"wires":[["824b3f807998037e","a9ea99ae0d1a7ce8"]]},{"id":"824b3f807998037e","type":"change","z":"b47ba089725ec70c","g":"5661f6970db0df66","name":"Notification Message","rules":[{"t":"set","p":"payload","pt":"msg","to":"Intermittent outage detected. Power cycling modem.","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1820,"y":500,"wires":[["af32694e902f135c"]]},{"id":"a9ea99ae0d1a7ce8","type":"link out","z":"b47ba089725ec70c","g":"5661f6970db0df66","name":"link out 3","mode":"link","links":["26e36143526b4543"],"x":1735,"y":560,"wires":[]},{"id":"af32694e902f135c","type":"link out","z":"b47ba089725ec70c","g":"5661f6970db0df66","name":"link out 5","mode":"link","links":["d273041df2cc0c7a"],"x":1945,"y":500,"wires":[]},{"id":"268001e2046e8b04","type":"group","z":"b47ba089725ec70c","g":"23d70618471de7f9","name":"Reboot modem","style":{"label":true},"nodes":["4913a92b1fd8fefd","d9c13f57616eec3b","06fc61e336eac1d5","1a246935b93238b0","d6b46b3d1df26943","335f889da8eb0f1a","0d7bdc4cdb0f49af","59c175617cbbdd45","827fe9ac1834dec9","a706875040421635","e88832b85aafc5d9","7e37c92bf34ac927","85d6fcc378833d4e","0499f9b237823081","26e36143526b4543","6cd7fa99c9faec59","826cfed30520542e"],"x":94,"y":1099,"w":2432,"h":172},{"id":"4913a92b1fd8fefd","type":"api-call-service","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"","server":"233a9c63.e2baf4","version":7,"debugenabled":false,"action":"switch.turn_off","floorId":[],"areaId":[],"deviceId":[],"entityId":["switch.kauf_plug_two"],"labelId":[],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"switch","service":"turn_off","x":1000,"y":1200,"wires":[["d9c13f57616eec3b"]]},{"id":"d9c13f57616eec3b","type":"delay","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":1160,"y":1200,"wires":[["06fc61e336eac1d5"]]},{"id":"06fc61e336eac1d5","type":"api-call-service","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"","server":"233a9c63.e2baf4","version":7,"debugenabled":false,"action":"switch.turn_on","floorId":[],"areaId":[],"deviceId":[],"entityId":["switch.kauf_plug_two"],"labelId":[],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","blockInputOverrides":true,"domain":"switch","service":"turn_on","x":1320,"y":1200,"wires":[["0499f9b237823081"]]},{"id":"1a246935b93238b0","type":"change","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"Failure Notification Message","rules":[{"t":"set","p":"payload","pt":"msg","to":"Internet is still out, power cycling modem again.","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":2270,"y":1220,"wires":[["826cfed30520542e"]]},{"id":"d6b46b3d1df26943","type":"ha-switch","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"Auto reboot modem","version":0,"debugenabled":false,"inputs":1,"outputs":2,"entityConfig":"ff28e76e61713996","enableInput":true,"outputOnStateChange":false,"outputProperties":[{"property":"outputType","propertyType":"msg","value":"state change","valueType":"str"},{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"}],"x":442,"y":1216.0000095367432,"wires":[["335f889da8eb0f1a"],["e88832b85aafc5d9"]]},{"id":"335f889da8eb0f1a","type":"change","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"","rules":[{"t":"set","p":"modem_restart_flow_running","pt":"flow","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":1196,"wires":[["4913a92b1fd8fefd"]]},{"id":"0d7bdc4cdb0f49af","type":"switch","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"","property":"payload","propertyType":"flow","rules":[{"t":"true"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":250,"y":1220,"wires":[[],["d6b46b3d1df26943"]]},{"id":"59c175617cbbdd45","type":"change","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"","rules":[{"t":"set","p":"modem_restart_flow_running","pt":"flow","to":"false","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":2090,"y":1140,"wires":[[]]},{"id":"827fe9ac1834dec9","type":"change","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"Success Notification Message","rules":[{"t":"set","p":"payload","pt":"msg","to":"Internet restored","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":2290,"y":1180,"wires":[["826cfed30520542e"]]},{"id":"a706875040421635","type":"ha-wait-until","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"","server":"233a9c63.e2baf4","version":3,"outputs":2,"entities":{"entity":["binary_sensor.internet_status"],"substring":[],"regex":[]},"property":"state","comparator":"is","value":"on","valueType":"str","timeout":"2","timeoutType":"num","timeoutUnits":"minutes","checkCurrentState":true,"blockInputOverrides":true,"outputProperties":[],"x":1820,"y":1200,"wires":[["59c175617cbbdd45","7e37c92bf34ac927"],["d6b46b3d1df26943","85d6fcc378833d4e"]]},{"id":"e88832b85aafc5d9","type":"change","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"","rules":[{"t":"set","p":"modem_restart_flow_running","pt":"flow","to":"false","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":1230,"wires":[[]]},{"id":"7e37c92bf34ac927","type":"ha-switch","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"Auto reboot modem","version":0,"debugenabled":false,"inputs":1,"outputs":2,"entityConfig":"ff28e76e61713996","enableInput":true,"outputOnStateChange":false,"outputProperties":[{"property":"outputType","propertyType":"msg","value":"state change","valueType":"str"},{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"}],"x":2030,"y":1180,"wires":[["827fe9ac1834dec9"],[]]},{"id":"85d6fcc378833d4e","type":"ha-switch","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"Auto reboot modem","version":0,"debugenabled":false,"inputs":1,"outputs":2,"entityConfig":"ff28e76e61713996","enableInput":true,"outputOnStateChange":false,"outputProperties":[{"property":"outputType","propertyType":"msg","value":"state change","valueType":"str"},{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"}],"x":2030,"y":1220,"wires":[["1a246935b93238b0"],[]]},{"id":"0499f9b237823081","type":"ha-switch","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"Auto reboot modem","version":0,"debugenabled":false,"inputs":1,"outputs":2,"entityConfig":"ff28e76e61713996","enableInput":true,"outputOnStateChange":false,"outputProperties":[{"property":"outputType","propertyType":"msg","value":"state change","valueType":"str"},{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"}],"x":1510,"y":1200,"wires":[["6cd7fa99c9faec59"],[]]},{"id":"26e36143526b4543","type":"link in","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"link in 2","links":["06ec5f4ea951b3a9","a9ea99ae0d1a7ce8","0b69b8cade99f703"],"x":135,"y":1220,"wires":[["0d7bdc4cdb0f49af"]]},{"id":"6cd7fa99c9faec59","type":"delay","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"","pauseType":"delay","timeout":"20","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":1680,"y":1200,"wires":[["a706875040421635"]]},{"id":"826cfed30520542e","type":"link out","z":"b47ba089725ec70c","g":"268001e2046e8b04","name":"link out 8","mode":"link","links":["d273041df2cc0c7a"],"x":2485,"y":1200,"wires":[]},{"id":"dae9c2ab26ac60a3","type":"group","z":"b47ba089725ec70c","g":"23d70618471de7f9","name":"Auto restart modem if packet loss >= 10% for 2 minutes","style":{"label":true},"nodes":["29d6fa3df3efe624","ac1612132924da81","de22234e0eecc2d1","cc48cd3baac974ba","0b69b8cade99f703","3da5e9fa08ac155a","64ce7caafb2ddcd8","978dd20c024e3f4b","b3b630c450ebe17a","b8a118efd3d9bd47"],"x":74,"y":859,"w":1592,"h":202},{"id":"29d6fa3df3efe624","type":"server-state-changed","z":"b47ba089725ec70c","g":"dae9c2ab26ac60a3","name":"","server":"233a9c63.e2baf4","version":6,"outputs":2,"exposeAsEntityConfig":"","entities":{"entity":["sensor.modem_packet_loss"],"substring":[],"regex":[]},"outputInitially":false,"stateType":"str","ifState":"10","ifStateType":"num","ifStateOperator":"gte","outputOnlyOnStateChange":true,"for":"2","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":true,"ignoreCurrentStateUnavailable":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":270,"y":900,"wires":[["ac1612132924da81"],[]]},{"id":"ac1612132924da81","type":"ha-switch","z":"b47ba089725ec70c","g":"dae9c2ab26ac60a3","name":"Auto reboot modem","version":0,"debugenabled":false,"inputs":1,"outputs":2,"entityConfig":"ff28e76e61713996","enableInput":true,"outputOnStateChange":false,"outputProperties":[{"property":"outputType","propertyType":"msg","value":"state change","valueType":"str"},{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"}],"x":630,"y":900,"wires":[["cc48cd3baac974ba"],[]]},{"id":"de22234e0eecc2d1","type":"api-current-state","z":"b47ba089725ec70c","g":"dae9c2ab26ac60a3","name":"Modem power on for >= 5 mins","server":"233a9c63.e2baf4","version":3,"outputs":2,"halt_if":"on","halt_if_type":"str","halt_if_compare":"is","entity_id":"switch.kauf_plug_two","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"}],"for":"5","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":1210,"y":900,"wires":[["0b69b8cade99f703","3da5e9fa08ac155a"],[]]},{"id":"cc48cd3baac974ba","type":"ha-switch","z":"b47ba089725ec70c","g":"dae9c2ab26ac60a3","name":"Auto reboot modem for packet loss","version":0,"debugenabled":false,"inputs":1,"outputs":2,"entityConfig":"350fca0b1d8c235b","enableInput":true,"outputOnStateChange":false,"outputProperties":[{"property":"outputType","propertyType":"msg","value":"state change","valueType":"str"},{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"}],"x":900,"y":900,"wires":[["de22234e0eecc2d1"],[]]},{"id":"0b69b8cade99f703","type":"link out","z":"b47ba089725ec70c","g":"dae9c2ab26ac60a3","name":"link out 4","mode":"link","links":["26e36143526b4543"],"x":1385,"y":940,"wires":[]},{"id":"3da5e9fa08ac155a","type":"change","z":"b47ba089725ec70c","g":"dae9c2ab26ac60a3","name":"Notification Message","rules":[{"t":"set","p":"payload","pt":"msg","to":"Packet loss detected. Power cycling modem.","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1480,"y":900,"wires":[["64ce7caafb2ddcd8"]]},{"id":"64ce7caafb2ddcd8","type":"link out","z":"b47ba089725ec70c","g":"dae9c2ab26ac60a3","name":"link out 6","mode":"link","links":["d273041df2cc0c7a"],"x":1625,"y":900,"wires":[]},{"id":"978dd20c024e3f4b","type":"server-state-changed","z":"b47ba089725ec70c","g":"dae9c2ab26ac60a3","name":"","server":"233a9c63.e2baf4","version":6,"outputs":1,"exposeAsEntityConfig":"","entities":{"entity":["sensor.google_com_packet_loss"],"substring":[],"regex":[]},"outputInitially":true,"stateType":"str","ifState":"","ifStateType":"str","ifStateOperator":"is","outputOnlyOnStateChange":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"string","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":280,"y":980,"wires":[["b8a118efd3d9bd47"]]},{"id":"b3b630c450ebe17a","type":"server-state-changed","z":"b47ba089725ec70c","g":"dae9c2ab26ac60a3","name":"","server":"233a9c63.e2baf4","version":6,"outputs":1,"exposeAsEntityConfig":"","entities":{"entity":["sensor.cloudflare_dns_packet_loss"],"substring":[],"regex":[]},"outputInitially":true,"stateType":"str","ifState":"","ifStateType":"str","ifStateOperator":"is","outputOnlyOnStateChange":true,"for":"0","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"string","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":290,"y":1020,"wires":[["b8a118efd3d9bd47"]]},{"id":"b8a118efd3d9bd47","type":"function","z":"b47ba089725ec70c","g":"dae9c2ab26ac60a3","name":"Packet Loss Tracker","func":"/**\n * This node tracks packet loss from multiple endpoints and only fires if we have\n * issues from <REQUIRED_FAILED_HOSTS> hosts having >= <THRESHOLD_PERCENT> packet loss for <DURATION_SECONDS> seconds.\n * (example: 2 hosts having >= 10% packet loss for 60 seconds)\n */\n\n// Configuration constants\nconst THRESHOLD_PERCENT = 10; // Packet loss percentage threshold\nconst REQUIRED_FAILED_HOSTS = 2; // Number of hosts that must exceed threshold\nconst DURATION_SECONDS = 60; // How long ALL hosts must be failing\n\n// Initialize context storage if needed\ncontext.failedHosts = context.failedHosts || {};\ncontext.timer = context.timer || null;\n\n// Helper function to check if trigger conditions are met and send message if so\nfunction checkAndTrigger() {\n const now = Date.now();\n const hostFailureDurations = Object.entries(context.failedHosts).map(([id, startTime]) => ({\n entity_id: id,\n duration_ms: now - startTime,\n duration_sec: (now - startTime) / 1000\n }));\n \n const failedHostCount = hostFailureDurations.length;\n \n // Check if we have enough hosts and all have been failing long enough\n if (failedHostCount >= REQUIRED_FAILED_HOSTS) {\n const shortestFailureDuration = Math.min(...hostFailureDurations.map(h => h.duration_ms));\n const shortestFailureSec = shortestFailureDuration / 1000;\n \n if (shortestFailureSec >= DURATION_SECONDS) {\n node.status({fill: \"red\", shape: \"dot\", text: `TRIGGERED: ${failedHostCount} hosts failing for ${Math.round(shortestFailureSec)}s+`});\n \n // Build the trigger message\n const triggerMsg = {\n payload: {\n triggered: true,\n failed_host_count: failedHostCount,\n minimum_duration_seconds: Math.round(shortestFailureSec),\n threshold_percent: THRESHOLD_PERCENT,\n failing_hosts: hostFailureDurations.map(h => ({\n entity_id: h.entity_id,\n failing_for_seconds: Math.round(h.duration_sec)\n }))\n }\n };\n \n // Reset tracking\n context.failedHosts = {};\n context.timer = null;\n \n // Send the message\n node.send(triggerMsg);\n return true;\n }\n }\n return false;\n}\n\n// Extract entity ID and packet loss value\nconst entityId = msg.data.entity_id;\nconst packetLoss = parseFloat(msg.payload);\n\n// Update the state for this host\nif (packetLoss >= THRESHOLD_PERCENT) {\n // Host is failing - record the time it started failing if not already tracked\n if (!context.failedHosts[entityId]) {\n context.failedHosts[entityId] = Date.now();\n }\n} else {\n // Host recovered - remove from failed list\n if (context.failedHosts[entityId]) {\n delete context.failedHosts[entityId];\n }\n}\n\n// Count how many hosts are currently failing\nconst failedHostCount = Object.keys(context.failedHosts).length;\n\n// Clear any existing timer if we don't have enough failed hosts\nif (failedHostCount < REQUIRED_FAILED_HOSTS) {\n if (context.timer !== null) {\n clearTimeout(context.timer);\n context.timer = null;\n node.status({fill: \"green\", shape: \"dot\", text: `${failedHostCount} hosts failing - timer cleared`});\n } else if (failedHostCount > 0) {\n node.status({fill: \"green\", shape: \"dot\", text: `${failedHostCount} hosts failing`});\n } else {\n node.status({fill: \"green\", shape: \"dot\", text: \"All hosts OK\"});\n }\n return null;\n}\n\n// Check if we should trigger immediately\nif (checkAndTrigger()) {\n return null; // Message already sent by checkAndTrigger\n}\n\n// Not all hosts have been failing long enough - set/update timer\nconst now = Date.now();\nconst hostFailureDurations = Object.entries(context.failedHosts).map(([id, startTime]) => \n now - startTime\n);\nconst shortestFailureDuration = Math.min(...hostFailureDurations);\nconst timeUntilTrigger = (DURATION_SECONDS * 1000) - shortestFailureDuration;\n\n// Clear existing timer if there is one\nif (context.timer !== null) {\n clearTimeout(context.timer);\n}\n\n// Set new timer to check again when the shortest-failing host reaches the threshold\ncontext.timer = setTimeout(() => {\n context.timer = null;\n checkAndTrigger();\n}, timeUntilTrigger);\n\nnode.status({\n fill: \"yellow\", \n shape: \"ring\", \n text: `${failedHostCount} hosts failing - triggering in ${Math.round(timeUntilTrigger/1000)}s`\n});\n\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":680,"y":1000,"wires":[["ac1612132924da81"]]},{"id":"350fca0b1d8c235b","type":"ha-entity-config","server":"233a9c63.e2baf4","deviceConfig":"c76f3feee4e3dec6","name":"Auto reboot modem for packet loss","version":6,"entityType":"switch","haConfig":[{"property":"name","value":"Auto reboot modem for packet loss"},{"property":"icon","value":"mdi:restart"},{"property":"entity_picture","value":""},{"property":"entity_category","value":""},{"property":"device_class","value":""}],"resend":false,"debugEnabled":false},{"id":"3b7b6fb56f174063","type":"group","z":"b47ba089725ec70c","g":"23d70618471de7f9","name":"Modem Reboot Notifications","style":{"label":true},"nodes":["d273041df2cc0c7a","81f6e03cf2ee8dde"],"x":74,"y":1299,"w":184,"h":142},{"id":"d273041df2cc0c7a","type":"link in","z":"b47ba089725ec70c","g":"3b7b6fb56f174063","name":"link in 5","links":["af32694e902f135c","64ce7caafb2ddcd8","7f125919f0066c1f","826cfed30520542e"],"x":135,"y":1400,"wires":[[]]},{"id":"81f6e03cf2ee8dde","type":"comment","z":"b47ba089725ec70c","g":"3b7b6fb56f174063","name":"Docs","info":"You can use this link in node to send notifications about the current internet situation.\n\nFor example, you could:\n\n- Play the notification over a smart speaker\n- Send the message to a chat room (I self host my own Matrix chat server so it is accessible even if the internet is down)\n- Send a Home Assistant mobile companion app push notification\n- Log to a text file","x":150,"y":1340,"wires":[]},{"id":"01e09bb0309d8149","type":"global-config","env":[],"modules":{"node-red-contrib-home-assistant-websocket":"0.80.3"}}]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment