Control download clients based on internet usage with OpenWrt (maybe others)

I’ve use download scheduling in the past to control NzbGet, which had drawbacks when I’m not using the internet, I would like to download at full speed. Also, If my schedule starts at 11:00PM/23:00 and I happen to be staying up late, the schedule ramps up full speed and I have to manually intervene.

So I wrote a simple script that can be called with the Openwrt custom commands luci addon which polls internet facing interfacing interfaces with the scrape integration to get the bandwidth in bps. I can then use this to detect when my internet download is greater than 100,000bps (or 100Kbps) and trigger an automation to pause/slowdown/resume/speedup nzbget (or possibly any other client with a homeassistant integration)

I have 3 “wan” interfaces I track, replace the native linux interface wgX with your interfaces, or only use one if you just have one internet connection

Shell script

cat measureif.sh 
#!/bin/ash

# Function to extract bytes from the specified interface
get_bytes() {
  local interface="$1"
  local dir="/sys/class/net/$interface/statistics"
  local x="$2"

  local received_bytes1=$(cat "$dir/rx_bytes" 2>/dev/null || echo 0)
  local sent_bytes1=$(cat "$dir/tx_bytes" 2>/dev/null || echo 0)
  sleep "$x"
  local received_bytes2=$(cat "$dir/rx_bytes" 2>/dev/null || echo 0)
  local sent_bytes2=$(cat "$dir/tx_bytes" 2>/dev/null || echo 0)

  local received_bytes_per_second=$(( (received_bytes2 - received_bytes1) / x ))
  local sent_bytes_per_second=$(( (sent_bytes2 - sent_bytes1) / x ))
  
  echo "$interface input: $received_bytes_per_second"
  echo "$interface output: $sent_bytes_per_second"
} > "/tmp/$interface"

# Start tests in parallel for each WireGuard interface
for interface_duration in "wg0 $1" "wg1 $1" "wg2 $1"; do
  interface=$(echo "$interface_duration" | cut -d' ' -f1)
  duration=$(echo "$interface_duration" | cut -d' ' -f2)

  # Start the get_bytes function in the background
  get_bytes "$interface" "$duration" &
done

# Wait for all background processes to complete
wait

# Display the results
for interface in wg0 wg1 wg2; do
  cat "/tmp/$interface"
  rm "/tmp/$interface"
done

scrape config. note “?args=5” configures the luci custom command to sample interface data over a 5 second period. configure this if you want more/less sample data but this increases the time the script will run before returning the response to scrape.

image

{{ value | regex_findall("wg0 input: (\d+)", ignorecase=True) | first |  int }}

Sensors config (with bonus internet connection

- platform: template
  sensors:
    wg0_utilization:
      friendly_name: "WG0 Utilization"
      value_template: "{{ states('sensor.wg0_average_input')|float > 100000 or states('sensor.wg0_average_output')|float > 100000 }}"
    wg1_utilization:
      friendly_name: "WG1 Utilization"
      value_template: "{{ states('sensor.wg1_average_input')|float > 100000 or states('sensor.wg1_average_output')|float > 100000 }}"
    wg2_utilization:
      friendly_name: "WG2 Utilization"
      value_template: "{{ states('sensor.wg2_average_input')|float > 100000 or states('sensor.wg2_average_output')|float > 100000 }}"

- platform: statistics
  name: wg0_average_input
  entity_id: sensor.wg0_input
  max_age:
    minutes: 5
  precision: 0
  state_characteristic: mean

- platform: statistics
  name: wg0_average_output
  entity_id: sensor.wg0_output
  max_age:
    minutes: 5
  precision: 0
  state_characteristic: mean

- platform: statistics
  name: wg1_average_input
  entity_id: sensor.wg1_input
  max_age:
    minutes: 5
  precision: 0
  state_characteristic: mean

- platform: statistics
  name: wg1_average_output
  entity_id: sensor.wg1_output
  max_age:
    minutes: 5
  precision: 0
  state_characteristic: mean

- platform: statistics
  name: wg2_average_input
  entity_id: sensor.wg2_input
  max_age:
    minutes: 5
  precision: 0
  state_characteristic: mean

- platform: statistics
  name: wg2_average_output
  entity_id: sensor.wg2_output
  max_age:
    minutes: 5
  precision: 0
  state_characteristic: mean

luci custom command config

I have two automations in nodered I won’t go into the specifics, but one triggers every 1 minute to check if the “sensor.wg0_input” is >100,000. and the other triggers every 10 minutes to check if the sensor.wg0_utilization is false.
This results in nzbget being throttled down to 1MB/s when there’s anymore than background traffic within 1 minute, and then resumes fullspeed if the 5 minute average usage is less than 100,000bps, every 10 minutes.

Note, my setup is unique in that my general internet traffic goes out wireguard tunnels, and nzbget downloads directly over the internet, so this is why I’m able to identify traffic is general vs nzbget. If you have a more typical network with LAN/WAN, you could configure the script to poll multiple interfaces excluding where your download client is connected to, and then craft a sensor to add them together and automate off that.