Virgin Media UK STM Monitoring Script


tievolu

Forum Guru
Member
Virgin Media UK cable internet connections are subject to a traffic management policy (commonly referred to as STM) that cuts your bandwidth by 50-75% if you download/upload a certain amount of data between certain hours of the day, as described here. When this happens, your QOS settings become useless and your connection usually slows to a crawl.

Over the last week or so I've been working on a script to monitor bandwidth usage (via rstats) and automatically reconfigure QOS if I go over my STM limit. It seems to work ok, so I thought I'd share it to see if anyone else was interested.

I've set up a small web page here describing it and providing instructions.

The script itself is available here.

You can enable the script on your router by adding the following to your "WAN Up" script and rebooting:

Code:
wget -O /tmp/stm-monitor.sh http://www.tievolu.co.uk/stm/stm-monitor.sh
chmod 755 /tmp/stm-monitor.sh
cru a STM-Monitor "1-59/2 * * * * /tmp/stm-monitor.sh [b][broadband type][/b]"
logger -t STM-Monitor "Downloaded `head /tmp/stm-monitor.sh | grep \"STM Monitor\" | sed 's/# //g'`"

Where [broadband type] = M, M+, L or XL

Here's the first version of the script, just so you can see what's involved:

Code:
#!/bin/sh -x
#
# Tomato STM Monitor v1.00
# Written by tievolu
# http://tievolu.googlepages.com/stm-monitor.html
#
# This script is for use on Virgin Media UK cable internet connections,
# which implement Subscriber Traffic Management (STM) i.e. the WAN
# bandwidth is limited for a set time when an predefined amount of data
# is transferred between certain hours of the day.
#
# It allows the WAN bandwidth usage to be automatically monitored and
# suitable QOS settings applied if an STM limit is exceeded. When the
# STM sentence has been served the QOS settings return to normal.
#
# Add the following to your "WAN Up" script to download the script and
# set it to run every two minutes:
#
#    wget -O /tmp/stm-monitor.sh http://tievolu.googlepages.com/stm-monitor.sh
#    chmod 755 /tmp/stm-monitor.sh
#    cru a STM-Monitor "1-59/2 * * * * /tmp/stm-monitor.sh [broadband type]"
#    logger -t STM-Monitor "Downloaded `head /tmp/stm-monitor.sh | grep \"STM Monitor\" | sed 's/# //g'`"
#
#    Where [broadband type] = "M", "L" or "XL" (no quotes)
#
# Events are recorded in the system log, and details are displayed in a
# small web page located at:
#
#    http://[router IP address]/ext/stm-monitor.htm
#
# The script assumes that the time on your router is accurate to within
# a minute or so, so make sure your NTP settings are correct! It also only
# works correcly if your router is left on throughout each STM period,
# because rebooting destroys the Tomato bandwidth stats this script relies on.
# Note that the actions of this script (i.e. setting QOS to deal with an STM
# sentence) use NVRAM variables so they *will* survive a reboot (but not a
# power cycle, because the NVRAM variables are not committed to flash).

# Logging command
LOGGER="logger -t STM-Monitor"

# STM Monitor version
stm_monitor_version=`head /tmp/stm-monitor.sh | grep "STM Monitor" | sed 's/# //g'`

# Check command line argument
if [[ "$#" -ne 1 && "$1" != "M" && "$1" != "L" && "$1" != "XL" ]]
then
	$LOGGER "Usage: stm-monitor.sh [M|L|XL]"
	exit
fi 

# The upload QOS bandwidth values are set at ~91% of the cable modem
# bandwidth limiter (download set at 100%), which works well for my
# connection. Cable connections tend to be pretty stable so they should work
# ok for most users. The STM related variables are based on the information
# published by VM here: http://allyours.virginmedia.com/html/internet/traffic.html

# STM period definitions (time values are hours of the day e.g. 16 = 16:00 = 4pm)
stm_period_name_1="Daytime Downstream"
stm_period_start_1=10
stm_period_end_1=15
stm_period_direction_1="RX"

stm_period_name_2="Evening Downstream"
stm_period_start_2=16
stm_period_end_2=21
stm_period_direction_2="RX"

stm_period_name_3="Evening Upstream"
stm_period_start_3=15
stm_period_end_3=20
stm_period_direction_3="TX"

# STM sentence length (i.e. how long STM lasts for once it's triggered)
stm_sentence_length_hours=5
stm_sentence_length_seconds=`expr $stm_sentence_length_hours \* 3600`

# Settings specific to XL (20Mb down, 768Kb up)
if [[ $1 = "XL" ]] 
then
	# STM limits (values in MB)
	stm_period_bw_limit_mb_1=6000
	stm_period_bw_limit_mb_2=3000
	stm_period_bw_limit_mb_3=1400
	# QOS settings (values in Kbits/s)
	inbound_bandwidth_without_stm=20480
	inbound_bandwidth_with_stm=5120
	outbound_bandwidth_without_stm=700
	outbound_bandwidth_with_stm=174
fi

# Settings specific to L (10Mb down, 512Kb up)
if [[ $1 = "L" ]]
then
	# STM limits (values in MB)
	stm_period_bw_limit_mb_1=2400
	stm_period_bw_limit_mb_2=1200
	stm_period_bw_limit_mb_3=700
	# QOS settings (values in Kbits/s)
	inbound_bandwidth_without_stm=10240
	inbound_bandwidth_with_stm=2560
	outbound_bandwidth_without_stm=465
	outbound_bandwidth_with_stm=116
fi

