├── .gitignore ├── .travis.yml ├── CONTROL ├── conffiles └── control ├── LICENSE ├── README.md ├── ipkg-build ├── luci-wrtbwmon ├── Makefile ├── htdocs │ └── luci-static │ │ └── wrtbwmon.js ├── luasrc │ ├── controller │ │ └── wrtbwmon.lua │ ├── model │ │ └── cbi │ │ │ └── wrtbwmon │ │ │ ├── config.lua │ │ │ └── custom.lua │ └── view │ │ └── wrtbwmon.htm └── root │ ├── etc │ └── config │ │ └── wrtbwmon │ └── usr │ └── share │ ├── luci │ └── menu.d │ │ └── luci-wrtbwmon.json │ └── rpcd │ └── acl.d │ └── luci-wrtbwmon.json ├── mkipk.sh └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.ipk 3 | *~ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: bash 2 | 3 | addons: 4 | apt: 5 | packages: 6 | - fakeroot 7 | 8 | script: ./mkipk.sh 9 | 10 | branches: 11 | only: 12 | - /^v\d+\.\d+(\.\d+)?(-\S*)?$/ 13 | 14 | deploy: 15 | provider: releases 16 | api_key: 17 | secure: L5e6XlInj2M+fwL0qNQnwsykLtJtmEWNQrALgGcZcoqnBKsuc8rXrSujnlx1FnhR7ntYaSawQk4q7Ca8gk4a/a9BGxZrv2YUOhzLbiAnnaskystZ6J/x6mJqvUizLtB77b/62AFuJj6OkrESSgXrHCj0daHDDfotkV2BOqcEdUg65TgbtjDWEVgs+dyGuV76lGgh90BW/nyJButN3WFczPQKfLleuHJJ+Rg7OCjgMyruTBM0vYuwfAnNWeUCsVhAcOriRPoM+dkdiUB5wla6yj9gnUphuk2Gn773HXlZbcMQULh0tWIlGc3K05KJMBQdNnJaLPMnA2nmI5eogDSpB+oTJiBk8CMYFhG3y9xWHOHcbMszC8q3o8mUOaJQagrx0GD8EEuQum7lksKCYZImDQQLs0K8Ndrc2QOIjZza2Sx1Pscpbt4Jvhn548h/bIEsccgOJeduYZm9XJkjxpyL/XN94pRjz2JWnEPnRlep9yVX1Pl76eDvN86IP1CPL0We31FsAVD8mkFXtSqmPRfUepgqXbD+5QvnX6PqEzDOOuZmE/E4Dlktvgj14ix2jJMzIjdQDfhafCUzZ6Wmjmcbx+RnqfRb9FE34QsHecI4y+tGaAb2w81NqDI/35o2qZX84NxuW0UPIOF0PW+owabvHB1smCBiwFepLNy7RDwlzzc= 18 | file_glob: true 19 | file: luci-wrtbwmon*.ipk 20 | skip_cleanup: true 21 | on: 22 | repo: Kiougar/luci-wrtbwmon 23 | tags: true 24 | -------------------------------------------------------------------------------- /CONTROL/conffiles: -------------------------------------------------------------------------------- 1 | /etc/config/wrtbwmon 2 | -------------------------------------------------------------------------------- /CONTROL/control: -------------------------------------------------------------------------------- 1 | Package: luci-wrtbwmon 2 | Version: 0.8.3 3 | Depends: luci, wrtbwmon 4 | Architecture: all 5 | Maintainer: Georgios Tzourmpakis 6 | Section: net 7 | Priority: optional 8 | Description: A Luci module that uses wrtbwmon to track bandwidth usage. 9 | Source: https://github.com/Kiougar/luci-wrtbwmon 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Georgios Tzourmpakis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bandwidth tracker for OpenWRT [![Build Status](https://travis-ci.org/Kiougar/luci-wrtbwmon.svg?branch=master)](https://travis-ci.org/Kiougar/luci-wrtbwmon) 2 | 3 | This Luci module uses [wrtbwmon](https://github.com/pyrovski/wrtbwmon) to track bandwidth usage. 4 | 5 | ##### Features 6 | * **Auto refresh** every 5 seconds (can be changed) 7 | * Track **speed per client** (if auto refresh is enabled) 8 | * **No cron job** required (wrtbwmon is updated on demand) 9 | * **Map MAC addresses to usernames** by editing a file from the UI. 10 | * Ability to **persist database** across reboots and firmware updates 11 | 12 | After installation you will see a new `Usage` menu item inside the `Network` menu list in the Luci GUI. 13 | 14 | ![Network Usage](https://github.com/Kiougar/luci-wrtbwmon/blob/master/screenshot.png?raw=true) 15 | 16 | ##### What it does 17 | 18 | It displays a table that includes all columns **wrtbwmon** provides, 19 | with two additional ones (emphasis given): 20 | 21 | 1. Client 22 | 2. **Download speed** 23 | 3. **Upload speed** 24 | 4. Total downloaded 25 | 5. Total uploaded 26 | 6. Total usage 27 | 7. First seen date 28 | 8. Last seen date 29 | 30 | ##### How it works 31 | 32 | The download/upload speed is calculated in memory on the **front end** using JS 33 | thus **minimizing resource consumption** on the router. To properly calculate these values 34 | an auto refresh interval must be set that runs the following commands on the router: 35 | 36 | * `wrtbwmon update /tmp/usage.db` 37 | * `wrtbwmon publish /tmp/usage.db /tmp/usage.htm /etc/wrtbwmon.user` 38 | 39 | For the above commands to work the only *requirement* is that the `wrtbwmon` package is installed and enabled. 40 | 41 | ## Install 42 | 43 | ##### Step 1 - install the `wrtbwmon` package: 44 | 45 | * Download the latest `.ipk` file from [wrtbwmon releases](https://github.com/pyrovski/wrtbwmon/releases) 46 | * Copy the file to your router `/tmp` directory 47 | * I use the following command: `scp wrtbwmon_*_all.ipk root@192.168.1.1:/tmp/` 48 | * Install the package `opkg install /tmp/wrtbwmon_*_all.ipk` 49 | 50 | ##### Step 2 - setup* the `wrtbwmon` package: 51 | 52 | * Schedule it to run on startup `/etc/init.d/wrtbwmon enable` 53 | * Manually start it now `/etc/init.d/wrtbwmon start` 54 | 55 | **If you have already setup a `cron job` to update the `wrtbwmon` database, it would be best if you removed it. 56 | There is no need for `wrtbwmon` to regurarly update the db since we only need to run it when the `Usage` page is active.* 57 | 58 | ##### Step 3 - install this module: 59 | 60 | * Download the latest `.ipk` file from [releases](https://github.com/Kiougar/luci-wrtbwmon/releases) 61 | * Copy the file to your router `/tmp` directory 62 | * I use the following command: `scp luci-wrtbwmon_*_all.ipk root@192.168.1.1:/tmp/` 63 | * Install the package `opkg install /tmp/luci-wrtbwmon_*_all.ipk` 64 | * Clear the cache for `luci` to get the web interface to refresh `rm /tmp/luci-indexcache` 65 | 66 | Note that the `luci-compat` package is required, which you can install with: `opkg update && opkg install luci-compat`. 67 | 68 | ## TODO 69 | 70 | * Add the `.ipk` package to the `OpenWRT` feed 71 | 72 | ## Contribute 73 | 74 | Feel free to contribute on any of the above TODO items, or even on any feature you might think is helpful. 75 | I would appreciate any help. 76 | 77 | ## Credits 78 | 79 | A big thanks to 80 | * [pyrovski](https://github.com/pyrovski) for creating `wrtbwmon` and helping me with creating the `.ipk` package 81 | * [OpenWRT](https://github.com/OpenWRT) organization for creating and maintaining `openwrt` and `luci` 82 | * Carl Worth for his `ipkg-build` script that lies in this repo 83 | -------------------------------------------------------------------------------- /ipkg-build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # ipkg-build -- construct a .ipk from a directory 4 | # Carl Worth 5 | # based on a script by Steve Redler IV, steve@sr-tech.com 5-21-2001 6 | # 2003-04-25 rea@sr.unh.edu 7 | # Updated to work on Familiar Pre0.7rc1, with busybox tar. 8 | # Note it Requires: binutils-ar (since the busybox ar can't create) 9 | # For UID debugging it needs a better "find". 10 | set -e 11 | 12 | version=1.0 13 | 14 | ipkg_extract_value() { 15 | sed -e "s/^[^:]*:[[:space:]]*//" 16 | } 17 | 18 | required_field() { 19 | field=$1 20 | 21 | value=`grep "^$field:" < $CONTROL/control | ipkg_extract_value` 22 | if [ -z "$value" ]; then 23 | echo "*** Error: $CONTROL/control is missing field $field" >&2 24 | return 1 25 | fi 26 | echo $value 27 | return 0 28 | } 29 | 30 | disallowed_field() { 31 | field=$1 32 | 33 | value=`grep "^$field:" < $CONTROL/control | ipkg_extract_value` 34 | if [ -n "$value" ]; then 35 | echo "*** Error: $CONTROL/control contains disallowed field $field" >&2 36 | return 1 37 | fi 38 | echo $value 39 | return 0 40 | } 41 | 42 | pkg_appears_sane() { 43 | local pkg_dir=$1 44 | 45 | local owd=$PWD 46 | cd $pkg_dir 47 | 48 | PKG_ERROR=0 49 | 50 | tilde_files=`find . -name '*~'` 51 | if [ -n "$tilde_files" ]; then 52 | if [ "$noclean" = "1" ]; then 53 | echo "*** Warning: The following files have names ending in '~'. 54 | You probably want to remove them: " >&2 55 | ls -ld $tilde_files 56 | echo >&2 57 | else 58 | echo "*** Removing the following files: $tilde_files" 59 | rm -f "$tilde_files" 60 | fi 61 | fi 62 | 63 | large_uid_files=`find . -uid +99 || true` 64 | 65 | if [ "$ogargs" = "" ] && [ -n "$large_uid_files" ]; then 66 | echo "*** Warning: The following files have a UID greater than 99. 67 | You probably want to chown these to a system user: " >&2 68 | ls -ld $large_uid_files 69 | echo >&2 70 | fi 71 | 72 | 73 | if [ ! -f "$CONTROL/control" ]; then 74 | echo "*** Error: Control file $pkg_dir/$CONTROL/control not found." >&2 75 | cd $owd 76 | return 1 77 | fi 78 | 79 | pkg=`required_field Package` 80 | [ "$?" -ne 0 ] && PKG_ERROR=1 81 | 82 | version=`required_field Version | sed 's/Version://; s/^.://g;'` 83 | [ "$?" -ne 0 ] && PKG_ERROR=1 84 | 85 | arch=`required_field Architecture` 86 | [ "$?" -ne 0 ] && PKG_ERROR=1 87 | 88 | required_field Maintainer >/dev/null 89 | [ "$?" -ne 0 ] && PKG_ERROR=1 90 | 91 | required_field Description >/dev/null 92 | [ "$?" -ne 0 ] && PKG_ERROR=1 93 | 94 | section=`required_field Section` 95 | [ "$?" -ne 0 ] && PKG_ERROR=1 96 | if [ -z "$section" ]; then 97 | echo "The Section field should have one of the following values:" >&2 98 | echo "admin, base, comm, editors, extras, games, graphics, kernel, libs, misc, net, text, web, x11" >&2 99 | fi 100 | 101 | priority=`required_field Priority` 102 | [ "$?" -ne 0 ] && PKG_ERROR=1 103 | if [ -z "$priority" ]; then 104 | echo "The Priority field should have one of the following values:" >&2 105 | echo "required, important, standard, optional, extra." >&2 106 | echo "If you don't know which priority value you should be using, then use \`optional'" >&2 107 | fi 108 | 109 | source=`required_field Source` 110 | [ "$?" -ne 0 ] && PKG_ERROR=1 111 | if [ -z "$source" ]; then 112 | echo "The Source field contain the URL's or filenames of the source code and any patches" 113 | echo "used to build this package. Either gnu-style tarballs or Debian source packages " 114 | echo "are acceptable. Relative filenames may be used if they are distributed in the same" 115 | echo "directory as the .ipk file." 116 | fi 117 | 118 | disallowed_filename=`disallowed_field Filename` 119 | [ "$?" -ne 0 ] && PKG_ERROR=1 120 | 121 | if echo $pkg | grep '[^a-z0-9.+-]'; then 122 | echo "*** Error: Package name $name contains illegal characters, (other than [a-z0-9.+-])" >&2 123 | PKG_ERROR=1; 124 | fi 125 | 126 | local bad_fields=`sed -ne 's/^\([^[:space:]][^:[:space:]]\+[[:space:]]\+\)[^:].*/\1/p' < $CONTROL/control | sed -e 's/\\n//'` 127 | if [ -n "$bad_fields" ]; then 128 | bad_fields=`echo $bad_fields` 129 | echo "*** Error: The following fields in $CONTROL/control are missing a ':'" >&2 130 | echo " $bad_fields" >&2 131 | echo "ipkg-build: This may be due to a missing initial space for a multi-line field value" >&2 132 | PKG_ERROR=1 133 | fi 134 | 135 | for script in $CONTROL/preinst $CONTROL/postinst $CONTROL/prerm $CONTROL/postrm; do 136 | if [ -f $script -a ! -x $script ]; then 137 | echo "*** Error: package script $script is not executable" >&2 138 | PKG_ERROR=1 139 | fi 140 | done 141 | 142 | if [ -f $CONTROL/conffiles ]; then 143 | for cf in `cat $CONTROL/conffiles`; do 144 | if [ ! -f ./$cf ]; then 145 | echo "*** Error: $CONTROL/conffiles mentions conffile $cf which does not exist" >&2 146 | PKG_ERROR=1 147 | fi 148 | done 149 | fi 150 | 151 | cd $owd 152 | return $PKG_ERROR 153 | } 154 | 155 | ### 156 | # ipkg-build "main" 157 | ### 158 | ogargs="" 159 | outer=ar 160 | noclean=0 161 | usage="Usage: $0 [-c] [-C] [-o owner] [-g group] []" 162 | while getopts "cg:ho:v" opt; do 163 | case $opt in 164 | o ) owner=$OPTARG 165 | ogargs="--owner=$owner" 166 | ;; 167 | g ) group=$OPTARG 168 | ogargs="$ogargs --group=$group" 169 | ;; 170 | c ) outer=tar 171 | ;; 172 | C ) noclean=1 173 | ;; 174 | v ) echo $version 175 | exit 0 176 | ;; 177 | h ) echo $usage >&2 ;; 178 | \? ) echo $usage >&2 179 | esac 180 | done 181 | 182 | 183 | shift $(($OPTIND - 1)) 184 | 185 | # continue on to process additional arguments 186 | 187 | case $# in 188 | 1) 189 | dest_dir=$PWD 190 | ;; 191 | 2) 192 | dest_dir=$2 193 | if [ "$dest_dir" = "." -o "$dest_dir" = "./" ] ; then 194 | dest_dir=$PWD 195 | fi 196 | ;; 197 | *) 198 | echo $usage >&2 199 | exit 1 200 | ;; 201 | esac 202 | 203 | pkg_dir=$1 204 | 205 | if [ ! -d $pkg_dir ]; then 206 | echo "*** Error: Directory $pkg_dir does not exist" >&2 207 | exit 1 208 | fi 209 | 210 | # CONTROL is second so that it takes precedence 211 | CONTROL= 212 | [ -d $pkg_dir/DEBIAN ] && CONTROL=DEBIAN 213 | [ -d $pkg_dir/CONTROL ] && CONTROL=CONTROL 214 | if [ -z "$CONTROL" ]; then 215 | echo "*** Error: Directory $pkg_dir has no CONTROL subdirectory." >&2 216 | exit 1 217 | fi 218 | 219 | if ! pkg_appears_sane $pkg_dir; then 220 | echo >&2 221 | echo "ipkg-build: Please fix the above errors and try again." >&2 222 | exit 1 223 | fi 224 | 225 | tmp_dir=$dest_dir/IPKG_BUILD.$$ 226 | mkdir $tmp_dir 227 | 228 | echo $CONTROL > $tmp_dir/tarX 229 | ( cd $pkg_dir && tar $ogargs -czf $tmp_dir/data.tar.gz . -X $tmp_dir/tarX ) 230 | ( cd $pkg_dir/$CONTROL && tar $ogargs -czf $tmp_dir/control.tar.gz . ) 231 | rm $tmp_dir/tarX 232 | 233 | echo "2.0" > $tmp_dir/debian-binary 234 | 235 | pkg_file=$dest_dir/${pkg}_${version}_${arch}.ipk 236 | rm -f $pkg_file 237 | if [ "$outer" = "ar" ] ; then 238 | ( cd $tmp_dir && ar -crf $pkg_file ./debian-binary ./data.tar.gz ./control.tar.gz ) 239 | else 240 | ( cd $tmp_dir && tar -zcf $pkg_file ./debian-binary ./data.tar.gz ./control.tar.gz ) 241 | fi 242 | 243 | rm $tmp_dir/debian-binary $tmp_dir/data.tar.gz $tmp_dir/control.tar.gz 244 | rmdir $tmp_dir 245 | 246 | echo "Packaged contents of $pkg_dir into $pkg_file" 247 | -------------------------------------------------------------------------------- /luci-wrtbwmon/Makefile: -------------------------------------------------------------------------------- 1 | include ../../build/config.mk 2 | include ../../build/module.mk -------------------------------------------------------------------------------- /luci-wrtbwmon/htdocs/luci-static/wrtbwmon.js: -------------------------------------------------------------------------------- 1 | var wrt = { 2 | // variables for auto-update, interval is in seconds 3 | scheduleTimeout: undefined, 4 | updateTimeout: undefined, 5 | isScheduled: true, 6 | interval: 5, 7 | // option on whether to show per host sub-totals 8 | perHostTotals: false, 9 | // variables for sorting 10 | sortData: { 11 | column: 7, 12 | elId: 'thTotal', 13 | dir: 'desc', 14 | cache: {} 15 | } 16 | }; 17 | 18 | (function () { 19 | var oldDate, oldValues = []; 20 | 21 | // find base path 22 | var re = /(.*?admin\/network\/[^/]+)/; 23 | var basePath = window.location.pathname.match(re)[1]; 24 | 25 | //---------------------- 26 | // HELPER FUNCTIONS 27 | //---------------------- 28 | 29 | /** 30 | * Human readable text for size 31 | * @param size 32 | * @returns {string} 33 | */ 34 | function getSize(size) { 35 | var prefix = [' ', 'k', 'M', 'G', 'T', 'P', 'E', 'Z']; 36 | var precision, base = 1000, pos = 0; 37 | while (size > base) { 38 | size /= base; 39 | pos++; 40 | } 41 | if (pos > 2) precision = 1000; else precision = 1; 42 | return (Math.round(size * precision) / precision) + ' ' + prefix[pos] + 'B'; 43 | } 44 | 45 | /** 46 | * Human readable text for date 47 | * @param date 48 | * @returns {string} 49 | */ 50 | function dateToString(date) { 51 | return date.toString().substring(0, 24); 52 | } 53 | 54 | /** 55 | * Gets the string representation of the date received from BE 56 | * @param value 57 | * @returns {*} 58 | */ 59 | function getDateString(value) { 60 | var tmp = value.split('_'), 61 | str = tmp[0].split('-').reverse().join('-') + 'T' + tmp[1]; 62 | return dateToString(new Date(str)); 63 | } 64 | 65 | /** 66 | * Create a `tr` element with content 67 | * @param content 68 | * @returns {string} 69 | */ 70 | function createTR(content) { 71 | var res = '' + data[2], {title: data[1]}), 258 | createTD(getSize(dlSpeed) + '/s', {right: true}), 259 | createTD(getSize(upSpeed) + '/s', {right: true}), 260 | createTD(getSize(data[3]), {right: true}), 261 | createTD(getSize(data[4]), {right: true}), 262 | createTD(getSize(data[5]), {right: true}), 263 | createTD(getDateString(data[6])), 264 | createTD(getDateString(data[7])) 265 | ]; 266 | 267 | // display row data 268 | var result = ''; 269 | for (var k = 0; k < displayData.length; k++) { 270 | result += displayData[k]; 271 | } 272 | result = createTR(result); 273 | return [result, rowData]; 274 | } 275 | 276 | /** 277 | * Creates the HTML output based on the `data` and `totals` inputs 278 | * @param data 279 | * @param totals 280 | * @returns {string} HTML output 281 | */ 282 | function getDisplayData(data, totals) { 283 | var result = 284 | createTH('Client', {id: 'thClient'}) + 285 | createTH('Download', {id: 'thDownload'}) + 286 | createTH('Upload', {id: 'thUpload'}) + 287 | createTH('Total Down', {id: 'thTotalDown'}) + 288 | createTH('Total Up', {id: 'thTotalUp'}) + 289 | createTH('Total', {id: 'thTotal'}) + 290 | createTH('First Seen', {id: 'thFirstSeen'}) + 291 | createTH('Last Seen', {id: 'thLastSeen'}); 292 | result = createTR(result); 293 | for (var k = 0; k < data.length; k++) { 294 | result += data[k][0]; 295 | } 296 | var totalsRow = createTH('TOTAL'); 297 | for (var m = 0; m < totals.length; m++) { 298 | var t = totals[m]; 299 | totalsRow += createTD(getSize(t) + (m < 2 ? '/s' : ''), {right: true}); 300 | } 301 | result += createTR(totalsRow); 302 | return result; 303 | } 304 | 305 | /** 306 | * Calculates per host sub-totals and adds them in the data input 307 | * @param data The data input 308 | */ 309 | function aggregateHostTotals(data) { 310 | if (!wrt.perHostTotals) return; 311 | 312 | var curHost = 0, insertAt = 1; 313 | while (curHost < data.length && insertAt < data.length) { 314 | // grab the current hostname/mac, and walk the data looking for rows with the same host/mac 315 | var hostName = data[curHost][1][0].toLowerCase(); 316 | for (var k = curHost + 1; k < data.length; k++) { 317 | if (data[k][1][0].toLowerCase() === hostName) { 318 | // this is another row for the same host, group it with any other rows for this host 319 | data.splice(insertAt, 0, data.splice(k, 1)[0]); 320 | insertAt++; 321 | } 322 | } 323 | 324 | // if we found more than one row for the host, add a subtotal row 325 | if (insertAt > curHost + 1) { 326 | var hostTotals = [data[curHost][1][0], '', '', 0, 0, 0, 0, 0]; 327 | for (var i = curHost; i < insertAt && i < data.length; i++) { 328 | for (var j = 3; j < hostTotals.length; j++) { 329 | hostTotals[j] += data[i][1][j]; 330 | } 331 | } 332 | var hostTotalRow = createTH(data[curHost][1][0] + '
(host total)', {title: data[curHost][1][1]}); 333 | for (var m = 3; m < hostTotals.length; m++) { 334 | var t = hostTotals[m]; 335 | hostTotalRow += createTD(getSize(t) + (m < 5 ? '/s' : ''), {right: true}); 336 | } 337 | hostTotalRow = createTR(hostTotalRow); 338 | data.splice(insertAt, 0, [hostTotalRow, hostTotals]); 339 | } 340 | curHost = insertAt; 341 | insertAt = curHost + 1; 342 | } 343 | } 344 | 345 | /** 346 | * Sorting function used to sort the `data`. Uses the global sort settings 347 | * @param x first item to compare 348 | * @param y second item to compare 349 | * @returns {number} 1 for desc, -1 for asc, 0 for equal 350 | */ 351 | function sortingFunction(x, y) { 352 | // get data from global variable 353 | var sortColumn = wrt.sortData.column, sortDirection = wrt.sortData.dir; 354 | var a = x[1][sortColumn]; 355 | var b = y[1][sortColumn]; 356 | if (a === b) { 357 | return 0; 358 | } else if (sortDirection === 'desc') { 359 | return a < b ? 1 : -1; 360 | } else { 361 | return a > b ? 1 : -1; 362 | } 363 | } 364 | 365 | /** 366 | * Sets the relevant global sort variables and re-renders the table to apply the new sorting 367 | * @param elId 368 | * @param column 369 | */ 370 | function setSortColumn(elId, column) { 371 | if (column === wrt.sortData.column) { 372 | // same column clicked, switch direction 373 | wrt.sortData.dir = wrt.sortData.dir === 'desc' ? 'asc' : 'desc'; 374 | } else { 375 | // change sort column 376 | wrt.sortData.column = column; 377 | // reset sort direction 378 | wrt.sortData.dir = 'desc'; 379 | } 380 | wrt.sortData.elId = elId; 381 | 382 | // render table data from cache 383 | renderTableData(wrt.sortData.cache.data, wrt.sortData.cache.totals); 384 | } 385 | 386 | /** 387 | * Registers the table events handlers for sorting when clicking the column headers 388 | */ 389 | function registerTableEventHandlers() { 390 | // note these ordinals are into the data array, not the table output 391 | document.getElementById('thClient').addEventListener('click', function () { 392 | setSortColumn(this.id, 0); // hostname 393 | }); 394 | document.getElementById('thDownload').addEventListener('click', function () { 395 | setSortColumn(this.id, 3); // dl speed 396 | }); 397 | document.getElementById('thUpload').addEventListener('click', function () { 398 | setSortColumn(this.id, 4); // ul speed 399 | }); 400 | document.getElementById('thTotalDown').addEventListener('click', function () { 401 | setSortColumn(this.id, 5); // total down 402 | }); 403 | document.getElementById('thTotalUp').addEventListener('click', function () { 404 | setSortColumn(this.id, 6); // total up 405 | }); 406 | document.getElementById('thTotal').addEventListener('click', function () { 407 | setSortColumn(this.id, 7); // total 408 | }); 409 | } 410 | 411 | /** 412 | * Fetches and handles the updated `values` from the BE 413 | * @param once If set to true, it re-schedules itself for execution based on selected interval 414 | */ 415 | function receiveData(once) { 416 | var ajax = new XMLHttpRequest(); 417 | ajax.onreadystatechange = function () { 418 | // noinspection EqualityComparisonWithCoercionJS 419 | if (this.readyState == 4 && this.status == 200) { 420 | var re = /(var values = new Array[^;]*;)/, 421 | match = ajax.responseText.match(re); 422 | if (!match) { 423 | handleError(); 424 | } else { 425 | // evaluate values 426 | eval(match[1]); 427 | //noinspection JSUnresolvedVariable 428 | var v = values; 429 | if (!v) { 430 | handleError(); 431 | } else { 432 | handleValues(v); 433 | // set old values 434 | oldValues = v; 435 | // set old date 436 | oldDate = new Date(); 437 | document.getElementById('updated').innerHTML = 'Last updated ' + dateToString(oldDate); 438 | } 439 | } 440 | var int = wrt.interval; 441 | if (!once && int > 0) reschedule(int); 442 | } 443 | }; 444 | ajax.open('GET', basePath + '/usage_data', true); 445 | ajax.send(); 446 | } 447 | 448 | /** 449 | * Registers DOM event listeners for user interaction 450 | */ 451 | function addEventListeners() { 452 | document.getElementById('intervalSelect').addEventListener('change', function () { 453 | var int = wrt.interval = this.value; 454 | if (int > 0) { 455 | // it is not scheduled, schedule it 456 | if (!wrt.isScheduled) { 457 | reschedule(int); 458 | } 459 | } else { 460 | // stop the scheduling 461 | stopSchedule(); 462 | } 463 | }); 464 | 465 | document.getElementById('resetDatabase').addEventListener('click', function () { 466 | if (confirm('This will delete the database file. Are you sure?')) { 467 | var ajax = new XMLHttpRequest(); 468 | ajax.onreadystatechange = function () { 469 | // noinspection EqualityComparisonWithCoercionJS 470 | if (this.readyState == 4 && this.status == 204) { 471 | location.reload(); 472 | } 473 | }; 474 | ajax.open('GET', basePath + '/usage_reset', true); 475 | ajax.send(); 476 | } 477 | }); 478 | 479 | document.getElementById('perHostTotals').addEventListener('change', function () { 480 | wrt.perHostTotals = !wrt.perHostTotals; 481 | }); 482 | } 483 | 484 | //---------------------- 485 | // AUTO-UPDATE 486 | //---------------------- 487 | 488 | /** 489 | * Stop auto-update schedule 490 | */ 491 | function stopSchedule() { 492 | window.clearTimeout(wrt.scheduleTimeout); 493 | window.clearTimeout(wrt.updateTimeout); 494 | setUpdateMessage(''); 495 | wrt.isScheduled = false; 496 | } 497 | 498 | /** 499 | * Start auto-update schedule 500 | * @param seconds 501 | */ 502 | function reschedule(seconds) { 503 | wrt.isScheduled = true; 504 | seconds = seconds || 60; 505 | updateSeconds(seconds); 506 | wrt.scheduleTimeout = window.setTimeout(receiveData, seconds * 1000); 507 | } 508 | 509 | /** 510 | * Sets the text of the `#updating` element 511 | * @param msg 512 | */ 513 | function setUpdateMessage(msg) { 514 | document.getElementById('updating').innerHTML = msg; 515 | } 516 | 517 | /** 518 | * Updates the 'Updating in X seconds' message 519 | * @param start 520 | */ 521 | function updateSeconds(start) { 522 | setUpdateMessage('Updating again in ' + start + ' seconds.'); 523 | if (start > 0) { 524 | wrt.updateTimeout = window.setTimeout(function () { 525 | updateSeconds(start - 1); 526 | }, 1000); 527 | } 528 | } 529 | 530 | //---------------------- 531 | // END AUTO-UPDATE 532 | //---------------------- 533 | 534 | /** 535 | * Check for dependency, and if all is well, run callback 536 | * @param cb Callback function 537 | */ 538 | function checkForDependency(cb) { 539 | var ajax = new XMLHttpRequest(); 540 | ajax.onreadystatechange = function () { 541 | // noinspection EqualityComparisonWithCoercionJS 542 | if (this.readyState == 4 && this.status == 200) { 543 | // noinspection EqualityComparisonWithCoercionJS 544 | if (ajax.responseText == "1") { 545 | cb(); 546 | } else { 547 | alert("wrtbwmon is not installed!"); 548 | } 549 | } 550 | }; 551 | ajax.open('GET', basePath + '/check_dependency', true); 552 | ajax.send(); 553 | } 554 | 555 | checkForDependency(function () { 556 | // register events 557 | addEventListeners(); 558 | // Main entry point 559 | receiveData(); 560 | }); 561 | 562 | })(); 563 | -------------------------------------------------------------------------------- /luci-wrtbwmon/luasrc/controller/wrtbwmon.lua: -------------------------------------------------------------------------------- 1 | module("luci.controller.wrtbwmon", package.seeall) 2 | 3 | function index() 4 | entry({"admin", "network", "usage"}, alias("admin", "network", "usage", "details"), _("Usage"), 60) 5 | entry({"admin", "network", "usage", "details"}, template("wrtbwmon"), _("Details"), 10).leaf=true 6 | entry({"admin", "network", "usage", "config"}, cbi("wrtbwmon/config"), _("Configuration"), 20).leaf=true 7 | entry({"admin", "network", "usage", "custom"}, form("wrtbwmon/custom"), _("User file"), 30).leaf=true 8 | entry({"admin", "network", "usage", "check_dependency"}, call("check_dependency")).dependent=true 9 | entry({"admin", "network", "usage", "usage_data"}, call("usage_data")).dependent=true 10 | entry({"admin", "network", "usage", "usage_reset"}, call("usage_reset")).dependent=true 11 | end 12 | 13 | function usage_database_path() 14 | local cursor = luci.model.uci.cursor() 15 | if cursor:get("wrtbwmon", "general", "persist") == "1" then 16 | return "/etc/config/usage.db" 17 | else 18 | return "/tmp/usage.db" 19 | end 20 | end 21 | 22 | function check_dependency() 23 | local ret = "0" 24 | local status, ipkg = pcall(require, "luci.model.ipkg") 25 | if not status or ipkg.installed('wrtbwmon') then 26 | ret = "1" 27 | end 28 | luci.http.prepare_content("text/plain") 29 | luci.http.write(ret) 30 | end 31 | 32 | function usage_data() 33 | local db = usage_database_path() 34 | local publish_cmd = "wrtbwmon publish " .. db .. " /tmp/usage.htm /etc/wrtbwmon.user" 35 | local cmd = "wrtbwmon update " .. db .. " && " .. publish_cmd .. " && cat /tmp/usage.htm" 36 | luci.http.prepare_content("text/html") 37 | luci.http.write(luci.sys.exec(cmd)) 38 | end 39 | 40 | function usage_reset() 41 | local db = usage_database_path() 42 | local ret = luci.sys.call("wrtbwmon update " .. db .. " && rm " .. db) 43 | luci.http.status(204) 44 | end 45 | -------------------------------------------------------------------------------- /luci-wrtbwmon/luasrc/model/cbi/wrtbwmon/config.lua: -------------------------------------------------------------------------------- 1 | local m = Map("wrtbwmon", "Usage - Configuration") 2 | 3 | local s = m:section(NamedSection, "general", "wrtbwmon", "General settings") 4 | 5 | local o = s:option(Flag, "persist", "Persist database", 6 | "Check this to persist the database file under /etc/config. " 7 | .. "This ensures usage is persisted even across firmware updates.") 8 | o.rmempty = false 9 | 10 | function o.write(self, section, value) 11 | if value == '1' then 12 | luci.sys.call("mv /tmp/usage.db /etc/config/usage.db") 13 | elseif value == '0' then 14 | luci.sys.call("mv /etc/config/usage.db /tmp/usage.db") 15 | end 16 | return Flag.write(self, section ,value) 17 | end 18 | 19 | return m 20 | -------------------------------------------------------------------------------- /luci-wrtbwmon/luasrc/model/cbi/wrtbwmon/custom.lua: -------------------------------------------------------------------------------- 1 | local USER_FILE_PATH = "/etc/wrtbwmon.user" 2 | 3 | local fs = require "nixio.fs" 4 | 5 | local f = SimpleForm("wrtbwmon", 6 | "Usage - Custom User File", 7 | "This file is used to match users with MAC addresses." 8 | .. "Each line must have the following format: \"00:aa:bb:cc:ee:ff,username\".") 9 | 10 | local o = f:field(Value, "_custom") 11 | 12 | o.template = "cbi/tvalue" 13 | o.rows = 20 14 | 15 | function o.cfgvalue(self, section) 16 | return fs.readfile(USER_FILE_PATH) 17 | end 18 | 19 | function o.write(self, section, value) 20 | value = value:gsub("\r\n?", "\n") 21 | fs.writefile(USER_FILE_PATH, value) 22 | end 23 | 24 | return f 25 | -------------------------------------------------------------------------------- /luci-wrtbwmon/luasrc/view/wrtbwmon.htm: -------------------------------------------------------------------------------- 1 | <%+header%> 2 |

