Speedtest-cli Internet Up-/Downlink Measurement Integration

Below you’ll find a complete solution on integrating the internet connection bandwidth speed test from Speedtest.net in your OpenHAB setup. This is accomplished by using the speedtest-cli script, which you have to provide on your system.

Version Info: The guide was updated for openHAB 2.0 but should work on other versions as well.

Functionality:

  • execute speed test once or twice a day, every hour or upon command
  • show summarized results on the sitemap
  • show all results on an extra page after click
  • a button is defined to start the speedtest, either through the sitemap or your own rules

Installation and Test

Linux

#Install easy_install if not yet available
sudo apt-get install python-setuptools
# Installation of speedtest-cli
sudo easy_install speedtest-cli
# Testing
speedtest-cli
speedtest-cli --simple

Please make sure the script is working before you continue.

openHAB: You’ll need the Exec Binding. Please install and configure it in your preferred way.

Windows

You can find an unofficial speedtest-cli for Windows here: https://github.com/zpeters/speedtest

Place the executable on your harddrive, e.g. “C:\openHAB\speedtest.exe”. All further steps are descried below.

OpenHAB files

Icons

All png and svg icons in one archive:


Click image to download

Extract all icons to the icons folder under $OPENHAB_CONF/icons/classic/
(openHAB 1.x: $OPENHAB_HOME/webapps/images/)

Icons source (free for non-commercial use): http://www.iconarchive.com/show/polygon-icons-by-graphicloads.html

Items (speedtest.items)

Group gSpeedtest <"network-icon"> (Whg)

String      SpeedtestSummary        "Speedtest [%s]"             <"speedtest_network">       (gSpeedtest)
Number      SpeedtestResultPing     "Ping [%.3f ms]"             <"speedtest_next5">         (gSpeedtest)
Number      SpeedtestResultDown     "Downlink [%.2f Mbit/s]"     <"speedtest_download">      (gSpeedtest)
Number      SpeedtestResultUp       "Uplink [%.2f Mbit/s]"       <"speedtest_upload">        (gSpeedtest)
String      SpeedtestRunning        "Speedtest running ... [%s]" <"speedtest_new">           (gSpeedtest)
Switch      SpeedtestRerun          "Start manually"             <"speedtest_reload2">       (gSpeedtest)
DateTime    SpeedtestResultDate     "Last executed [%1$td.%1$tm.%1$tY, %1$tH:%1$tM Uhr]"   <"speedtest_problem4">      (gSpeedtest)

Sitemap (Part of myhome.sitemap)

...
Text item=SpeedtestSummary {
  Frame label="Ergebnisse" {
    Text item=SpeedtestResultDown
    Text item=SpeedtestResultUp
    Text item=SpeedtestResultPing
  }
  Frame label="Steuerung" {
    Text item=SpeedtestResultDate
    Text item=SpeedtestRunning label="Speedtest [%s]" visibility=[SpeedtestRunning != "-"]
    Switch item=SpeedtestRerun mappings=[ON="Start"]
  }
  Frame label="Statistik" {
    Text label="..." icon="speedtest_analytics8"
  }
}
...

Side Note on Statistics: The three dots under statistics are just a placeholder for whatever statistics you want to generate and present out of your measurements over time (see persistence).

One option is to persist the introduced items to InfluxDB and am drawing Grafana graphs from them. These are then displayed under statistics. For more details check the InfluxDB+Grafana tutorial (a speedtest graph example can be seen in the middle of the first posting).

Rule (Linux speedtest.rules)

val String filename = "speedtest.rules"

rule "Speedtest init"
when
    System started
then
    createTimer(now.plusSeconds(195)) [|
        if (SpeedtestRerun.state == NULL) SpeedtestRerun.postUpdate(OFF)
        if (SpeedtestRunning.state == NULL) SpeedtestRunning.postUpdate("-")
        if (SpeedtestSummary.state == NULL || SpeedtestSummary.state == "")
            SpeedtestSummary.postUpdate("⁉ (unbekannt)")
    ]
end
rule "Speedtest"
when
    //Time cron "0 0 5,13 * * ?" or
    Time cron "0 0 * * * ?" or
    Item SpeedtestRerun received command ON