# Settings specific to M (2Mb down, 256Kb up)
if [[ $1 = "M" ]]
then
	# STM limits (values in MB)
	stm_period_bw_limit_mb_1=1000
	stm_period_bw_limit_mb_2=500
	stm_period_bw_limit_mb_3=200
	# QOS settings (values in Kbits/s)
	inbound_bandwidth_without_stm=2048
	inbound_bandwidth_with_stm=1024
	outbound_bandwidth_without_stm=233
	outbound_bandwidth_with_stm=116
fi

# Set up web page. Use a string to store web page content and pipe
# it to /var/wwwext/stm-monitor.htm when the script exits
if [[ ! -e "/var/wwwext" ]]
then
	mkdir /var/wwwext
fi
web_page=""

# Grab the current time
current_hours=`date +%H`
current_minutes=`date +%M`
current_unix_time=`date +%s`

# Check whether we are still serving an STM sentence that has completed
if [[ "`nvram get stm_sentence_active`" = 1  && $current_unix_time -gt "`nvram get stm_sentence_end_unix_time`" ]]
then
	# Return QOS configuration to non-STM settings
	$LOGGER "STM sentence served: applying normal QOS configuration"
	nvram set qos_obw=$outbound_bandwidth_without_stm
	nvram set qos_ibw=$inbound_bandwidth_without_stm

	# Restart QOS
	service qos restart

	# Unset our NVRAM variables
	nvram unset stm_sentence_active
	nvram unset stm_sentence_end_time
	nvram unset stm_sentence_end_unix_time
fi   

# Grab current statistics from rstats and wait for the file to be written
killall -USR1 rstats
sleep 2
stats_file=/var/spool/rstats-speed.js

# Read the stats file and extract the parts we're interested in (vlan1 RX and TX)
read_next_rx_tx=0
while read line
do
	case $line in
		*vlan1*) read_next_rx_tx=1 ;;
	esac

	if [[ $read_next_rx_tx = 1 ]]
	then
		case $line in
			*rx:*) rx_data=$line ;;
			*tx:*) tx_data=$line ; read_next_rx_tx=0 ;;
		esac
	fi
done < $stats_file
rm $stats_file

# Check which STM periods are active, check total TX/RX against
# STM limits and note if an STM limit was exceeded
stm_sentence_triggered=0
web_page="${web_page}<table cellpadding=9 cellspacing=3 border=0>"
web_page="${web_page}<tr align=\"center\" bgcolor=\"DDDDFF\"><th>STM Description<th>Start</th><th>End</th><th>Bandwidth Limit</th><th>Bandwidth Used</th><th>Average Rate</th></tr>"
for i in 1 2 3; do
	stm_period_name=`eval echo "\\$stm_period_name_$i"`
	stm_period_start=`eval echo "\\$stm_period_start_$i"`
	stm_period_end=`eval echo "\\$stm_period_end_$i"`
	stm_period_direction=`eval echo "\\$stm_period_direction_$i"`
	stm_period_bw_limit_mb=`eval echo "\\$stm_period_bw_limit_mb_$i"`
	
	if [[ $current_hours -ge $stm_period_start && $current_hours -lt $stm_period_end ]]
	then
		web_page="${web_page}<tr bgcolor=\"EEEEEE\" align=\"center\">"
		web_page="${web_page}<td align=\"left\">${stm_period_name} (${1})</td><td>${stm_period_start}:00</td><td>${stm_period_end}:00</td><td>${stm_period_bw_limit_mb} MB</td>"

		# Calculate the number of values to take from the rstats data (each value covers two minutes)
		minutes_since_stm_period_started=`echo "" | awk '{ print((((hours - stm_start) * 60) + minutes)); }' hours=$current_hours minutes=$current_minutes stm_start=${stm_period_start}`

		number_of_values=`expr $minutes_since_stm_period_started / 2`
		
		if [[ $stm_period_direction = "RX" ]]
		then
			data=$rx_data
		else
			data=$tx_data
		fi
		
		if [[ $number_of_values != 0 ]]
		then
			# Sum the last XXX values of the 720 in the rstats data to get the
			# total bytes transferred since the STM period started 
			total_transferred_bytes=`echo $data | awk '
				{
					values_string = substr($0, index($0, "[") + 1, index($0, "]") - 6);
					split(values_string, values_array, ",");
					for (i=(721-number_of_values); i<=720; i++) {
						total += values_array[i];
					} printf("%0.f" total);
				}' number_of_values=$number_of_values total=0`
			total_transferred_mb=`echo "" | awk '{ printf("%.2f", bytes / 1048576) }' bytes=$total_transferred_bytes`	
			average_rate=`echo "" | awk '{ printf("%.2f", bytes / (number_of_values * 2 * 60 * 1024)) }' number_of_values=$number_of_values bytes=$total_transferred_bytes`
		else
			total_transferred_bytes="0"
			total_transferred_mb="0.00"
		    everage_rate="0.00"
		fi	
	
		# Calculate maximum average rate (i.e. rate required to trigger STM),
		# then compare this to our actual rate. If the actual rate is equal or
		# higher, we paint the cell red, if it's greater that 75% of the max
		# we paint it amber, and if it's anything else we paint it green.
		stm_period_max_ave_rate=`echo "" | awk '{printf((limit * 1024) / ((end - start)*3600))}' limit=$stm_period_bw_limit_mb start=$stm_period_start end=$stm_period_end`
		cell_color=`echo | awk '{ if (rate >= max) {print("FF6633")} else if (rate > (0.75 * max)) {print("FFCC33")} else {print("66FF33")}}' rate=$average_rate max=$stm_period_max_ave_rate`
	
		# Add data to web page
		web_page="${web_page}<td>$total_transferred_mb MB</td><td bgcolor=\"${cell_color}\">$average_rate KB/s</td>"
	
		# Check whether STM limit has been exceeded
		stm_period_bw_limit_bytes=`expr $stm_period_bw_limit_mb \* 1048576`
		result=`echo "" | awk '{ if (total > limit) {print(1)} else {print(0)}}' total=$total_transferred_bytes limit=$stm_period_bw_limit_bytes`
		if [[ $result = 1 && "`nvram get stm_sentence_active`" != 1 ]]
		then
			stm_sentence_triggered=1
			nvram set stm_sentence_active=1
			$LOGGER "STM Triggered: ${stm_period_name} (${stm_period_start}:00 -> ${stm_period_end}:00, ${stm_period_direction}, ${stm_period_bw_limit_mb} MB)"
		fi
	else
		web_page="${web_page}<tr bgcolor=\"EEEEEE\" style=\"color:AAAAAA\" align=\"center\">"
		web_page="${web_page}<td align=\"left\">${stm_period_name} (${1})</td><td>${stm_period_start}:00</td><td>${stm_period_end}:00</td><td>${stm_period_bw_limit_mb} MB</td><td>N/A</td><td>N/A</td>"	
	fi
	web_page="${web_page}</tr>"
