diff options
| author | Oskari Timperi <oskari.timperi@iki.fi> | 2019-11-20 22:37:50 +0200 |
|---|---|---|
| committer | Oskari Timperi <oskari.timperi@iki.fi> | 2019-11-20 22:37:50 +0200 |
| commit | 1ed78edd3fa169f1b1d16cf4c852f438bd4c10fd (patch) | |
| tree | d570f4b02a2d4094736f3f3f186840d788baf332 | |
| download | weather-1ed78edd3fa169f1b1d16cf4c852f438bd4c10fd.tar.gz weather-1ed78edd3fa169f1b1d16cf4c852f438bd4c10fd.zip | |
Initial commit
| -rw-r--r-- | README.md | 15 | ||||
| -rw-r--r-- | config.nims | 68 | ||||
| -rw-r--r-- | src/update_database.nim | 75 | ||||
| -rw-r--r-- | src/update_graphs.nim | 107 |
4 files changed, 265 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..81016bd --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Usage on kapsi.fi + +- Clone the repository to e.g. `/home/users/johndoe/sites/example.com/weather` + +- Execute `nim build` to build binaries for updating database and graphs + +- Create your databases with `nim createdb database/$DATABASE.rrd` + +- Install the CGI script with `nim install_cgi_kapsi $CGI_SCRIPT_PATH` + + - For example `nim install_cgi_kapsi $HOME/sites/example.com/www/weather/update.cgi` + +- Install a cron job for updating graphs + + - For example `$HOME/sites/example.com/weather/bin/update_graphs $HOME/sites/example.com/www/weather` diff --git a/config.nims b/config.nims new file mode 100644 index 0000000..b88d310 --- /dev/null +++ b/config.nims @@ -0,0 +1,68 @@ +import strformat +import strutils +from os import `/` + +task build, "build tools": + mkdir("bin") + exec("nim c -o:bin/update_graphs src/update_graphs.nim") + exec("nim c -o:bin/update_database src/update_database.nim") + +task createdb, "create a new database": + hint("QuitCalled", false) + + # This database will store the following data + # - 1 year worth of samples each 15 minutes + # - average of each day for 10 years + # - min of each day for 10 years + # - max of each day for 10 years + + let + database = thisDir() / "database" / paramStr(2) & ".rrd" + step = 15 * 60 + heartbeat = 20 * 60 + secondsInDay = 24 * 60 * 60 + samplesInDay = secondsInDay div step + secondsInYear = 365 * secondsInDay + samplesInYear = secondsInYear div step + daysIn10Years = 365 * 10 + + if fileExists(database): + echo(&"error: database '{database}' exists") + quit(1) + + echo(&"step = {step} seconds") + echo(&"heartbeat = {heartbeat} seconds") + echo(&"samplesInYear = {samplesInYear}") + echo(&"samplesInDay = {samplesInDay}") + echo(&"daysIn10Years = {daysIn10Years}") + + let args = [ + "create", database, + "--start", "now", + "--step", $step, + &"DS:temperature:GAUGE:{heartbeat}:-40:40", + &"DS:humidity:GAUGE:{heartbeat}:0:100", + &"DS:pressure:GAUGE:{heartbeat}:U:U", + &"DS:battery:GAUGE:{heartbeat}:0:4", + &"RRA:AVERAGE:0.5:1:{samplesInYear}", + &"RRA:MIN:0.5:{samplesInDay}:{daysIn10Years}", + &"RRA:MAX:0.5:{samplesInDay}:{daysIn10Years}", + &"RRA:AVERAGE:0.5:{samplesInDay}:{daysIn10Years}", + ] + + echo("rrdtool " & args.join(" ")) + exec("rrdtool " & args.join(" ")) + +task install_cgi_kapsi, "install cgi script on kapsi.fi": + proc modifyPath(s: string): string = + result = s.replace("/home/users", "/var/www/userhome") + let + databaseDir = modifyPath(thisDir() / "database") + updateDatabasePath = modifyPath(thisDir() / "bin" / "update_database") + scriptPath = paramStr(2) + script = &""" +#!{updateDatabasePath} +databaseDir = "{databaseDir}" +""" + + writeFile(scriptPath, script) diff --git a/src/update_database.nim b/src/update_database.nim new file mode 100644 index 0000000..1a27517 --- /dev/null +++ b/src/update_database.nim @@ -0,0 +1,75 @@ +import json +import os +import osproc +import parsecfg +import strformat +import strutils + +type + CgiError = object of CatchableError + code: int + +proc newCgiError(code: int, message: string): ref CgiError = + result = newException(CgiError, message) + result.code = code + +proc readData(): string = + var size = parseInt(getEnv("CONTENT_LENGTH").string) + if size == 0: + return "" + result = newString(size) + if readBuffer(stdin, addr result[0], size) != 0: + raise newCgiError(400, "not enough data") + +proc main() = + if paramCount() != 2: + raise newCgiError(500, "one argument required") + + let config = loadConfig(paramStr(1)) + let databaseDir = config.getSectionValue("", "databaseDir") + if databaseDir == "": + raise newCgiError(500, "databaseDir is empty") + + if getEnv("REQUEST_METHOD") != "POST": + raise newCgiError(400, "request method must be POST") + + let j = parseJson(readData()) + + for alias, measurement in j: + let filename = normalizedPath(joinPath(databaseDir, alias & ".rrd")) + + if not filename.startsWith(databaseDir): + raise newCgiError(400, "invalid database") + let + timestamp = measurement["timestamp"].getBiggestInt() + temperature = measurement["temperature"].getFloat() + humidity = measurement["humidity"].getFloat() + pressure = measurement["pressure"].getFloat() + battery = measurement["battery_potential"].getFloat() + data = &"{timestamp}:{temperature}:{humidity}:{pressure}:{battery}" + (_, ec) = execCmdEx(&"rrdtool update {filename} {data}") + if ec != 0: + raise newCgiError(500, "rrdtool failed") + +try: + main() + echo("Content-Type: text/plain") + echo("") +except CgiError as e: + echo(&"Status: {e.code}") + echo("Content-Type: text/plain") + echo("") + echo(e.msg) + quit(0) +except JsonParsingError as e: + echo(&"Status: 400 Bad Request") + echo("Content-Type: text/plain") + echo("") + echo(&"invalid json: {e.msg}") + quit(0) +except Exception as e: + echo(&"Status: 500 Internal Server Error") + echo("Content-Type: text/plain") + echo("") + echo(e.msg) + quit(0) diff --git a/src/update_graphs.nim b/src/update_graphs.nim new file mode 100644 index 0000000..3f902a4 --- /dev/null +++ b/src/update_graphs.nim @@ -0,0 +1,107 @@ +import os +import osproc +import strformat + +let + databaseDir = getAppDir().parentDir() / "database" + outputDir = paramStr(2) + width = 800 + height = 600 + +proc graph(filename, title, vlabel: string, args: varargs[string, `$`]) = + var cmdline: seq[string] = @[ + "graph", joinPath(outputDir, filename), + "--width", $width, + "--height", $height, + "--title", title, + "--vertical-label", vlabel, + ] & @args + + echo(&"Generating {joinPath(outputDir, filename)} ...") + + let p = startProcess("rrdtool", args=cmdline, + options={poStdErrToStdOut, poUsePath, poParentStreams}) + + if p.waitForExit() != 0: + quit(1) + + +for database in walkFiles(joinPath(databaseDir, "*.rrd")): + let (_, base, _) = splitFile(database) + + graph(&"{base}-temperature-today.png", "Temperature", "Celcius", + "--end", "23:59 today", + "--start", "00:00 today", + "--alt-autoscale", + "--left-axis-format", "%.1lf", + &"DEF:temp1={database}:temperature:AVERAGE", + &"DEF:temp2={database}:temperature:AVERAGE:end=00\\:00 today:start=end-24h", + "LINE1:temp1#000000:today", + "LINE1:temp2#FF0000:yesterday", + "SHIFT:temp2:86400", + ) + + graph(&"{base}-temperature-month.png", "Temperature", "Celcius", + "--end", "23:59 today", + "--start", "end-1month", + "--step", "86400", + "--alt-autoscale", + "--x-grid", "DAY:1:DAY:1:DAY:1:86400:%d", + &"DEF:avg={database}:temperature:AVERAGE", + &"DEF:min={database}:temperature:MIN", + &"DEF:max={database}:temperature:MAX", + "LINE1:avg#000000:average", + "LINE2:min#FF0000:min", + "LINE3:max#00FF00:max" + ) + + graph(&"{base}-humidity-today.png", "Relative humidity", "%", + "--end", "23:59 today", + "--start", "00:00 today", + &"DEF:humi1={database}:humidity:AVERAGE", + &"DEF:humi2={database}:humidity:AVERAGE:end=00\\:00 today:start=end-24h", + "LINE1:humi1#000000:today", + "LINE1:humi2#FF0000:yesterday", + "SHIFT:humi2:86400", + ) + + graph(&"{base}-humidity-month.png", "Relative humidity", "%", + "--end", "23:59 today", + "--start", "end-1month", + "--step", "86400", + "--alt-autoscale", + "--x-grid", "DAY:1:DAY:1:DAY:1:86400:%d", + &"DEF:avg={database}:humidity:AVERAGE", + &"DEF:min={database}:humidity:MIN", + &"DEF:max={database}:humidity:MAX", + "LINE1:avg#000000:average", + "LINE2:min#FF0000:min", + "LINE3:max#00FF00:max", + ) + + graph(&"{base}-pressure-today.png", "Pressure", "Pascal", + "--end", "23:59 today", + "--start", "00:00 today", + "--alt-autoscale", + "--left-axis-format", "%.1lf", + &"DEF:pres1={database}:pressure:AVERAGE", + &"DEF:pres2={database}:pressure:AVERAGE:end=00\\:00 today:start=end-24h", + "LINE1:pres1#000000:today", + "LINE1:pres2#FF0000:yesterday", + "SHIFT:pres2:86400", + ) + + graph(&"{base}-pressure-month.png", "Pressure", "Pascal", + "--end", "23:59 today", + "--start", "end-1month", + "--step", "86400", + "--alt-autoscale", + "--x-grid", "DAY:1:DAY:1:DAY:1:86400:%d", + "--left-axis-format", "%.1lf", + &"DEF:avg={database}:pressure:AVERAGE", + &"DEF:min={database}:pressure:MIN", + &"DEF:max={database}:pressure:MAX", + "LINE1:avg#000000:average", + "LINE2:min#FF0000:min", + "LINE3:max#00FF00:max", + ) |