then
    logInfo(filename, "--> speedtest executed...")
    SpeedtestRunning.postUpdate("Messung läuft...")

    // update timestamp for last execution
    SpeedtestResultDate.postUpdate(new DateTimeType())

    // execute the script, you may have to change the path depending on your system
    var String speedtestCliOutput = executeCommandLine("/usr/local/bin/speedtest-cli@@--simple", 120*1000)

    // for debugging:
    //var String speedtestCliOutput = "Ping: 43.32 ms\nDownload: 21.64 Mbit/s\nUpload: 4.27 Mbit/s"
    //logInfo(filename, "--> speedtest output:\n" + speedtestCliOutput + "\n\n")

    SpeedtestRunning.postUpdate("Datenauswertung...")

    // starts off with a fairly simple error check, should be enough to catch all problems I can think of
    if (speedtestCliOutput.startsWith("Ping") && speedtestCliOutput.endsWith("Mbit/s")) {
        var String[] results = speedtestCliOutput.split("\\r?\\n")
        var float ping = new java.lang.Float(results.get(0).split(" ").get(1))
        var float down = new java.lang.Float(results.get(1).split(" ").get(1))
        var float up   = new java.lang.Float(results.get(2).split(" ").get(1))
        SpeedtestResultPing.postUpdate(ping)
        SpeedtestResultDown.postUpdate(down)
        SpeedtestResultUp.postUpdate(up)
        SpeedtestSummary.postUpdate(String::format("ᐁ  %.1f Mbit/s  ᐃ %.1f Mbit/s (%.0f ms)", down, up, ping))
        SpeedtestRunning.postUpdate("-")
        logInfo(filename, "--> speedtest finished.")
    } else {
        SpeedtestResultPing.postUpdate(0)
        SpeedtestResultDown.postUpdate(0)
        SpeedtestResultUp.postUpdate(0)
        SpeedtestSummary.postUpdate("(unbekannt)")
        SpeedtestRunning.postUpdate("Fehler bei der Ausführung")
        logError(filename, "--> speedtest failed. Output:\n" + speedtestCliOutput + "\n\n")
    }
    SpeedtestRerun.postUpdate(OFF)
end

// vim: syntax=Xtend

That’s it. If you have problems, just activate the logging lines and have a look in your openhab.log to get an idea of what’s going on.

Rule (Windows speedtest.rules)

The following changes are needed compared to the Linux rule above:

...
var String speedtestCliOutput = executeCommandLine("c:\\openHAB\\speedtest.exe@@--report", 120*1000)
...
if (speedtestCliOutput.startsWith("201")) {        
    var String[] results = speedtestCliOutput.split("\\|")
    var float ping = new java.lang.Float(results.get(3))
    var float down = new java.lang.Float(results.get(4))
    var float up   = new java.lang.Float(results.get(5))
    SpeedtestResultPing.postUpdate(ping)
    SpeedtestResultDown.postUpdate(down/1024)
    SpeedtestResultUp.postUpdate(up/1024)
...
53 Likes

By jeez I love open source software! This is a great little script - I will be doing something very similar - thank you so much for sharing!

1 Like

I like this one. The wife has been complaining that speeds decrease in the evening, so this is a good way to keep track and for her to easily view the results.

I’ve updated your rule so that I don’t have to call a script that calls the script. I also run every hour and will add a graph result as well.

My items:

String 		SpeedtestPingResult 		"Ping [%s ms]" 
String 		SpeedtestDownResult 		"Downlink [%s MBit/s]"
String 		SpeedtestUpResult 			"Uplink [%s MBit/s]"

My rule:

rule "Speedtest every hour"
when
  Time cron "0 0 * * * ?"
then 
    logInfo("RULE", "speedtest started...")
//var String results = "Ping: 108.311 ms\nDownload: 12.26 Mbit/s\nUpload: 0.99 Mbit/s"
    var String results = executeCommandLine("/opt/scripts/speedtest-cli@@--simple", 120*1000)
    var textStr = results.split("\\r?\\n")
if (textStr != null)
{		
	SpeedtestPingResult.postUpdate(textStr.get(0).split(" ").get(1))
	SpeedtestDownResult.postUpdate(textStr.get(1).split(" ").get(1))
	SpeedtestUpResult.postUpdate(textStr.get(2).split(" ").get(1))
}	
end
3 Likes

Nice work fellas.

I installed it myself. I’m getting errors when it attempts to run, sounds like it’s indexing the array wrong?

2016-02-16 21:54:01.716 [ERROR] [.o.m.r.i.engine.ExecuteRuleJob] - Error during the execution of rule Speedtest every hour
java.lang.ArrayIndexOutOfBoundsException: 1
    at org.eclipse.xtext.xbase.lib.Conversions$WrappedArray.get(Conversions.java:183) ~[na:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0]
    at java.lang.reflect.Method.invoke(Method.java:483) ~[na:1.8.0]

But also, when I ran the test in the shell, I got a download speed of 75.46Mbits/sec. I ran the Speedtest app on my iPhone to check and got 126.59Mbits/sec…!

It’s also running once a minute, not once an hour!

C

Hey @tracstarr, great input!
I was mainly interested in my downlink connection but especially for graphs all three values are nice. I updated the first posting with your additions, combined things in a String representation for the sitemap and added a bit of logging. The extra variables are mainly for clarity.
I’m not sure if I like the idea to run this script hourly. I’m running it twice per day when nobody is active in my network. If wife/son/dog is watching netflix, the results will go down, that’s a self-fulfilling prophecy and will create trouble for you :smiley: Maybe you could graph the speedtest in conjunction with the current traffic from your router (SNMP?) to get the overall bandwidth…

Hey @tinkerfailure, can you try again with the updated code?

Excellent! I like the additions. Not much error checking on the arrays etc, but to be honest it’s not worth it. Either it works or it doesn’t. I agree with the hourly, too much.

Thanks! I added another condition. Now error checking ins done by checking if the output of speedtest-cli is something like “Ping…MBit/s”. If that’s the case than I guess everything went fine. Otherwise we’ll get the error logging we can work with. Just to be sure that the summary element doesn’t stay in it’s old state on an exception, I did another small modification and moved the reset to the top.

Btw. can somebody help with the String::format? The line throws an exception and I can’t figure out why…

your variables are all string. if you want to have float, your should cast them when you use get.split.get

1 Like

Ouch! :dizzy_face: that’s what I was thinking I’m doing haha. Somehow I forgot to move the “new DecimalType” up into the variable creations. It was late yesterday ^^ nevermind

:wink: it’s always easier when you check someone else code.

1 Like

A last update. Now with more info and control on the sitemap, including a button to manually start a speedtest.
@tracstarr I was surprised how stable the results are, even with clients active in the network. So I included hourly as a default option :wink:

2 Likes

This really needs a page in the wiki - would you mind transferring it across so it isn’t lost in these forums?

1 Like

good idea - done :slight_smile: Check the first posting.
Added some explanation and the icons I use.

1 Like

I saw the idea of integrating a Speedtest here:
https://klenzel.de/2665

Modifications i did with his help:

netstat.sh:

#!/bin/bash 
WGET="/usr/bin/wget"
$WGET -q --tries=2 -4 --timeout=10 http://www.google.com -O /tmp/google.idx &> /dev/null
if [ ! -s /tmp/google.idx ]
then
PING=0
DOWN=0
UP=0
IP="127.0.0.1"
else
/volume1/public/OpenHAB/configurations/scripts/speedtest-cli --simple >> /tmp/speedresult.txt
PING=$(cat /tmp/speedresult.txt |grep Ping |cut -d " " -f 2)
DOWN=$(cat /tmp/speedresult.txt |grep Download |cut -d " " -f 2)
UP=$(cat /tmp/speedresult.txt |grep Upload |cut -d " " -f 2) 
IP="`wget http://www.wieistmeineip.de -U "" -qO - | egrep -o '[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}' | uniq`"
fi
/usr/bin/curl -s --header "Content-Type: text/plain" --request POST --data $PING http://127.0.0.1:8081/rest/items/Inet_P 
/usr/bin/curl -s --header "Content-Type: text/plain" --request POST --data $DOWN http://127.0.0.1:8081/rest/items/Inet_D
/usr/bin/curl -s --header "Content-Type: text/plain" --request POST --data $UP   http://127.0.0.1:8081/rest/items/Inet_U
/usr/bin/curl -s --header "Content-Type: text/plain" --request POST --data $IP   http://127.0.0.1:8081/rest/items/Inet_IP
DATE=`date +'%d.%m.%Y - %T'`
#echo "$DATE: Internet-Speedtest wurde durchgefuehrt (Down: $DOWN, UP: $UP, PING: $PING, Public IP: $IP)"'\n' > /volume1/public/OpenHAB/configurations/scripts/log.txt
rm -f /tmp/speedresult.txt
rm -f /tmp/google.idx

main.rules:

rule "Cron 30 Minutes"
when
	Time cron "0 */30 * * * ?"
then
	executeCommandLine("sh /volume1/public/OpenHAB/configurations/scripts/netstat.sh")
end

Hey Dominic,
what’s the benefit of this approach?
Besides having the IP as an additional information (which seems unrelated to the rest), why would you bother and go “von hinten durch die Brust ins Auge”? :wink:

Since “speedtest-cli --simple --share” will also give you image url of the result, can that image shown in the sitemap?

I don’t believe that there is a way to feed a dynamic URL into a sitemap (short of rewriting the sitemap file on the fly). I would love to be proven wrong, though. Giving meaning to the widget Image item=MyDynamicURL would be great.

I am personally not really interested in the share-image but if you find a way to include it, tell us! :wink:

The problem is getting the image updated because once the first one is loaded, OH has it locked and you can’t rewrite it without first existing out of OH and freeing the file up. If someone has a solution to that then it would be easy to set up a job to copy the generated image to your image file location.

That would be a bug if openHAB was responsible for leaving an image file locked. Is there any chance that the code you use to copy the dynamic image to a well-known name is leaving the target file open, or that the copy is happening when the Jetty embedded web server is read the file? Is this on Windows by any chance?