done
web_page="${web_page}</table>"

# If STM is active, check whether QOS is set accordingly
if [[ "`nvram get stm_sentence_active`" = 1  && "`nvram get qos_obw`" != $outbound_bandwidth_with_stm ]]
then
	if [[ $stm_sentence_triggered = 1 ]]
	then
		# We must have triggered an STM sentence on this run.
		# Work out the hour component of the time the STM sentence will end
		# (for logging purposes only - we will use unix time to track the
		# STM sentence accurately)
		stm_end_hour=`expr $current_hours + $stm_sentence_length_hours`
		if [[ $stm_end_hour -gt 23 ]]
		then
			stm_end_hour="0`expr $stm_end_hour - 24`"
		fi
		
		# Set NVRAM variables to describe the STM sentence being served
		nvram set stm_sentence_end_time="$stm_end_hour:$current_minutes"
		nvram set stm_sentence_end_unix_time=`expr $current_unix_time + $stm_sentence_length_seconds`

		# Send information to log
		$LOGGER "STM sentence started: applying STM QOS configuration (until $stm_end_hour:$current_minutes)"
	else
		# An STM sentence is active and we didn't trigger it on this run,
		# but the QOS settings are wrong. We must have rebooted, or someone
		# messed with the QOS settings. So we want to set the QOS settings
		# to deal with the STM, but we don't want to overwrite our NVRAM
		# variables.
		# Send information to log
		$LOGGER "STM sentence resumed: applying STM QOS configuration (until `nvram get stm_sentence_end_time`)" 
	fi
	
	# Set QOS NVRAM variables
	nvram set qos_obw=$outbound_bandwidth_with_stm
	nvram set qos_ibw=$inbound_bandwidth_with_stm

	# Restart QOS
	service qos restart
fi

# Add QOS settings to web page
web_page="${web_page}<p>Current QOS settings - <b>`nvram get qos_ibw`</b> kbits/s down, <b>`nvram get qos_obw`</b> kbits/s up</p>"

# Add latest log entries to web page
web_page="${web_page}<h3>Latest Log Entries</h3>"
log=""
while read line
do
	case $line in
		*STM-Monitor*) log="${log}<nobr><tt>${line}</tt></nobr><br>" ;;
	esac
done < /tmp/var/log/messages
web_page="${web_page}$log"

# Publish web page
# Write the title and status now, so that they accurately relfect the data in the table
web_page_header="<html><head><title>${stm_monitor_version}</title><meta http-equiv=\"refresh\" content=\"120\"</head>"
web_page_header="${web_page_header}<body style=\"font-family:verdana\"><h2>${stm_monitor_version}</h2>"
web_page_header="${web_page_header}<h3>Status at ${current_hours}:${current_minutes} - "
if [[ "`nvram get stm_sentence_active`" = 1 ]]
then
	web_page_header="${web_page_header}<b style=\"color:red\">STM has been triggered - the sentence will end at `nvram get stm_sentence_end_time`</b></h3>"
else
	web_page_header="${web_page_header}<b style=\"color:green\">STM has not been triggered</b></h3>"
fi 
web_page="${web_page_header}${web_page}</body></html>"
echo $web_page > /var/wwwext/stm-monitor.htm

Even if you're not interested in this particular functionality, there may well be stuff in the script that you find useful for other scripts. I learned quite a lot while writing it.

Any feedback or suggestions most welcome.
 
Tiev, thanks for the script, but I couldn't get it to work. Could you please verify I have the following correct for a VM L service

wget -O /tmp/stm-monitor.sh http://tievolu.googlepages.com/stm-monitor.sh
chmod 755 /tmp/stm-monitor.sh
cru a STM-Monitor "1-59/2 * * * * /tmp/stm-monitor.sh L"
logger -t STM-Monitor "Downloaded `head stm-monitor.sh | grep \"STM Monitor\" | sed 's/# //g'`"

I couldn't call the web page either
http://192.168.1.1/ext/stm-monitor.sh

I have 'logging' enabled. I could see your script was downloaded when i looked the the log

thx
 
Tiev, thanks for the script, but I couldn't get it to work. Could you please verify I have the following correct for a VM L service

wget -O /tmp/stm-monitor.sh http://tievolu.googlepages.com/stm-monitor.sh
chmod 755 /tmp/stm-monitor.sh
cru a STM-Monitor "1-59/2 * * * * /tmp/stm-monitor.sh L"
logger -t STM-Monitor "Downloaded `head stm-monitor.sh | grep \"STM Monitor\" | sed 's/# //g'`"

