aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOskari Timperi <oskari.timperi@iki.fi>2019-11-20 22:37:50 +0200
committerOskari Timperi <oskari.timperi@iki.fi>2019-11-20 22:37:50 +0200
commit1ed78edd3fa169f1b1d16cf4c852f438bd4c10fd (patch)
treed570f4b02a2d4094736f3f3f186840d788baf332
downloadweather-1ed78edd3fa169f1b1d16cf4c852f438bd4c10fd.tar.gz
weather-1ed78edd3fa169f1b1d16cf4c852f438bd4c10fd.zip
Initial commit
-rw-r--r--README.md15
-rw-r--r--config.nims68
-rw-r--r--src/update_database.nim75
-rw-r--r--src/update_graphs.nim107
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",
+ )