tempestas

A REST API for processing sensor.community data
git clone https://git.bracken.jp/tempestas.git
Log | Files | Refs | README | LICENSE

commit a5f244a207e079f21d612e14f1952ef7e2d979b9
Author: Chris Bracken <chris@bracken.jp>
Date:   Thu, 11 Nov 2021 08:47:34 -0800

Initial import

Diffstat:
A.gitignore | 1+
ALICENSE | 26++++++++++++++++++++++++++
Aairrohr/airrohr.go | 39+++++++++++++++++++++++++++++++++++++++
Ago.mod | 8++++++++
Ago.sum | 4++++
Amain.go | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asetup_db.sql | 13+++++++++++++
7 files changed, 180 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +tempestas diff --git a/LICENSE b/LICENSE @@ -0,0 +1,26 @@ +Copyright 2021, Chris Bracken <chris@bracken.jp> + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/airrohr/airrohr.go b/airrohr/airrohr.go @@ -0,0 +1,39 @@ +package airrohr + +import ( + "encoding/json" + "io" +) + +// Value is a reading from a sensor. Values have a type, which specifies the +// type of reading and the value of the reading itself. +// +// Known value types are: +// SDS_P1: 10 μm particulate matter +// SDS_P2: 2.5 μm particulate matter +// BME280_temperature: temperature in degrees Celsius +// BME280_pression: pressure in Pascals +// BME280_humidity: percent humidity +// samples: the count of loops the main() function ran +// min_micro: the minimum time for a loop through main() in μs +// max_micro: the maximum time for a loop through main() in μs +// signal: WiFi signal strength in dBm +type Value struct { + Type string `json:"value_type"` + Value float64 `json:"value,string"` +} + +// Sensor report from an ESP8266 NodeMCU running AirRohr firmware. +type Report struct { + SensorId string `json:"esp8266id"` + SoftwareVersion string `json:"software_version"` + Values []Value `json:"sensordatavalues"` +} + +// Parse reads a JSON-encoded value and stores it in the value pointed to by v. +func Parse(r io.Reader, v *Report) error { + decoder := json.NewDecoder(r) + decoder.DisallowUnknownFields() + err := decoder.Decode(&v) + return err +} diff --git a/go.mod b/go.mod @@ -0,0 +1,8 @@ +module git.bracken.jp/tempestas + +go 1.17 + +require ( + github.com/lib/pq v1.10.3 + goji.io v2.0.2+incompatible +) diff --git a/go.sum b/go.sum @@ -0,0 +1,4 @@ +github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg= +github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c= +goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk= diff --git a/main.go b/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" + + "git.bracken.jp/tempestas/airrohr" + _ "github.com/lib/pq" + "goji.io" + "goji.io/pat" +) + +const ( + DB_USER = "tempestas" + DB_PASSWORD = "<SET_PASSWORD_HERE>" // TODO: read from file/env + DB_NAME = "tempestas" + INSERT_REPORT = "INSERT INTO " + + "sensor_data(sensor_id, sw_version, reading_time, reading_type, reading_value) " + + "VALUES($1, $2, $3, $4, $5)" +) + +var ( + db *sql.DB +) + +func setupDB() *sql.DB { + dbinfo := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", DB_USER, DB_PASSWORD, DB_NAME) + db, err := sql.Open("postgres", dbinfo) + if err != nil { + panic("Error connecting to database") + } + return db +} + +func storeReport(report *airrohr.Report, t time.Time) { + for _, v := range report.Values { + _, err := db.Exec(INSERT_REPORT, report.SensorId, report.SoftwareVersion, t, v.Type, v.Value) + if err != nil { + fmt.Println("Error: failed to write sensor data") + fmt.Println(err) + } + } +} + +func PostAirrohr(w http.ResponseWriter, r *http.Request) { + contentType := r.Header.Get("Content-Type") + if contentType != "application/json" { + errorResponse(w, "Content-Type must be application/json", http.StatusUnsupportedMediaType) + return + } + var report airrohr.Report + err := airrohr.Parse(r.Body, &report) + if err != nil { + s, err := io.ReadAll(r.Body) + fmt.Println("Error: unknown JSON format from sensor: " + err.Error() + ". Message: " + string(s)) + errorResponse(w, "Bad Request. "+err.Error(), http.StatusBadRequest) + return + } + t := time.Now().UTC() + fmt.Println(t.String() + " Report received from sensor " + report.SensorId) + storeReport(&report, t) + errorResponse(w, "Success", http.StatusOK) +} + +func errorResponse(w http.ResponseWriter, message string, httpStatusCode int) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(httpStatusCode) + resp := make(map[string]string) + resp["message"] = message + jsonResp, _ := json.Marshal(resp) + w.Write(jsonResp) +} + +func main() { + fmt.Println("Initializing database") + db = setupDB() + defer db.Close() + + fmt.Println("Waiting for requests") + mux := goji.NewMux() + mux.HandleFunc(pat.Post("/sensor/airrohr/"), PostAirrohr) + log.Fatal(http.ListenAndServe(":8080", mux)) + +} diff --git a/setup_db.sql b/setup_db.sql @@ -0,0 +1,13 @@ +CREATE ROLE tempestas WITH LOGIN PASSWORD '<password>'; + +CREATE DATABASE tempestas WITH OWNER tempestas TEMPLATE template0 ENCODING UTF8 LC_COLLATE 'en_CA.UTF-8' LC_CTYPE 'en_CA.UTF-8'; + +CREATE TABLE sensor_data ( + sensor_id VARCHAR(63) NOT NULL, + sw_version VARCHAR(63) NOT NULL, + reading_time TIMESTAMP NOT NULL, + reading_type VARCHAR(63) NOT NULL, + reading_value NUMERIC(20,6) NOT NULL +); + +GRANT SELECT, INSERT ON sensor_data TO tempestas;