I couldn't call the web page either
http://192.168.1.1/ext/stm-monitor.sh

I have 'logging' enabled. I could see your script was downloaded when i looked the the log

thx

Can you log into the router and run "ls -l /tmp" and paste the output?

If the script is there, try running it directly and tell me what happens i.e. run:

/tmp/stm-monitor.sh L

EDIT: The web page link will be http://192.168.1.1/ext/stm-monitor.htm (not .sh) That's probably your only problem - the WAN Up code looks fine (that was my fault btw - I've just corrected the web page - d'oh!)
 
tievolu, I managed to get this working and this script is fantastic, also great web interface. If I read correctly does the STM 5 hour period start from when you overstep the limits?

Is it possible to script this so that i include the whole file in my wan-up code? (i.e. it doesn't go off to load a file from a server). For my security i'd like to know what is within a file everytime my router boots.
Great work!!
Thanks
 
Glad you like it :)

While I promise not to do anything dodgy with the script, the possibility that I could if I was that way inclined is definitely there, so I completely understand your concerns about downloading it automatically.

You could build the script entirely within the WAN Up script, but it would be a bit ugly - lots of echo commands, with the need to escape all the special characters in the correct way. It would be much easier to mount a JFFS partition and store a copy of the script there permanently.

So, assuming your router has enough flash, and you like the script the way it is right now, create and mount your JFFS partition using the Tomato GUI (Administration -> JFFS2) and then issue the following commands (i.e. just telnet in and run them - don't add them to your WAN Up script):

Code:
wget -O /jffs/stm-monitor.sh http://tievolu.googlepages.com/stm-monitor.sh
chmod 755 /jffs/stm-monitor.sh

Then you would just need the following command in your WAN Up or startup script to set up the cron job when the router boots:

Code:
cru a STM-Monitor "1-59/2 * * * * /jffs/stm-monitor.sh [M|L|XL]"

Hope this helps.
 
If I read correctly does the STM 5 hour period start from when you overstep the limits?
Just realised I didn't answer this before...

Yes, the 5 hour punishment period begins when you go over the STM threshold.

So, if you went over the limit for the 16:00 -> 21:00 period at, say, 18:30, you'd have your bandwidth reduced for five hours from that point i.e. until 23:30.

Some would argue that it's a bit harsh, but that's the way it works.
 
This script is excellent. Thank you tievolu.

Although I do not really use QOS I have found the STM monitor page extremely useful. I live in a student house with 5 other people so we frequently go over the limit, usually within the last hour of the evening downstream period :mad:

A suggestion I would like to make would be to have the QOS settings become stricter according to the amount of time and bandwidth left to prevent the limit being reached in the last couple of hours. I will expand upon this further if you need me to, but for know I am struggling to turn my thoughts into words :)

Tanks again
 
A suggestion I would like to make would be to have the QOS settings become stricter according to the amount of time and bandwidth left to prevent the limit being reached in the last couple of hours. I will expand upon this further if you need me to, but for know I am struggling to turn my thoughts into words :)

I've thought about this too - it would be nice to have the option of preventing STM from kicking in, rather than merely coping with it when it does.

I don't think it would be too hard to implement. I'll have a look into it...
 
Ok, here we go. There are now two modes of operation:

STM Mitigation - This is the default mode. If an STM limit is exceeded, QOS limits are applied in accordance with the bandwidth limits imposed by STM.

STM Prevention - If bandwidth usage is on target to exceed an STM limit, QOS limits are applied to prevent that from happening. Activated by the "-p" option (see below).

Usage is as follows:

Code:
stm-monitor.sh [broadband type] [-p]

