Internet Connection Health Dashboard in Home Assistant

Example of Issue with Internet Connection

When reporting internet connectivity issues, ISP support often defaults to blaming the user’s hardware. Proving that the fault lies on the ISP’s side is challenging, particularly with streaming performance issues. To build a solid case, it’s essential to gather evidence by tracing the network path and identifying which specific hop is responsible for the latency.

For this type of diagnostics, MTR is the superior utility. This guide provides step-by-step instructions on how to add MTR data to your Home Assistant dashboard, giving you real-time visibility into your ISP’s performance.

Table of Contents
    Add a header to begin generating the table of contents

    Why Monitor with TCP-based MTR?

    If you’ve ever struggled to prove to your ISP that your connection is lagging, this guide is for you. Here is the high-level breakdown of the solution:

    • The Gap: Home Assistant lacks native tools for deep network path analysis.

    • The Solution: A custom script that uses MTR (My Traceroute) with TCP probes to simulate real-world streaming traffic to YouTube.

    • The Integration: Data is pushed directly to Home Assistant via a Webhook Sensor for real-time dashboard visualization.

    • Why TCP? Unlike standard ICMP (ping), TCP probes reveal hidden congestion and jitter that actually affect your browsing and streaming.

    • Expert Tip: This script focuses on the final three hops of the path. By monitoring the “Last Mile” to the service provider (like YouTube/Google), you filter out local noise and see exactly how the destination server is receiving your data, even if your ISP’s firewall drops monitoring packets for any reason.

    • The Universal Approach: This solution is specifically designed to work across all Home Assistant versions. Because the script pushes data via Webhooks, it is the perfect workaround for HAOS (Home Assistant OS) users who cannot easily install custom binaries, but can run a script on another instanse in their home lab. HA Docker setup users can run that script on the same host as HA.

    Create the Script

    First of all, install the necessary tools:

    sudo apt-get update && sudo apt-get install mtr bc jq curl -y

     

    ToolPurpose in your Script
    mtrPerforms the actual TCP-based traceroute to redirector.googlevideo.com.
    bcHandles the mathematical comparisons (floating-point math) for thresholds (e.g., checking if $window_max_lat > $LATENCY_THRESHOLD).
    jqProcesses and formats the complex JSON payload before sending it to Home Assistant.
    curlPerforms the POST request to the Home Assistant API/Webhook.

    Next, create the file and make it excecutable:

    sudo touch /opt/host_availability.sh
    sudo chmod +x /opt/host_availability.sh

    Open it in your preferred text editor e.g. mcedit, and add the following script:

    #!/usr/bin/env bash
    
    # Requirements: sudo apt install mtr bc jq -y
    TOKEN="YOUR_HA_TOKEN"
    
    
    # --- CONFIGURATION ---
    TARGET="redirector.googlevideo.com"
    PORT=443
    CYCLES=5
    INTERVAL=1
    TIMEOUT=2
    CHECK_WINDOW=3      # Only look at the last 3 visible hops
    LOSS_THRESHOLD=25   # Alert if loss in window > 25%
    LATENCY_THRESHOLD=150
    SD_THRESHOLD=100
    
    export LC_NUMERIC=C
    
    HA_URL="YOUR_HA_URL" # Ensure port is correct for your setup
    
    # --- SCRIPT START ---
    timestamp() {
        date -u +"%Y-%m-%dT%H:%M:%SZ"
    }
    
    # Run mtr with wide report to ensure columns don't truncate
    mtr_output=$(mtr --report --report-cycles="$CYCLES" --tcp --port="$PORT" --no-dns --report-wide --timeout=$TIMEOUT --interval="$INTERVAL" "$TARGET" 2>&1)
    
    if [[ $? -ne 0 ]]; then
        echo "Error running mtr: $mtr_output" >&2
        exit 1
    fi
    
    max_loss=0
    worst_hop=""
    worst_avg_latency=0
    hops_json="["
    
    # 1. Parse full MTR report into JSON including StDev
    while IFS= read -r line; do
        # This regex captures Hop, Host, Loss, Sent, Avg, and StDev
        if [[ "$line" =~ ^[[:space:]]*([0-9]+)\.\|--[[:space:]]+([^[:space:]]+)[[:space:]]+([0-9.]+)%[[:space:]]+([0-9]+)[[:space:]]+[0-9.]+[[:space:]]+([0-9.]+)[[:space:]]+[0-9.]+[[:space:]]+([0-9.]+)[[:space:]]+([0-9.]+) ]]; then
            hop_num="${BASH_REMATCH[1]}"
            host="${BASH_REMATCH[2]}"
            loss="${BASH_REMATCH[3]}"
            sent="${BASH_REMATCH[4]}"
            avg="${BASH_REMATCH[5]}"
            worst="${BASH_REMATCH[6]}"
            stdev="${BASH_REMATCH[7]}"
    
            # Append to JSON string
            hops_json+=$(printf '{"hop":%d,"host":"%s","loss_percent":%.1f,"sent":%d,"avg_ms":%.1f,"worst_ms":%.1f,"stdev":%.1f},' \
                "$hop_num" "$host" "$loss" "$sent" "$avg" "$worst" "$stdev")
    
            # Track absolute worsts for internal debugging
            if (( $(echo "$loss > $max_loss" | bc -l) )); then
                max_loss="$loss"
                worst_hop="$hop_num ($host)"
                worst_avg_latency=$(printf "%.1f" "$avg")
            fi
        fi
    done <<< "$mtr_output"
    
    # Remove trailing comma and close array
    hops_json="${hops_json%,}]"
    
    # --- 2. Extract specific Window for On/Off Logic ---
    recent_hops=$(echo "$mtr_output" | grep "|--" | grep -v "???" | tail -n "$CHECK_WINDOW")
    
    # Extract Window Loss
    window_max_loss=$(echo "$recent_hops" | tr -d '%' | awk '{print $3}' | sort -rn | head -n 1)
    # Extract Window Latency (Avg)
    window_max_lat=$(echo "$recent_hops" | awk '{print $6}' | sort -rn | head -n 1)
    # Extract Window Jitter (StDev)
    window_max_sd=$(echo "$recent_hops" | awk '{print $9}' | sort -rn | head -n 1)
    
    # SAFETY: If any value is not a valid number, force it to 0
    [[ ! "$window_max_loss" =~ ^[0-9.]+$ ]] && window_max_loss=0
    [[ ! "$window_max_lat" =~ ^[0-9.]+$ ]] && window_max_lat=0
    [[ ! "$window_max_sd" =~ ^[0-9.]+$ ]] && window_max_sd=0
    
    # --- 3. Final Status Determination ---
    
    # Check if BOTH thresholds are exceeded at the same time
    
    if (( $(echo "$window_max_lat > $LATENCY_THRESHOLD" | bc -l) )) && (( $(echo "$window_max_loss > $LOSS_THRESHOLD" | bc -l) )); then
        status="on"
        friendly="Network Crisis: High Latency (${window_max_lat}ms) AND High Loss (${window_max_loss}%) detected."
    
    # Optional: Keep individual checks as warnings or turn status "off" for them
    elif (( $(echo "$window_max_sd > $SD_THRESHOLD" | bc -l) )); then
        status="on"
        friendly="High Jitter: ${window_max_sd}ms at destination."
    
    else
        status="off"
        friendly="Stable: ${window_max_lat}ms (SD: ${window_max_sd}ms)"
    fi
    
    
    # 4. Create the data payload
    # We pass the windowed (cleaned) metrics to HA's main attributes
    api_payload=$(jq -n \
      --arg state "$status" \
      --arg ts "$(timestamp)" \
      --arg tar "$TARGET" \
      --argjson port "$PORT" \
      --arg friend "$friendly" \
      --argjson loss "$window_max_loss" \
      --arg worst "$worst_hop" \
      --argjson lat "$window_max_lat" \
      --argjson sd "$window_max_sd" \
      --argjson hops "$hops_json" \
      '{
        state: $state,
        attributes: {
          timestamp: $ts,
          target: $tar,
          port: $port,
          status: $state,
          friendly: $friend,
          window_loss: $loss,
          worst_hop: $worst,
          window_latency_avg: $lat,
          window_jitter_sd: $sd,
          hops: $hops
        }
      }')
    
    # 5. Push to Home Assistant
    curl -X POST \
         -H "Authorization: Bearer $TOKEN" \
         -H "Content-Type: application/json" \
         -d "$api_payload" \
         "$HA_URL/api/states/sensor.mtr_availability"
    

    ⚠️ Explanation:

    1. Target and Connection Settings

    These define where the script looks and how it connects.

    • TARGET: Set to redirector.googlevideo.com. This is the endpoint YouTube uses to balance traffic. Monitoring this gives you the best representation of “streaming health.”

    • PORT: Set to 443. This ensures the script uses HTTPS traffic (TCP), simulating an actual browser or app request.

    • CYCLES: The number of probes sent to each hop. A higher number (e.g., 10) gives more accurate averages but makes the script run longer; additionally, a higher number can trigger security rules on firewalls to start dropping your packets.

    • INTERVAL & TIMEOUT: Controls the speed of the probes. 1 second is standard for a balance between speed and reliability.


    2. The “Window” Logic (Sensitivity)

    This is the “brain” of your script that prevents false alarms.

    • CHECK_WINDOW: Set to 3. This tells the script to only evaluate the last three visible hops before the destination. This is your “Last Mile” strategy—ignoring local fluctuations to focus on the service’s entry point.

    • LOSS_THRESHOLD: Currently set twice (see note below). This is the percentage of packet loss that triggers a “Network Crisis” state.


    3. Thresholds (The Alarm Triggers)

    These variables determine when the Home Assistant sensor flips from OFF for “Stable” to ON for  “Crisis” or “High Jitter.”

    • LATENCY_THRESHOLD: Set to 150. If the average round-trip time exceeds 150ms, the script flags it.

    • LOSS_THRESHOLD: Set to 25. This is the final gate for the “On” status. 

    • SD_THRESHOLD: Set to 100. This monitors “Standard Deviation” (Jitter). High jitter means the connection is unstable, even if the speed is fast.


    4. Home Assistant Integration
    • TOKEN: Your Long-Lived Access Token. This is your “key” to talk to the HA API.

    • HA_URL: The address of your Home Assistant instance.

      • Example: http://192.168.1.100:8123 or https://yourdomain.ui:your_external_port.

    Home Assistant Configuration

    Generate Long-lived access token

    First of all, you need a Long-lived access token for your script:

    • Click to your HA username with foto in the bottom-left corner of your HA web-UI.
    • Switch to “Security” tab, scroll down, and click “Create tocken”.
    • Name your new token and copy it to the TOKEN variable in your script.

    Now, run your script, and check if a new entity is appeared in your HA:

    • Navigate to SettingsDeveloper Tools, and switch to the States tab.
    • Type “mtr” in “Filter entities” field, and check a new entity:
    MTR Entity

    ⚠️ Explanation:

    As you can see, the entity has two states ON if a connection issue is detected, and OFF if no issues are detected.

    It’s very useful to set up an automation for sending notifications. 

    HA Dashboard set up

    The added sensor isn’t convenient for use with standard dashboard cards.

    Hopefully, flex-table-card is available in HACS integration.

    So, install that card with flex and add it to your dashboard.

    That card can be configured with yaml editor only. Therefore, insert the following code:

    type: custom:flex-table-card
    title: Network Diagnostics
    entities:
      - sensor.mtr_availability
    columns:
      - name: Hop
        data: hops
        modify: x.hop
      - name: Host
        data: hops
        modify: x.host
      - name: Loss
        data: hops
        modify: x.loss_percent
        suffix: "%"
      - name: Latency
        data: hops
        modify: x.avg_ms
        suffix: " ms"
      - name: STDEV
        data: hops
        modify: x.stdev
        suffix: " ms"

    As a result, you should see a card like below, depending on your current style:

    Example of flex-table-card

    Telegram Notification

    Telegram notification

    Additionally, you can use the following yaml code for sending detailed notifications in Telegram:

    action: telegram_bot.send_message
    data:
      target: YOUR_BOT_ID
      title: ⚠️ Network Issue Detected
      parse_mode: html
      message: >
        <b>🌐 Network QoS Report</b>
    
        <pre>
    
        H|Host           |Loss %|Latency|StDev
    
        -|---------------|------|-------|-----
    
        {% for h in state_attr('sensor.mtr_availability', 'hops') -%}
    
        {{ "%1s|%-15.15s| %3d%% |%7.1f|%5.1f" | format(h.hop|string|last, h.host,
        h.loss_percent|int, h.avg_ms|float, h.stdev|float) }}
    
        {% endfor %}
    
        </pre>
    
        <b>Status:</b> {{ "🟢" if is_state('sensor.mtr_availability', 'off') else
        "🔴" }} {{ state_attr('sensor.mtr_availability', 'friendly') }}
    

    ⚠️ Explanation:

    Don’t forget to replace YOUR_BOT_ID with your actual Telegram bot ID.

    Running Script by Scchedule

    The last step is configuring cron to run the script by schedule.

    Let’s create a cron file and set up the right permissions:

    sudo touch /etc/cron.d/mtr_monitor 
    sudo chmod 0644 /etc/cron.d/mtr_monitor

    Open it in your preferred text editor e.g. mcedit, and add the following config:

    # Example of job definition:
    # .---------------- minute (0 - 59)
    # |  .------------- hour (0 - 23)
    # |  |  .---------- day of month (1 - 31)
    # |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
    # |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
    # |  |  |  |  |
    # *  *  *  *  * user-name command to be executed
    
    */5 * * * * root /opt/host_availability.sh
     

    ⚠️ Tips:

    • Adjust scheduling according to your preferences. 
    • The last EMPTY row is MANDATORY, otherwise cron daemon won’t apply your config.

    Restart cron daemon to apply the config:

    sudo systemctl restart cron

    Now you have your Internet health monitoring sensor working!!!