Usage - Details

3 |

4 | 5 |

6 |

7 | 8 | 9 | 30 |
31 | 35 | 39 |
40 |

41 | 42 | 43 |
Loading...
44 | 45 | 46 | <%+footer%> 47 | -------------------------------------------------------------------------------- /luci-wrtbwmon/root/etc/config/wrtbwmon: -------------------------------------------------------------------------------- 1 | 2 | config wrtbwmon 'general' 3 | option persist '0' 4 | 5 | -------------------------------------------------------------------------------- /luci-wrtbwmon/root/usr/share/luci/menu.d/luci-wrtbwmon.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-wrtbwmon": { 3 | "description": "Grant UCI access for lucip-wrtbwmon", 4 | "read": { 5 | "uci": [ "wrtbwmon" ] 6 | }, 7 | "write": { 8 | "uci": [ "wrtbwmon" ] 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /luci-wrtbwmon/root/usr/share/rpcd/acl.d/luci-wrtbwmon.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-app-wrtbwmon": { 3 | "description": "Grant UCI access for luci-wrtbwmon", 4 | "read": { 5 | "uci": [ "wrtbwmon" ] 6 | }, 7 | "write": { 8 | "uci": [ "wrtbwmon" ] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mkipk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | tmpdir=`mktemp -d` 4 | mkdir -p $tmpdir/usr/lib/lua/luci && rsync -a luci-wrtbwmon/luasrc/ $_ 5 | rsync -a luci-wrtbwmon/root/ $tmpdir 6 | rsync -a luci-wrtbwmon/htdocs/ $tmpdir/www 7 | # this automatically sets the version based on the tag name, if it exists 8 | if [[ ! -z "${TRAVIS_TAG}" ]]; then 9 | sed -i "s/Version: .*/Version: ${TRAVIS_TAG}/" CONTROL/control 10 | fi 11 | rsync -a CONTROL $tmpdir 12 | chmod a+x ipkg-build 13 | fakeroot -- ./ipkg-build -c $tmpdir 14 | rm -rf "$tmpdir" 15 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiougar/luci-wrtbwmon/68982a7ecd1f6b0bebded75451e98559949e5640/screenshot.png --------------------------------------------------------------------------------