With STM prevention enabled, the script monitors the average rate, checking whether it has exceeded the maximum rate (i.e. when the cell is red and you're on course to trigger STM). If this happens, the script calculates how much bandwidth you have left in this STM period, and limits the inbound or outbound QOS such that you will only be able to transfer 90% of that bandwidth before the STM period ends. The QOS limit returns to normal if the average rate drops below 90% of the maximum, or when the STM period ends.

The end result of STM prevention is that you may take a more severe bandwidth cut than if you triggered STM, but that cut will usually last for a much shorter period of time. Personally, I'm not really sure whether prevention is better than mitigation, but YMMV.

I've also modified the status page slightly. See this page for an example.

Code:
#!/bin/sh -x
#
# Tomato STM Monitor v1.01
# Written by tievolu
# http://tievolu.googlepages.com/stm-monitor.html
#
# This script is for use on Virgin Media UK cable internet connections,
# which implement Subscriber Traffic Management (STM) i.e. the WAN
# bandwidth is limited for a set time when an predefined amount of data
# is transferred between certain hours of the day.
#
# It allows the WAN bandwidth usage to be automatically monitored and
# suitable QOS settings applied. There are two modes of operation:
#
# 1. STM Mitigation
#
#    This is the default mode. If an STM limit is exceeded, QOS limits
#    are applied in accordance with the bandwidth limits imposed by STM.
#
# 2. STM Prevention
#
#    If bandwidth usage is on target to exceed an STM limit, QOS limits
#    are applied to prevent that from happening. Activated by the "-p"
#    option (see below). 
#
# Add the following to your "WAN Up" script to download the script and
# set it to run every two minutes:
#
#    wget -O /tmp/stm-monitor.sh http://tievolu.googlepages.com/stm-monitor.sh
#    chmod 755 /tmp/stm-monitor.sh
#    cru a STM-Monitor "1-59/2 * * * * /tmp/stm-monitor.sh [broadband type] [-p]"
#    logger -t STM-Monitor "Downloaded `head /tmp/stm-monitor.sh | grep \"STM Monitor\" | sed 's/# //g'`"
#
#    Where [broadband type] = "M", "L" or "XL" (no quotes)
#                      [-p] = Activates STM prevention
#
# Events are recorded in the system log, and details are displayed in a
# small web page located at:
#
#    http://[router IP address]/ext/stm-monitor.htm
#
# The script assumes that the time on your router is accurate to within
# a minute or so, so make sure your NTP settings are correct! It also only
# works correcly if your router is left on throughout each STM period,
# because rebooting destroys the Tomato bandwidth stats this script relies on.

# Logging command
LOGGER="logger -t STM-Monitor"

# STM Monitor version
stm_monitor_version=`head /tmp/stm-monitor.sh | grep "STM Monitor" | sed 's/# //g'`

# Check command line arguments
if [[ "$#" -eq 1 && "$1" != "M" && "$1" != "L" && "$1" != "XL" ]]
then
	$LOGGER "Usage: stm-monitor.sh [M|L|XL] [-p]"
	exit
fi
if [[ "$#" -eq 2 && "$2" != "-p" ]]
then
	$LOGGER "Usage: stm-monitor.sh [M|L|XL] [-p]"
	exit
fi

# For testing purposes
if [[ "$2" = "-p" ]]
then
	preventative_mode=1
fi 

# The upload QOS bandwidth values are set at ~91% of the cable modem
# bandwidth limiter (download set at 100%), which works well for my
# connection. Cable connections tend to be pretty stable so they should work
# ok for most users. The STM related variables are based on the information
# published by VM here: http://allyours.virginmedia.com/html/internet/traffic.html

# STM period definitions (time values are hours of the day e.g. 16 = 16:00 = 4pm)
stm_period_name_1="Daytime Downstream"
stm_period_start_1=10
stm_period_end_1=15
stm_period_direction_1="RX"

stm_period_name_2="Evening Downstream"
stm_period_start_2=16
stm_period_end_2=21
stm_period_direction_2="RX"

stm_period_name_3="Evening Upstream"
stm_period_start_3=15
stm_period_end_3=20
stm_period_direction_3="TX"

# STM sentence length (i.e. how long STM lasts for once it's triggered)
stm_sentence_length_hours=5
stm_sentence_length_seconds=`expr $stm_sentence_length_hours \* 3600`

# Settings specific to XL (20Mb down, 768Kb up)
if [[ $1 = "XL" ]] 
then
	# STM limits (values in MB)
	stm_period_bw_limit_mb_1=6000
	stm_period_bw_limit_mb_2=3000
	stm_period_bw_limit_mb_3=1400
	# QOS settings (values in Kbits/s)
	inbound_bandwidth_without_stm=20480
	inbound_bandwidth_with_stm=5120
	outbound_bandwidth_without_stm=700
	outbound_bandwidth_with_stm=174
fi

# Settings specific to L (10Mb down, 512Kb up)
if [[ $1 = "L" ]]
then
	# STM limits (values in MB)
	stm_period_bw_limit_mb_1=2400
	stm_period_bw_limit_mb_2=1200
	stm_period_bw_limit_mb_3=700
	# QOS settings (values in Kbits/s)
	inbound_bandwidth_without_stm=10240
	inbound_bandwidth_with_stm=2560
	outbound_bandwidth_without_stm=465
	outbound_bandwidth_with_stm=116
fi

# Settings specific to M (2Mb down, 256Kb up)
if [[ $1 = "M" ]]
then
	# STM limits (values in MB)
	stm_period_bw_limit_mb_1=1000
	stm_period_bw_limit_mb_2=500
	stm_period_bw_limit_mb_3=200
	# QOS settings (values in Kbits/s)
	inbound_bandwidth_without_stm=2048
	inbound_bandwidth_with_stm=1024
	outbound_bandwidth_without_stm=233
	outbound_bandwidth_with_stm=116
fi

# Grab the current time
current_hours=`date +%H`
current_minutes=`date +%M`
current_unix_time=`date +%s`

# Set up web page. Use a string to store web page content and pipe
# it to /var/wwwext/stm-monitor.htm when the script exits
if [[ ! -e "/var/wwwext" ]]
then
	mkdir /var/wwwext
fi
web_page="<html><head><title>${stm_monitor_version} - Status at ${current_hours}:${current_minutes}</title><meta http-equiv=\"refresh\" content=\"120\"</head>"
web_page="${web_page}<body style=\"font-family:verdana\"><h2>${stm_monitor_version}</h2>"
web_page="${web_page}<h3>Status at ${current_hours}:${current_minutes}</h3>"

# Check whether we are still serving an STM sentence that has completed
if [[ "`nvram get stm_sentence_active`" = 1  && $current_unix_time -gt "`nvram get stm_sentence_end_unix_time`" ]]
then
	# Return QOS configuration to non-STM settings
	$LOGGER "STM sentence served: applying normal QOS configuration"
	nvram set qos_obw=$outbound_bandwidth_without_stm
	nvram set qos_ibw=$inbound_bandwidth_without_stm

	# Restart QOS
	service qos restart

	# Unset our NVRAM variables
	nvram unset stm_sentence_active
	nvram unset stm_sentence_end_time
	nvram unset stm_sentence_end_unix_time
fi   

# Grab current statistics from rstats and wait for the file to be written
killall -USR1 rstats
sleep 2
stats_file=/var/spool/rstats-speed.js

# Read the stats file and extract the parts we're interested in (vlan1 RX and TX)
read_next_rx_tx=0
while read line
do
	case $line in
		*vlan1*) read_next_rx_tx=1 ;;
	esac

	if [[ $read_next_rx_tx = 1 ]]
	then
		case $line in
			*rx:*) rx_data=$line ;;
			*tx:*) tx_data=$line ; read_next_rx_tx=0 ;;
		esac
	fi
done < $stats_file
rm $stats_file

# Check which STM periods are active, check total TX/RX against
# STM limits and note if an STM limit was exceeded
stm_sentence_triggered=0
web_page="${web_page}<table cellpadding=9 cellspacing=3 border=0>"
web_page="${web_page}<tr align=\"center\" bgcolor=\"DDDDFF\"><th>STM Description<th>Start</th><th>End</th><th>Bandwidth Limit</th><th>Bandwidth Used</th><th>Average Rate</th></tr>"
for i in 1 2 3; do
	stm_period_name=`eval echo "\\$stm_period_name_$i"`
	stm_period_start=`eval echo "\\$stm_period_start_$i"`
	stm_period_end=`eval echo "\\$stm_period_end_$i"`
	stm_period_direction=`eval echo "\\$stm_period_direction_$i"`
	stm_period_bw_limit_mb=`eval echo "\\$stm_period_bw_limit_mb_$i"`
	
	if [[ $current_hours -ge $stm_period_start && $current_hours -lt $stm_period_end ]]
	then
		# This STM period is active.	
		web_page="${web_page}<tr bgcolor=\"EEEEEE\" align=\"center\">"
		web_page="${web_page}<td align=\"left\">${stm_period_name} (${1})</td><td>${stm_period_start}:00</td><td>${stm_period_end}:00</td><td>${stm_period_bw_limit_mb} MB</td>"

		# Calculate the number of values to take from the rstats data (each value covers two minutes)
		minutes_since_stm_period_started=`echo "" | awk '{ print((((hours - stm_start) * 60) + minutes)); }' hours=$current_hours minutes=$current_minutes stm_start=${stm_period_start}`
		number_of_values=`expr $minutes_since_stm_period_started / 2`
		
		if [[ $stm_period_direction = "RX" ]]
		then
			data=$rx_data
			stm_period_rx_active=1
		else
			data=$tx_data
			stm_period_tx_active=1
		fi
		
		if [[ $number_of_values != 0 ]]
		then
			# Sum the last XXX values of the 720 in the rstats data to get the
			# total bytes transferred since the STM period started 
			total_transferred_bytes=`echo $data | awk '
				{
					values_string = substr($0, index($0, "[") + 1, index($0, "]") - 6);
					split(values_string, values_array, ",");
					for (i=(721-number_of_values); i<=720; i++) {
						total += values_array[i];
					} printf("%0.f" total);
				}' number_of_values=$number_of_values total=0`
			total_transferred_mb=`echo "" | awk '{ printf("%.2f", bytes / 1048576) }' bytes=$total_transferred_bytes`	
			average_rate=`echo "" | awk '{ printf("%.2f", bytes / (number_of_values * 2 * 60 * 1024)) }' number_of_values=$number_of_values bytes=$total_transferred_bytes`
			average_rate_kilobits=`echo "" | awk '{printf("%.2f", rate_kilobytes * 8)}' rate_kilobytes=$average_rate`
		else
			total_transferred_bytes="0"
			total_transferred_mb="0.00"
		    average_rate="0.00"
			average_rate_kilobits="0.00"
		fi	
	
		# Calculate maximum average rate (i.e. rate required to trigger STM),
		# then compare this to our actual rate. If the actual rate is equal or
		# higher, we paint the cell red, if it's greater than 90% of the max
		# we paint it amber, and if it's anything else we paint it green.
		stm_period_max_ave_rate=`echo "" | awk '{printf((limit * 1024) / ((end - start)*3600))}' limit=$stm_period_bw_limit_mb start=$stm_period_start end=$stm_period_end`
		cell_color=`echo | awk '{ if (rate >= max) {print("FF6633")} else if (rate > (0.9 * max)) {print("FFCC33")} else {print("66FF33")}}' rate=$average_rate max=$stm_period_max_ave_rate`
		
		# STM Prevention code
		if [[ $preventative_mode = 1 ]]
		then
			if [[ $cell_color = "FF6633" ]]
			then
				# Cell is red => calculate preventative QOS limit
				# Calculate the number of kilobits we have left before we trigger STM
				total_kilobits_remaining=`echo "" | awk '{print(((limit*1048576)-total)/128)}' limit=$stm_period_bw_limit_mb total=$total_transferred_bytes`
							
				# Calculate time remaining until end of STM period
				seconds_remaining=`echo "" | awk '{print(((end_hour - current_hour - 1) * 3600) + (3600 - (60 * current_minute)))}' end_hour=$stm_period_end current_hour=$current_hours current_minute=$current_minutes`
			
				# Calculate a QOS limit such that we'll download a maxmum of 90% of this in the remaining time
				preventative_qos_limit=`echo "" | awk '{printf("%.0f", 0.9*(kilobits/secs))}' kilobits=$total_kilobits_remaining secs=$seconds_remaining`
				
				if [[ $stm_period_direction = "RX" && "`nvram get qos_ibw`" = $inbound_bandwidth_without_stm ]]
				then
					$LOGGER "Setting preventative inbound QOS limit of $preventative_qos_limit kbits/s"
					nvram set qos_ibw=$preventative_qos_limit
					# We must set the inbound rate limits to 100% or less for this to work ("None" will not work
					# because the rate will not be limited). To make sure this does work, we'll change the
					# settings to 100% across the board and restore the old settings when we remove the
					# temporary QOS limit.
					nvram set qos_irates_old=`nvram get qos_irates`
					nvram set qos_irates=100,100,100,100,100,100,100,100,100,100
					service qos restart	
				fi
				if [[ $stm_period_direction = "TX" && "`nvram get qos_obw`" = $outbound_bandwidth_without_stm ]]
				then
					$LOGGER "Setting preventative outbound QOS limit of $preventative_qos_limit kbits/s"
					nvram set qos_obw=$preventative_qos_limit
					service qos restart	
				fi		
			fi
		
			if [[ $cell_color = "66FF33" ]]
			then
				# Cell is green => apply normal QOS if necessary
				if [[ $stm_period_direction = "RX" && "`nvram get qos_ibw`" != $inbound_bandwidth_without_stm ]]
				then
					$LOGGER "Removing preventative inbound QOS limit"
					nvram set qos_ibw=$inbound_bandwidth_without_stm
					nvram set qos_irates=`nvram get qos_irates_old`
					nvram unset qos_irates_old
					service qos restart	
				fi
				if [[ $stm_period_direction = "TX" && "`nvram get qos_obw`" != $outbound_bandwidth_without_stm ]]
				then
					$LOGGER "Removing preventative outbound QOS limit"
					nvram set qos_obw=$outbound_bandwidth_without_stm
					service qos restart	
				fi			
			fi
		fi # End of STM prevention code
		
		# Add data to web page
		web_page="${web_page}<td>$total_transferred_mb MB</td><td bgcolor=\"${cell_color}\">$average_rate KB/s ($average_rate_kilobits kb/s)</td>"
	
		# Check whether STM limit has been exceeded
		stm_period_bw_limit_bytes=`expr $stm_period_bw_limit_mb \* 1048576`
		result=`echo "" | awk '{ if (total > limit) {print(1)} else {print(0)}}' total=$total_transferred_bytes limit=$stm_period_bw_limit_bytes`
		if [[ $result = 1 && "`nvram get stm_sentence_active`" != 1 ]]
		then
			stm_sentence_triggered=1
			nvram set stm_sentence_active=1
			$LOGGER "STM Triggered: ${stm_period_name} (${stm_period_start}:00 -> ${stm_period_end}:00, ${stm_period_direction}, ${stm_period_bw_limit_mb} MB)"
		fi
	else
		# This STM period is not currently active
		web_page="${web_page}<tr bgcolor=\"EEEEEE\" style=\"color:AAAAAA\" align=\"center\">"
		web_page="${web_page}<td align=\"left\">${stm_period_name} (${1})</td><td>${stm_period_start}:00</td><td>${stm_period_end}:00</td><td>${stm_period_bw_limit_mb} MB</td><td>N/A</td><td>N/A</td>"	
	fi
	web_page="${web_page}</tr>"
done
web_page="${web_page}</table>"

# Remove RX/TX STM Prevention QOS limits if necessary. This code makes sure the temporary limits
# are removed when they are not relevant (i.e. when we're not in an STM period and STM has not
# been triggered)
if [[ $preventative_mode = 1 ]]
then
	if [[ "`nvram get stm_sentence_active`" != 1 && "$stm_period_rx_active" != 1 && "`nvram get qos_ibw`" != $inbound_bandwidth_without_stm ]]
	then
		$LOGGER "Removing preventative inbound QOS limit"
		nvram set qos_ibw=$inbound_bandwidth_without_stm
		nvram set qos_irates=`nvram get qos_irates_old`
		nvram unset qos_irates_old
		service qos restart	
	fi
	if [[ "`nvram get stm_sentence_active`" != 1 && "$stm_period_tx_active" != 1 && "`nvram get qos_obw`" != $outbound_bandwidth_without_stm ]]
	then
		$LOGGER "Removing preventative outbound QOS limit"
		nvram set qos_obw=$outbound_bandwidth_without_stm
		service qos restart	
	fi
fi

# If STM is active, check whether QOS is set accordingly
if [[ "`nvram get stm_sentence_active`" = 1  && "`nvram get qos_obw`" != $outbound_bandwidth_with_stm ]]
then
	if [[ $stm_sentence_triggered = 1 ]]
	then
		# We must have triggered an STM sentence on this run.
		# Work out the hour component of the time the STM sentence will end
		# (for logging purposes only - we will use unix time to track the
		# STM sentence accurately)
		stm_end_hour=`expr $current_hours + $stm_sentence_length_hours`
		if [[ $stm_end_hour -gt 23 ]]
		then
			stm_end_hour="0`expr $stm_end_hour - 24`"
		fi
		
		# Set NVRAM variables to describe the STM sentence being served
		nvram set stm_sentence_end_time="$stm_end_hour:$current_minutes"
		nvram set stm_sentence_end_unix_time=`expr $current_unix_time + $stm_sentence_length_seconds`

		# Send information to log
		$LOGGER "STM sentence started: applying STM QOS configuration (until $stm_end_hour:$current_minutes)"
	else
		# An STM sentence is active and we didn't trigger it on this run,

		# but the QOS settings are wrong. We must have rebooted, or someone
		# messed with the QOS settings. So we want to set the QOS settings

		# to deal with the STM, but we don't want to overwrite our NVRAM
		# variables.
		# Send information to log
		$LOGGER "STM sentence resumed: applying STM QOS configuration (until `nvram get stm_sentence_end_time`)" 
	fi
	
	# Set QOS NVRAM variables
	nvram set qos_obw=$outbound_bandwidth_with_stm
	nvram set qos_ibw=$inbound_bandwidth_with_stm

	# Restart QOS
	service qos restart
fi

# Add status to the web page
web_page="${web_page}<h4>STM Prevention: "
if [[ $preventative_mode = 1 ]]
then 
	if [[ "`nvram get qos_ibw`" = $inbound_bandwidth_without_stm ]]
	then
		web_page="${web_page}<b style=\"color:green\">Inactive</b></h4>"
	else
		web_page="${web_page}<b style=\"color:red\">Active</b></h4>"
	fi
else
	web_page="${web_page}<b style=\"color:CCCCCC\">Not enabled</b></h4>"
fi

web_page="${web_page}<h4>STM Mitigation: "
if [[ "`nvram get stm_sentence_active`" = 1 ]]
then
	web_page="${web_page}<b style=\"color:red\">Active - STM sentence will end at `nvram get stm_sentence_end_time`</b></h4>"
else
	web_page="${web_page}<b style=\"color:green\">Inactive</b></h4>"
fi

web_page="${web_page}<h4>Current QOS settings: <b style=\"color:blue\">`nvram get qos_ibw`</b> kb/s down, <b style=\"color:blue\">`nvram get qos_obw`</b> kb/s up</h4>"

# Add latest log entries to web page
web_page="${web_page}<h3>Latest Log Entries</h3>"
log=""
while read line
do
	case $line in
		*STM-Monitor*) log="${log}<nobr><tt>${line}</tt></nobr><br>" ;;
	esac
done < /tmp/var/log/messages
web_page="${web_page}$log"

# Publish web page
web_page="${web_page}</body></html>"
echo $web_page > /var/wwwext/stm-monitor.htm

I've done as much testing as I had time for but there may still be bugs. Feedback is most welcome as always.
 
I have been using version 1.01 for a few days now and not found any problems other then at first I used -P instead of -p but that became apparent when I looked in the log :) . Recently we haven't been STM'd though as I've found that by showing the other house mates how to check the STM monitor page they have curbed their downloads during STM period.

I'm finding at the moment my bandwidth to be fairly unstable sometimes as low as 365 kB/s during peak but usually between 500 - 980 kB/s (Damn you virginmedia!). This really does make my QOS almost useless so I may have trouble testing the scripts full capabilities, but I should think its beyond this scripts scope to do bandwidth tests so will take it to the tomato feature request thread.

Thanks for updating!
 
Hi Tievolu,

I have a small problem with the bandwidth totals, it seems they're not working. Here's my STM monitor page output and as you can see the values are zero. I'm guessing this might affect the STM triggering too ?

* I'm using vanilla tomato 1.23.

TIA,
Dave

Tomato STM Monitor v1.01
Status at 11:07
STM Description Start End Bandwidth Limit Bandwidth Used Average Rate
Daytime Downstream (L) 10:00 15:00 2400 MB 0.00 MB 0.00 KB/s (0.00 kb/s)
Evening Downstream (L) 16:00 21:00 1200 MB N/A N/A
Evening Upstream (L) 15:00 20:00 700 MB N/A N/A

STM Prevention: Not enabled
STM Mitigation: Inactive
Current QOS settings: 102400 kb/s down, 465 kb/s up
Latest Log Entries


* Thanks for taking the time to write this script :)
 
davemuk, the script uses 'rstats' so please check if you have 'Bandwidth monitoring' enabled under Admin.
If that fails try enabling loggin aswell.
 
This is amazing, really good idea Tievolu. Will try this out when I get home. Thanks again for sharing your work.
 
Tievolu,
I moved to version 1.01 this week, previously was happy with version 1. 1.01 works very well and i managed to activate the STM mitigation on purpose to test the logic. Its a much improved version as the user no longer needs to actively monitor the bandwidth used.

Some feedback and hopefully you can add the following,,,
I have a virgin 'L' service and try to download/use as much of the given bandwidth. For example between 10am-3pm i get 2400mb for which i may download 2 files of size 999mb in succession.
I feel the STM prevention kicks in too soon. The STM prevention should only kick-in after a certain limit has been achieved (which may be user specified through the script), i.e. 85% (which is about 2000mb for my service). This would therefore give me a fast download for the 2 files and start STM prevention when bandwidth is almost used up.

I look forward to any updates you make.
thanks
 
Unfortunately I've had to change the location of this script, as Google are killing off their Google Pages service. The script can now be found here:

http://host.blucube.co.uk/~tievolu/stm/stm-monitor.sh

and the (limited) documentation can be found here:

http://host.blucube.co.uk/~tievolu/stm/stm-monitor.html

The old Google Pages site will be killed in June, so update your WAN Up scripts as soon as you can...

On a more postitive note, version 1.02 is now available, which adds an STM prevention threshold to prevent STM prevention kicking in too early. In v1.02, STM prevention won't become active until you've transferred 80% of an STM limit. Thanks to kardzzz for suggesting this feature and testing my bungled attempts at implementing it :)

In addition you can now customise some of the settings, including the STM prevention threshold. More details here:

http://host.blucube.co.uk/~tievolu/stm/stm-monitor.html#CUSTOMISE

Let me know if you have any questions.
 
I've got a good feature Idea,
If STM prevention is on, recalculate the speed you can download every time the script is run.
EG, when I've downloaded 2.4GB (so have 600mb remaining) and have 2 hours left in the stm period, it may set 200kb/s as my downrate. but if I don't download anything for an hour I've still got the same speed of 200kb/s set but since I still have the same 600mb to use in the 1 hour remaining I should be at 400kb/s.

I'm loving your work so far.
 

Back
Top