Compare commits
10 commits
02f7c6bdd1
...
a679a702c0
Author | SHA1 | Date | |
---|---|---|---|
Felix Niederwanger | a679a702c0 | ||
Felix Niederwanger | dd7c4f251e | ||
Felix Niederwanger | 5948210a92 | ||
Felix Niederwanger | 69211935ba | ||
Felix Niederwanger | 214397ab76 | ||
Felix Niederwanger | 439dfbeac5 | ||
Felix Niederwanger | 1e3873fd88 | ||
Felix Niederwanger | 54977b0730 | ||
Felix Niederwanger | 754cdbdb7e | ||
Felix Niederwanger | 7d049f20b8 |
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -34,6 +34,7 @@
|
|||
|
||||
# Debug files
|
||||
*.dSYM/
|
||||
__debug_bin
|
||||
|
||||
# GoLand
|
||||
.idea
|
||||
|
@ -44,9 +45,9 @@
|
|||
# Build files
|
||||
build
|
||||
build/
|
||||
/meteo
|
||||
/meteod
|
||||
/meteo-influx-gateway
|
||||
|
||||
## Custom config files
|
||||
*.conf
|
||||
*.toml
|
||||
*.ini
|
||||
|
|
35
Makefile
35
Makefile
|
@ -1,41 +1,12 @@
|
|||
default: all
|
||||
all: meteo meteod influxgateway $(SUBDIRS)
|
||||
all: meteod
|
||||
|
||||
install: all
|
||||
install meteo /usr/local/bin/
|
||||
install meteod /usr/local/bin/
|
||||
|
||||
## ==== Easy requirement install ============================================ ##
|
||||
|
||||
# requirements for meteod (server)
|
||||
req:
|
||||
go get "github.com/BurntSushi/toml"
|
||||
go get "github.com/gorilla/mux"
|
||||
go get "github.com/mattn/go-sqlite3"
|
||||
go get "github.com/eclipse/paho.mqtt.golang"
|
||||
# requirements for meteo (client)
|
||||
req-meteo:
|
||||
go get "github.com/BurntSushi/toml"
|
||||
req-gateway:
|
||||
go get "github.com/BurntSushi/toml"
|
||||
go get "github.com/jacobsa/go-serial/serial"
|
||||
|
||||
|
||||
## === Builds =============================================================== ##
|
||||
|
||||
meteo: cmd/meteo/meteo.go
|
||||
go build $^
|
||||
meteod: cmd/meteod/meteod.go
|
||||
go build $^
|
||||
gateway: cmd/gateway/gateway.go
|
||||
go build $^
|
||||
influxgateway: cmd/influxgateway/meteo-mqtt-influxdb.go cmd/influxgateway/influxdb.go cmd/influxgateway/mqtt.go
|
||||
go build -o meteo-influx-gateway $^
|
||||
meteod: cmd/meteod/*.go
|
||||
go build -o $@ $^
|
||||
|
||||
## === Tests ================================================================ ##
|
||||
|
||||
test: internal/database.go
|
||||
go test -v ./...
|
||||
|
||||
$(SUBDIRS):
|
||||
$(MAKE) -C $@
|
||||
|
|
66
README.md
66
README.md
|
@ -4,51 +4,7 @@
|
|||
|
||||
Lightweight environmental monitoring solution
|
||||
|
||||
This project aims to provide a centralized environmental and room monitoring system for different sensors.
|
||||
The meteo-daemon (`meteod`) runs on a centralised server instance, that collect all sensor data from different sensor nodes.
|
||||
|
||||
Client can attach to this server instance in order to read out the different readings of the sensors.
|
||||
|
||||
# Server
|
||||
|
||||
Requires `go >= 1.9.x` and the following repositories
|
||||
|
||||
go get "github.com/BurntSushi/toml"
|
||||
go get "github.com/gorilla/mux"
|
||||
go get "github.com/mattn/go-sqlite3"
|
||||
go get "github.com/eclipse/paho.mqtt.golang"
|
||||
|
||||
A quick way of installing the requirements is
|
||||
|
||||
$ make req # Installs requirements
|
||||
|
||||
## Configuration
|
||||
|
||||
Currently manually. See `meteod.toml` for information
|
||||
|
||||
## Storage
|
||||
|
||||
`meteod` stores all data in a Sqlite3 database. By default `meteod.db` is taken, but the filename can be configured in `meteod.toml`.
|
||||
|
||||
# Client
|
||||
|
||||
There is currently a very simple CLI client available: `meteo`
|
||||
|
||||
meteo REMOTE
|
||||
|
||||
$ meteo http://meteo-service.local/
|
||||
* 1 meteo-cluster 2019-05-14-17:24:01 22.51C|23.00 %rel| 95337hPa
|
||||
|
||||
## Build
|
||||
|
||||
Requires `go >= 1.9.x` and `"github.com/BurntSushi/toml"`
|
||||
|
||||
$ make req-meteo
|
||||
$ make meteo
|
||||
|
||||
## Configuration
|
||||
|
||||
Currently manually. See `meteod.toml`
|
||||
This project aims to provide a centralized environmental and room monitoring system for different sensors using mqtt. Data storage is supported via a `meteo-influxdb` gateway.
|
||||
|
||||
# Nodes/Sensors
|
||||
|
||||
|
@ -73,14 +29,22 @@ Every node that publishes `MQTT` packets in the given format is accepted.
|
|||
PAYLOAD: {"node":1,"timestamp":0,"distance":12.1}
|
||||
# if the timestamp is 0, the server replaces it with it's current time
|
||||
|
||||
## Data-Push via HTTP
|
||||
# meteo-influx-gateway
|
||||
|
||||
To push data via HTTP, you need a access `token`. The token identifies where the data belongs to.
|
||||
The provided `meteo-influx-gateway` is a program to collect meteo data points from mqtt and push them to a influxdb database. This gateway is written in go and needs to be build
|
||||
|
||||
The data is expected to be in the following `json` format
|
||||
go build ./...
|
||||
|
||||
{ "token":"test", "T":32.0, "Hum": 33.1, "P":1013.5 }
|
||||
The gateway is configured via a [simple INI file](meteo-influx-gateway.ini.example):
|
||||
|
||||
Example-`curl` script to push data to station 5
|
||||
```ini
|
||||
[mqtt]
|
||||
remote = "127.0.0.1"
|
||||
|
||||
[influxdb]
|
||||
remote = "http://127.0.0.1:8086"
|
||||
username = "meteo"
|
||||
password = "meteo"
|
||||
database = "meteo"
|
||||
```
|
||||
|
||||
$ curl 'http://localhost:8802/station/5' -X POST -H "Content-Type: application/json" --data { "token":"test", "T":32.0, "Hum": 33.1, "P":1013.5 }
|
|
@ -1,24 +1,23 @@
|
|||
#include <WiFi.h>
|
||||
#include <BME280I2C.h>
|
||||
#include <Wire.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <WiFi.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#include <WebServer.h>
|
||||
|
||||
|
||||
/* ==== CONFIGURE HERE ====================================================== */
|
||||
|
||||
#define SERIAL_BAUD 115200
|
||||
#define LED_BUILTIN 2
|
||||
#define N_SAMPLES 2 // Average report over this samples
|
||||
#define SAMPLE_DELAY 5000 // Milliseconds between samples
|
||||
#define SAMPLE_ALPHA 0.75 // Sampling average alpha
|
||||
#define N_SAMPLES 2 // Average report over this samples
|
||||
#define SAMPLE_DELAY 5000 // Milliseconds between samples
|
||||
#define SAMPLE_ALPHA 0.75 // Sampling average alpha
|
||||
|
||||
// TODO: Set your Wifi SSID and password
|
||||
|
||||
#define WIFI_SSID ""
|
||||
#define WIFI_PASSWORD ""
|
||||
#define WIFI_RECONNECT_DELAY 10000 // Reconnection delay in milliseconds
|
||||
#define WIFI_RECONNECT_DELAY 10000 // Reconnection delay in milliseconds
|
||||
|
||||
// TODO: Configure your node here
|
||||
|
||||
|
@ -33,9 +32,9 @@
|
|||
|
||||
/* ========================================================================== */
|
||||
|
||||
|
||||
BME280I2C bme; // Default : forced mode, standby time = 1000 ms
|
||||
// Oversampling = pressure ×1, temperature ×1, humidity ×1, filter off,
|
||||
BME280I2C
|
||||
bme; // Default : forced mode, standby time = 1000 ms
|
||||
// Oversampling = pressure ×1, temperature ×1, humidity ×1, filter off,
|
||||
|
||||
WiFiClient espClient;
|
||||
PubSubClient client(espClient);
|
||||
|
@ -45,55 +44,44 @@ WebServer server(80);
|
|||
static float temp(0), hum(0), pres(0);
|
||||
|
||||
static void led_toggle(const bool is_on) {
|
||||
digitalWrite(LED_BUILTIN, is_on?HIGH:LOW);
|
||||
digitalWrite(LED_BUILTIN, is_on ? HIGH : LOW);
|
||||
}
|
||||
|
||||
|
||||
void setup() {
|
||||
Serial.begin(SERIAL_BAUD);
|
||||
while(!Serial) {} // Wait for serial port
|
||||
|
||||
while (!Serial) {
|
||||
} // Wait for serial port
|
||||
|
||||
// initialize digital pin LED_BUILTIN as an output.
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
led_toggle(false);
|
||||
|
||||
// Init BME280
|
||||
Wire.begin();
|
||||
while(!bme.begin())
|
||||
{
|
||||
while (!bme.begin()) {
|
||||
Serial.println("ERR: BME280");
|
||||
delay(1000);
|
||||
}
|
||||
|
||||
// bme.chipID(); // Deprecated. See chipModel().
|
||||
switch(bme.chipModel())
|
||||
{
|
||||
case BME280::ChipModel_BME280:
|
||||
Serial.println("BME280");
|
||||
break;
|
||||
case BME280::ChipModel_BMP280:
|
||||
Serial.println("BMP280");
|
||||
break;
|
||||
default:
|
||||
Serial.println("ERR: Sensor");
|
||||
switch (bme.chipModel()) {
|
||||
case BME280::ChipModel_BME280:
|
||||
Serial.println("BME280");
|
||||
break;
|
||||
case BME280::ChipModel_BMP280:
|
||||
Serial.println("BMP280");
|
||||
break;
|
||||
default:
|
||||
Serial.println("ERR: Sensor");
|
||||
}
|
||||
read_bme280(temp,hum,pres); // Initial read
|
||||
|
||||
read_bme280(temp, hum, pres); // Initial read
|
||||
|
||||
// Wifi connection
|
||||
while(true) {
|
||||
Serial.println("Wifi...");
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
|
||||
Serial.println("ERR: Wifi");
|
||||
delay(5000);
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Serial.print("Wifi OK "); Serial.println(WiFi.localIP());
|
||||
WiFi.onEvent(wifi_connected, SYSTEM_EVENT_STA_CONNECTED);
|
||||
WiFi.onEvent(wifi_got_ip, SYSTEM_EVENT_STA_GOT_IP);
|
||||
WiFi.onEvent(wifi_disconnected, SYSTEM_EVENT_STA_DISCONNECTED);
|
||||
WiFi.mode(WIFI_STA);
|
||||
wifi_connect();
|
||||
client.setServer(MQTT_REMOTE, MQTT_PORT);
|
||||
server.on("/", www_handle);
|
||||
server.on("/csv", csv_handle);
|
||||
|
@ -101,6 +89,51 @@ void setup() {
|
|||
server.begin();
|
||||
}
|
||||
|
||||
void wifi_connect() {
|
||||
while (true) {
|
||||
Serial.println("Wifi ...");
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
|
||||
Serial.println("ERR: Wifi");
|
||||
delay(5000);
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void wifi_connected(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
Serial.println("Wifi: Connected");
|
||||
}
|
||||
|
||||
void wifi_got_ip(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
Serial.println("IP address: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
}
|
||||
|
||||
void wifi_disconnected(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
Serial.print("WiFi lost: ");
|
||||
Serial.println(info.disconnected.reason);
|
||||
Serial.println("Wifi reconnect (via event) ... ");
|
||||
WiFi.disconnect();
|
||||
WiFi.reconnect();
|
||||
}
|
||||
|
||||
void wifi_reconnect() {
|
||||
// Note: Wifi reconnect happens over events. This is only a last resort
|
||||
if ((WiFi.status() != WL_CONNECTED)) {
|
||||
static unsigned long previous = 0;
|
||||
const unsigned long current = millis();
|
||||
if (current - previous >= WIFI_RECONNECT_DELAY) {
|
||||
Serial.println("Wifi reconnect (via loop) ... ");
|
||||
WiFi.disconnect();
|
||||
WiFi.reconnect();
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int read_bme280(float &temp, float &hum, float &pres) {
|
||||
BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
|
||||
BME280::PresUnit presUnit(BME280::PresUnit_Pa);
|
||||
|
@ -108,90 +141,94 @@ int read_bme280(float &temp, float &hum, float &pres) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
bool reconnect_mqtt() {
|
||||
int retries = 5;
|
||||
while (!client.connected() && retries-- > 0) {
|
||||
// Don't retry if wifi is down
|
||||
if (WiFi.status() != WL_CONNECTED)
|
||||
return false;
|
||||
|
||||
void reconnect_mqtt() {
|
||||
while (!client.connected()) {
|
||||
if (client.connect(MQTT_CLIENTID)) {
|
||||
if (client.connected())
|
||||
return true;
|
||||
} else {
|
||||
Serial.print("MQTT connect failed, rc=");
|
||||
Serial.print("MQTT connect failed: error code ");
|
||||
Serial.print(client.state());
|
||||
delay(5000); // wait 5 seconds to not flood the network
|
||||
}
|
||||
delay(5000);
|
||||
}
|
||||
return client.connected();
|
||||
}
|
||||
|
||||
void report(float temp, float hum, float pres) {
|
||||
char message[256];
|
||||
char topic[64];
|
||||
snprintf(topic, 64, "meteo/%d", NODE_ID);
|
||||
snprintf(message, 255, "{\"id\":%d,\"name\":\"%s\",\"t\":%.2f,\"hum\":%.2f,\"p\":%.2f}", NODE_ID, NODE_NAME, temp, hum, pres);
|
||||
snprintf(message, 255,
|
||||
"{\"id\":%d,\"name\":\"%s\",\"t\":%.2f,\"hum\":%.2f,\"p\":%.2f}",
|
||||
NODE_ID, NODE_NAME, temp, hum, pres);
|
||||
Serial.println(message);
|
||||
client.publish(topic, message);
|
||||
}
|
||||
|
||||
void www_handle() {
|
||||
char html[512];
|
||||
snprintf(html, 512, "<!DOCTYPE html>\n<html>\n<body><h1>ESP32 Meteo Node</h1>\n<table><tr><td>Node</td><td>%d <b>%s</b></td></tr> <tr><td>Temperature</td><td>%.2f deg C</td></tr> <tr><td>Humidity</td><td>%.2f %% rel</td></tr> <tr><td>Pressure</td><td>%.2f hPa</td></tr> <tr><td>Pressure</td><td>%.2f hPa</td></tr> </table>\n<p>Readings: <a href=\"/csv\">[csv]</a> <a href=\"/json\">[json]</a></p></body></html>", NODE_ID, NODE_NAME, temp, hum, pres);
|
||||
snprintf(html, 511,
|
||||
"<!DOCTYPE html>\n<html>\n<body><h1>ESP32 Meteo "
|
||||
"Node</h1>\n<table><tr><td>Node</td><td>%d <b>%s</b></td></tr> "
|
||||
"<tr><td>Temperature</td><td>%.2f deg C</td></tr> "
|
||||
"<tr><td>Humidity</td><td>%.2f %% rel</td></tr> "
|
||||
"<tr><td>Pressure</td><td>%.2f hPa</td></tr> "
|
||||
"</table>\n<p>Readings: <a "
|
||||
"href=\"/csv\">csv</a> <a href=\"/json\">json</a></p></body></html>",
|
||||
NODE_ID, NODE_NAME, temp, hum, pres);
|
||||
html[511] = '\0';
|
||||
server.send(200, "text/html", html);
|
||||
}
|
||||
|
||||
void csv_handle() {
|
||||
char csv[32];
|
||||
snprintf(csv, 32, "%.2f,%.2f,%.2f\n", temp, hum, pres);
|
||||
char csv[64];
|
||||
snprintf(csv, 63, "%.2f,%.2f,%.2f\n", temp, hum, pres);
|
||||
csv[63] = '\0';
|
||||
server.send(200, "text/csv", csv);
|
||||
}
|
||||
|
||||
void json_handle() {
|
||||
char json[256];
|
||||
snprintf(json, 255, "{\"node\":%d,\"name\":\"%s\",\"t\":%.2f,\"hum\":%.2f,\"p\":%.2f}\n", NODE_ID, NODE_NAME, temp, hum, pres);
|
||||
snprintf(json, 255,
|
||||
"{\"node\":%d,\"name\":\"%s\",\"t\":%.2f,\"hum\":%.2f,\"p\":%.2f}\n",
|
||||
NODE_ID, NODE_NAME, temp, hum, pres);
|
||||
json[255] = '\0';
|
||||
server.send(200, "text/json", json);
|
||||
}
|
||||
|
||||
void wifi_reconnect() {
|
||||
Serial.print("Wifi reconnect ... ");
|
||||
WiFi.disconnect();
|
||||
WiFi.reconnect();
|
||||
// XXX: Test if this is somehow behaving weird
|
||||
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
|
||||
Serial.println("failed");
|
||||
} else {
|
||||
Serial.println("ok");
|
||||
}
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// wifi reconnect, if required
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
// But don't spam reconnections
|
||||
static unsigned long previousMillis = 0;
|
||||
const unsigned long currentMillis = millis();
|
||||
if (currentMillis - previousMillis >= WIFI_RECONNECT_DELAY) {
|
||||
wifi_reconnect();
|
||||
previousMillis = currentMillis;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Wifi reconnect handled via events. This is only an additional check
|
||||
wifi_reconnect();
|
||||
|
||||
// mqtt reconnect
|
||||
if (!client.connected()) { reconnect_mqtt(); }
|
||||
if (!client.connected()) {
|
||||
reconnect_mqtt();
|
||||
}
|
||||
client.loop();
|
||||
server.handleClient();
|
||||
|
||||
// Sensor readout
|
||||
const long timestamp = millis();
|
||||
static long next_sample = timestamp + SAMPLE_DELAY;
|
||||
if(timestamp > next_sample) {
|
||||
if (timestamp > next_sample) {
|
||||
next_sample = timestamp + SAMPLE_DELAY;
|
||||
float t(0), h(0), p(0);
|
||||
if(read_bme280(t, h, p) != 0) {
|
||||
if (read_bme280(t, h, p) != 0) {
|
||||
Serial.println("ERR: BME280");
|
||||
} else {
|
||||
temp = SAMPLE_ALPHA*temp + (1.0-SAMPLE_ALPHA)*t;
|
||||
hum = SAMPLE_ALPHA*hum + (1.0-SAMPLE_ALPHA)*h;
|
||||
pres = SAMPLE_ALPHA*pres + (1.0-SAMPLE_ALPHA)*p;
|
||||
|
||||
temp = SAMPLE_ALPHA * temp + (1.0 - SAMPLE_ALPHA) * t;
|
||||
hum = SAMPLE_ALPHA * hum + (1.0 - SAMPLE_ALPHA) * h;
|
||||
pres = SAMPLE_ALPHA * pres + (1.0 - SAMPLE_ALPHA) * p;
|
||||
|
||||
static int n = 0;
|
||||
if(++n >= N_SAMPLES) {
|
||||
report(temp,hum,pres);
|
||||
if (++n >= N_SAMPLES) {
|
||||
report(temp, hum, pres);
|
||||
n = 0;
|
||||
}
|
||||
}
|
||||
|
|
2
cmd/gateway/.gitignore
vendored
2
cmd/gateway/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
# Binary
|
||||
gateway
|
|
@ -1,5 +0,0 @@
|
|||
default: all
|
||||
all: gateway
|
||||
|
||||
gateway: gateway.go
|
||||
go build gateway.go
|
|
@ -1,5 +0,0 @@
|
|||
# meteo Gateway
|
||||
|
||||
The meteo-gateway is a program to read current measurements from a serial port and push them to a `meteo` webserver
|
||||
|
||||
The use case is to have a Rasperry-Pi with `Arduino Nano` Sensors connected via serial port.
|
|
@ -1,184 +0,0 @@
|
|||
/*
|
||||
* Meteo program to read out connected ServerNode and push the data via HTTP to
|
||||
* a given server
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/jacobsa/go-serial/serial"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Stations map[string]SerialStation `toml:"Serial"`
|
||||
}
|
||||
|
||||
type SerialStation struct {
|
||||
Device string
|
||||
Baud uint
|
||||
Remote string
|
||||
Token string
|
||||
}
|
||||
|
||||
type DataPoint struct {
|
||||
Station int
|
||||
Name string
|
||||
Temperature float64
|
||||
Humidity float64
|
||||
Pressure float64
|
||||
}
|
||||
|
||||
var cf Config
|
||||
|
||||
func parse(line string) (DataPoint, error) {
|
||||
dp := DataPoint{}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
return dp, nil
|
||||
}
|
||||
// XXX: Improve this: Espaced name
|
||||
split := strings.Split(line, " ")
|
||||
// Expected format: '1 "Meteo-Station" 2601 27 94366' (without '')
|
||||
if len(split) != 5 {
|
||||
return dp, errors.New("Illegal packet")
|
||||
}
|
||||
|
||||
var err error
|
||||
dp.Station, err = strconv.Atoi(split[0])
|
||||
if err != nil {
|
||||
return dp, err
|
||||
}
|
||||
dp.Name = split[1]
|
||||
dp.Temperature, err = strconv.ParseFloat(split[2], 10)
|
||||
if err != nil {
|
||||
return dp, err
|
||||
}
|
||||
dp.Temperature /= 100.0
|
||||
dp.Humidity, err = strconv.ParseFloat(split[3], 10)
|
||||
if err != nil {
|
||||
return dp, err
|
||||
}
|
||||
dp.Pressure, err = strconv.ParseFloat(split[4], 10)
|
||||
if err != nil {
|
||||
return dp, err
|
||||
}
|
||||
|
||||
return dp, nil
|
||||
}
|
||||
|
||||
func serialOpen(device string, baud uint) (io.ReadWriteCloser, error) {
|
||||
// Set up options.
|
||||
options := serial.OpenOptions{
|
||||
PortName: device,
|
||||
BaudRate: baud,
|
||||
DataBits: 8,
|
||||
StopBits: 1,
|
||||
MinimumReadSize: 4,
|
||||
}
|
||||
|
||||
// Open the port.
|
||||
return serial.Open(options)
|
||||
}
|
||||
|
||||
func handleSerial(station SerialStation) {
|
||||
fmt.Println(station.Remote)
|
||||
|
||||
serial, err := serialOpen(station.Device, station.Baud)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error opening %s: %s\n", station.Device, err)
|
||||
return
|
||||
}
|
||||
defer serial.Close()
|
||||
data := make([]byte, 1024)
|
||||
for { // Read continously
|
||||
n, err := serial.Read(data)
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data[:n]), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
dp, err := parse(line)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading: %s\n", err)
|
||||
} else {
|
||||
if dp.Station == 0 {
|
||||
continue
|
||||
} // Ignore, because invalid
|
||||
err := publish(dp, station.Remote, station.Token)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error publishing: %s\n", err)
|
||||
} else {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Configuration - set defaults and read from file
|
||||
toml.DecodeFile("/etc/meteo/gateway.toml", &cf)
|
||||
toml.DecodeFile("meteo.toml", &cf)
|
||||
toml.DecodeFile("meteo-gateway.toml", &cf)
|
||||
|
||||
if len(cf.Stations) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "No station defined\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create thread for each serial port
|
||||
var wg sync.WaitGroup
|
||||
for _, serial := range cf.Stations {
|
||||
wg.Add(1)
|
||||
go func(station SerialStation) {
|
||||
defer wg.Done()
|
||||
handleSerial(station)
|
||||
}(serial)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func publish(dp DataPoint, remote string, token string) error {
|
||||
// Build json packet
|
||||
json := fmt.Sprintf("{\"token\":\"%s\",\"T\":%.2f,\"Hum\":%.2f,\"P\":%.2f}", token, dp.Temperature, dp.Humidity, dp.Pressure)
|
||||
//fmt.Println(json)
|
||||
|
||||
// Prepare POST request
|
||||
hc := http.Client{}
|
||||
addr := fmt.Sprintf("%s/station/%d", remote, dp.Station)
|
||||
req, err := http.NewRequest("POST", addr, strings.NewReader(json))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
resp, err := hc.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if resp.StatusCode != 200 {
|
||||
fmt.Fprintf(os.Stderr, "POST returned status %d\n%s\n\n", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// All good
|
||||
return nil
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
client "github.com/influxdata/influxdb1-client"
|
||||
)
|
||||
|
||||
type InfluxDB struct {
|
||||
database string
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
func ConnectInfluxDB(remote string, username string, password string, database string) (InfluxDB, error) {
|
||||
var influx InfluxDB
|
||||
influx.database = database
|
||||
|
||||
host, err := url.Parse(remote)
|
||||
if err != nil {
|
||||
return influx, err
|
||||
}
|
||||
conf := client.Config{
|
||||
URL: *host,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
influx.client, err = client.NewClient(conf)
|
||||
return influx, err
|
||||
}
|
||||
|
||||
// Ping the InfluxDB server
|
||||
func (influx *InfluxDB) Ping() (time.Duration, string, error) {
|
||||
return influx.client.Ping()
|
||||
}
|
||||
|
||||
// Write a measurement into the database
|
||||
func (influx *InfluxDB) Write(measurement string, tags map[string]string, fields map[string]interface{}) error {
|
||||
point := client.Point{
|
||||
Measurement: measurement,
|
||||
Tags: tags,
|
||||
Fields: fields,
|
||||
//Time: time.Now(),
|
||||
Precision: "s",
|
||||
}
|
||||
pts := make([]client.Point, 0)
|
||||
pts = append(pts, point)
|
||||
bps := client.BatchPoints{
|
||||
Points: pts,
|
||||
Database: influx.database,
|
||||
//RetentionPolicy: "default",
|
||||
}
|
||||
_, err := influx.client.Write(bps)
|
||||
return err
|
||||
}
|
|
@ -1,267 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
// Config is the internal singleton configuration for this program
|
||||
type Config struct {
|
||||
MqttHost string `ini:"mqtt,remote"`
|
||||
InfluxHost string `ini:"influxdb,remote"`
|
||||
InfluxUsername string `ini:"influxdb,username"`
|
||||
InfluxPassword string `ini:"influxdb,password"`
|
||||
InfluxDatabase string `ini:"influxdb,database"`
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
func (c *Config) loadIni(filename string) error {
|
||||
cfg, err := ini.Load(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mqtt := cfg.Section("mqtt")
|
||||
mqtthost := mqtt.Key("remote").String()
|
||||
if mqtthost != "" {
|
||||
c.MqttHost = mqtthost
|
||||
}
|
||||
influx := cfg.Section("influxdb")
|
||||
influxhost := influx.Key("remote").String()
|
||||
if influxhost != "" {
|
||||
c.InfluxHost = influxhost
|
||||
}
|
||||
influxuser := influx.Key("username").String()
|
||||
if influxuser != "" {
|
||||
c.InfluxUsername = influxuser
|
||||
}
|
||||
influxpass := influx.Key("password").String()
|
||||
if influxpass != "" {
|
||||
c.InfluxPassword = influxpass
|
||||
}
|
||||
influxdb := influx.Key("database").String()
|
||||
if influxdb != "" {
|
||||
c.InfluxDatabase = influxdb
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var config Config
|
||||
|
||||
var influx InfluxDB
|
||||
|
||||
func assembleJson(node int, data map[string]interface{}) string {
|
||||
ret := fmt.Sprintf("node %d: {", node)
|
||||
first := true
|
||||
for k, v := range data {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
ret += ", "
|
||||
}
|
||||
ret += fmt.Sprintf("\"%s\":%f", k, v)
|
||||
}
|
||||
ret += "}"
|
||||
return ret
|
||||
}
|
||||
|
||||
func received(msg mqtt.Message) {
|
||||
data := make(map[string]interface{}, 0)
|
||||
if err := json.Unmarshal(msg.Payload(), &data); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "json unmarshall error: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// We don't log the name, remove it, if present
|
||||
if _, ok := data["name"]; ok {
|
||||
delete(data, "name")
|
||||
}
|
||||
// ID is taken from the topic
|
||||
if _, ok := data["id"]; ok {
|
||||
delete(data, "id")
|
||||
}
|
||||
|
||||
// Parse node ID from topic
|
||||
nodeID, err := strconv.Atoi(msg.Topic()[6:])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid meteo id\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Write to InfluxDB
|
||||
for k, v := range data {
|
||||
tags := map[string]string{"node": fmt.Sprintf("%d", nodeID)}
|
||||
f, err := strconv.ParseFloat(fmt.Sprintf("%f", v), 32)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "non-float value received: %s\n", err)
|
||||
return
|
||||
}
|
||||
fields := map[string]interface{}{"value": f}
|
||||
if err := influx.Write(k, tags, fields); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cannot write to influxdb: %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// OK
|
||||
if config.Verbose {
|
||||
fmt.Println(assembleJson(nodeID, data))
|
||||
}
|
||||
}
|
||||
|
||||
// awaits SIGINT or SIGTERM
|
||||
func awaitTerminationSignal() {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
done := make(chan bool, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
sig := <-sigs
|
||||
fmt.Println(sig)
|
||||
done <- true
|
||||
}()
|
||||
<-done
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println("meteo-influxdb-gateway")
|
||||
fmt.Printf("Usage: %s [OPTIONS]\n", os.Args[0])
|
||||
fmt.Println("OPTIONS")
|
||||
fmt.Println(" -h,--help Display this help message")
|
||||
fmt.Println(" -c,--config FILE Load config file")
|
||||
fmt.Println(" -v, --verbose Verbose output")
|
||||
fmt.Println(" --mqtt MQTT Set mqtt server")
|
||||
fmt.Println(" --influx HOST Set influxdb hostname")
|
||||
fmt.Println(" --username USER Set influxdb username")
|
||||
fmt.Println(" --password PASS Set influxdb password")
|
||||
fmt.Println(" --database DB Set influxdb database")
|
||||
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
// Default settings
|
||||
config.MqttHost = "127.0.0.1"
|
||||
config.InfluxHost = "http://127.0.0.1:8086"
|
||||
config.InfluxUsername = "meteo"
|
||||
config.InfluxPassword = ""
|
||||
config.InfluxDatabase = "meteo"
|
||||
config.Verbose = false
|
||||
|
||||
configFile := "/etc/meteo/meteo-influx-gateway.ini"
|
||||
if fileExists(configFile) {
|
||||
if err := config.loadIni(configFile); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error loading ini file %s: %s\n", configFile, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
args := os.Args[1:]
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := strings.TrimSpace(args[i])
|
||||
if arg == "" {
|
||||
continue
|
||||
} else if arg[0] == '-' {
|
||||
last := i >= len(args)-1
|
||||
if arg == "-h" || arg == "--help" {
|
||||
printUsage()
|
||||
return
|
||||
} else if arg == "-c" || arg == "--config" {
|
||||
if last {
|
||||
fmt.Fprintf(os.Stderr, "Missing argument: config file\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
i++
|
||||
configFile = args[i]
|
||||
if err := config.loadIni(configFile); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error loading ini file %s: %s\n", configFile, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else if arg == "-v" || arg == "--verbose" {
|
||||
config.Verbose = true
|
||||
} else if arg == "--mqtt" {
|
||||
if last {
|
||||
fmt.Fprintf(os.Stderr, "Missing argument: mqtt remote\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
i++
|
||||
config.MqttHost = args[i]
|
||||
} else if arg == "--influx" {
|
||||
if last {
|
||||
fmt.Fprintf(os.Stderr, "Missing argument: influx remote\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
i++
|
||||
config.InfluxHost = args[i]
|
||||
} else if arg == "--username" {
|
||||
if last {
|
||||
fmt.Fprintf(os.Stderr, "Missing argument: influx username\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
i++
|
||||
config.InfluxUsername = args[i]
|
||||
} else if arg == "--password" {
|
||||
if last {
|
||||
fmt.Fprintf(os.Stderr, "Missing argument: influx password\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
i++
|
||||
config.InfluxPassword = args[i]
|
||||
} else if arg == "--database" {
|
||||
if last {
|
||||
fmt.Fprintf(os.Stderr, "Missing argument: influx database\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
i++
|
||||
config.InfluxDatabase = args[i]
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Invalid argument: %s\n", arg)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect InfluxDB
|
||||
influx, err = ConnectInfluxDB(config.InfluxHost, config.InfluxUsername, config.InfluxPassword, config.InfluxDatabase)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cannot connect to influxdb: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if ping, version, err := influx.Ping(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cannot ping influxdb: %s\n", err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
if config.Verbose {
|
||||
fmt.Printf("influxdb connected: v%s - Ping: %d ms\n", version, ping.Milliseconds())
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to mqtt server
|
||||
if mqtt, err := ConnectMQTT(config.MqttHost, 1883); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "mqtt error: %s\n", err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
mqtt.Subscribe("meteo/#", received)
|
||||
if config.Verbose {
|
||||
fmt.Println("mqtt connected: " + config.MqttHost)
|
||||
}
|
||||
}
|
||||
fmt.Println("meteo-mqtt-influx gateway is up and running")
|
||||
|
||||
awaitTerminationSignal()
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
type MqttReceive func(msg mqtt.Message)
|
||||
|
||||
type MQTT struct {
|
||||
client mqtt.Client
|
||||
}
|
||||
|
||||
func ConnectMQTT(remote string, port int) (MQTT, error) {
|
||||
var ret MQTT
|
||||
opts := mqtt.NewClientOptions()
|
||||
remote = fmt.Sprintf("tcp://%s:%d", remote, port)
|
||||
opts.AddBroker(remote)
|
||||
opts.AutoReconnect = true
|
||||
|
||||
ret.client = mqtt.NewClient(opts)
|
||||
token := ret.client.Connect()
|
||||
for !token.WaitTimeout(5 * time.Second) {
|
||||
}
|
||||
return ret, token.Error()
|
||||
}
|
||||
|
||||
func (mq *MQTT) Subscribe(topic string, callback MqttReceive) error {
|
||||
token := mq.client.Subscribe(topic, 0, func(client mqtt.Client, msg mqtt.Message) {
|
||||
callback(msg)
|
||||
})
|
||||
token.Wait()
|
||||
return token.Error()
|
||||
}
|
|
@ -1,297 +0,0 @@
|
|||
/*
|
||||
* Simple CLI meteo client
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
// Terminal color codes
|
||||
const KNRM = "\x1B[0m"
|
||||
const KRED = "\x1B[31m"
|
||||
const KGRN = "\x1B[32m"
|
||||
const KYEL = "\x1B[33m"
|
||||
const KBLU = "\x1B[34m"
|
||||
const KMAG = "\x1B[35m"
|
||||
const KCYN = "\x1B[36m"
|
||||
const KWHT = "\x1B[37m"
|
||||
|
||||
type LocalStation struct {
|
||||
Id int
|
||||
Name string
|
||||
Timestamp int64
|
||||
Temperature float32
|
||||
Humidity float32
|
||||
Pressure float32
|
||||
}
|
||||
|
||||
type StationMeta struct {
|
||||
Id int
|
||||
Name string
|
||||
Location string
|
||||
Description string
|
||||
}
|
||||
|
||||
/* Warning and error ranges */
|
||||
type Ranges struct {
|
||||
T_Range []float32 `toml:"temp_range"`
|
||||
Hum_Range []float32 `toml:"hum_range"`
|
||||
//P_Range []float32 `toml:"p_range"`
|
||||
}
|
||||
|
||||
type tomlClientConfig struct {
|
||||
DefaultRemote string `toml:"DefaultRemote"`
|
||||
|
||||
Remotes map[string]Remote `toml:"Remotes"`
|
||||
StationRanges map[string]Ranges `toml:"Ranges"`
|
||||
}
|
||||
|
||||
type Remote struct {
|
||||
Remote string
|
||||
}
|
||||
|
||||
func httpGet(url string) ([]byte, error) {
|
||||
ret := make([]byte, 0)
|
||||
if len(url) == 0 {
|
||||
return ret, errors.New("Empty URL")
|
||||
}
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
if !strings.HasPrefix(resp.Status, "200 ") {
|
||||
return ret, errors.New(fmt.Sprintf("Http error %s", resp.Status))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
return body, err
|
||||
}
|
||||
|
||||
func GetStations(baseUrl string) (map[int]StationMeta, error) {
|
||||
ret := make(map[int]StationMeta, 0)
|
||||
url := baseUrl + "/stations"
|
||||
body, err := httpGet(url)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
response := strings.TrimSpace(string(body))
|
||||
for _, line := range strings.Split(response, "\n") {
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
params := strings.Split(line, ",")
|
||||
if len(params) == 4 {
|
||||
var err error
|
||||
station := StationMeta{}
|
||||
station.Id, err = strconv.Atoi(params[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
station.Name = params[1]
|
||||
station.Location = params[2]
|
||||
station.Description = params[3]
|
||||
ret[station.Id] = station
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func request(hostname string) ([]LocalStation, error) {
|
||||
ret := make([]LocalStation, 0)
|
||||
|
||||
url := hostname
|
||||
|
||||
// Add https if nothing is there
|
||||
autoHttps := false
|
||||
if !strings.Contains(hostname, "://") {
|
||||
autoHttps = true
|
||||
url = "https://" + hostname + "/meteo"
|
||||
}
|
||||
if len(url) == 0 {
|
||||
return ret, errors.New("Empty URL")
|
||||
}
|
||||
if url[len(url)-1] == '/' {
|
||||
url = url[:len(url)-1]
|
||||
}
|
||||
|
||||
body, err := httpGet(url + "/current")
|
||||
if err != nil {
|
||||
// Try with http instead of https, if we have added https automatically
|
||||
if autoHttps {
|
||||
//fmt.Fprintln(os.Stderr, " # Warning: Fallback to unencrypted http!")
|
||||
url = strings.Replace(url, "https://", "http://", 1)
|
||||
body, err = httpGet(url + "/current")
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
} else {
|
||||
return ret, err
|
||||
}
|
||||
}
|
||||
|
||||
stations, _ := GetStations(url) // Ignore errors here
|
||||
|
||||
response := strings.TrimSpace(string(body))
|
||||
// Parse response lines
|
||||
for _, line := range strings.Split(response, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
if line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
params := strings.Split(line, ",")
|
||||
if len(params) == 5 {
|
||||
station := LocalStation{}
|
||||
var err error
|
||||
var f float64
|
||||
station.Id, err = strconv.Atoi(params[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
station.Timestamp, err = strconv.ParseInt(params[1], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
f, err = strconv.ParseFloat(params[2], 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
station.Temperature = float32(f)
|
||||
f, err = strconv.ParseFloat(params[3], 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
station.Humidity = float32(f)
|
||||
f, err = strconv.ParseFloat(params[4], 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
station.Pressure = float32(f)
|
||||
station.Name = stations[station.Id].Name
|
||||
ret = append(ret, station)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func printHelp() {
|
||||
fmt.Printf("Usage: %s [OPTIONS] REMOTE [REMOTE] ...\n", os.Args[0])
|
||||
fmt.Println(" REMOTE defines a running meteo webserver")
|
||||
fmt.Printf(" e.g. %s http://meteo.local/\n", os.Args[0])
|
||||
fmt.Printf(" %s https://monitor-server.local/meteo/\n", os.Args[0])
|
||||
fmt.Printf("\n")
|
||||
fmt.Printf("OPTIONS\n")
|
||||
fmt.Printf(" -h, --help Print this help message\n")
|
||||
fmt.Printf("\n")
|
||||
fmt.Println("https://github.com/grisu48/meteo")
|
||||
}
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
hosts := make([]string, 0)
|
||||
|
||||
// Read configuration
|
||||
var cf tomlClientConfig
|
||||
toml.DecodeFile("/etc/meteo.toml", &cf)
|
||||
toml.DecodeFile("meteo.toml", &cf)
|
||||
|
||||
// Parse arguments
|
||||
for _, arg := range args {
|
||||
if arg == "" {
|
||||
continue
|
||||
}
|
||||
if arg[0] == '-' {
|
||||
// Check for special parameters
|
||||
if arg == "-h" || arg == "--help" {
|
||||
printHelp()
|
||||
os.Exit(0)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Illegal argument: %s\n", arg)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
hosts = append(hosts, arg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
if cf.DefaultRemote == "" {
|
||||
printHelp()
|
||||
os.Exit(1)
|
||||
} else {
|
||||
hosts = append(hosts, cf.DefaultRemote)
|
||||
}
|
||||
}
|
||||
|
||||
for _, hostname := range hosts {
|
||||
|
||||
if val, ok := cf.Remotes[hostname]; ok {
|
||||
hostname = val.Remote
|
||||
}
|
||||
|
||||
fmt.Println(hostname)
|
||||
stations, err := request(hostname)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||
}
|
||||
for _, station := range stations {
|
||||
timestamp := time.Unix(station.Timestamp, 0)
|
||||
|
||||
/* ==== Building the line with colors ==== */
|
||||
// Yes, this has become a bit messy
|
||||
fmt.Printf(" * %3d %-22s ", station.Id, station.Name)
|
||||
// Set color for time
|
||||
if time.Since(timestamp).Minutes() > 5 {
|
||||
fmt.Printf(KRED)
|
||||
} else {
|
||||
fmt.Printf(KGRN)
|
||||
}
|
||||
fmt.Printf("%19s", timestamp.Format("2006-01-02-15:04:05"))
|
||||
fmt.Printf(KNRM)
|
||||
fmt.Printf(" ")
|
||||
// Check if we have some custom ranges
|
||||
if val, ok := cf.StationRanges[station.Name]; ok {
|
||||
if len(val.T_Range) == 4 {
|
||||
if station.Temperature <= val.T_Range[0] || station.Temperature >= val.T_Range[3] {
|
||||
fmt.Printf(KRED)
|
||||
} else if station.Temperature <= val.T_Range[1] || station.Temperature >= val.T_Range[2] {
|
||||
fmt.Printf(KYEL)
|
||||
} else {
|
||||
fmt.Printf(KGRN)
|
||||
}
|
||||
}
|
||||
fmt.Printf("%5.2fC", station.Temperature)
|
||||
fmt.Printf(KNRM)
|
||||
fmt.Printf("|")
|
||||
if len(val.Hum_Range) == 4 {
|
||||
if station.Humidity <= val.Hum_Range[0] || station.Humidity >= val.Hum_Range[3] {
|
||||
fmt.Printf(KRED)
|
||||
} else if station.Humidity <= val.Hum_Range[1] || station.Humidity >= val.Hum_Range[2] {
|
||||
fmt.Printf(KYEL)
|
||||
} else {
|
||||
fmt.Printf(KGRN)
|
||||
}
|
||||
}
|
||||
fmt.Printf("%5.2f %%rel", station.Humidity)
|
||||
fmt.Printf(KNRM)
|
||||
fmt.Printf("|%6.0fhPa\n", station.Pressure)
|
||||
} else {
|
||||
fmt.Printf("%5.2fC|%5.2f %%rel|%6.0fhPa\n", station.Temperature, station.Humidity, station.Pressure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
48
cmd/meteod/influxdb.go
Normal file
48
cmd/meteod/influxdb.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||
)
|
||||
|
||||
type InfluxDB struct {
|
||||
client influxdb2.Client
|
||||
}
|
||||
|
||||
func ConnectInfluxDB(remote string, token string) (InfluxDB, error) {
|
||||
var influx InfluxDB
|
||||
influx.client = influxdb2.NewClient(remote, token)
|
||||
return influx, nil
|
||||
}
|
||||
|
||||
func (influx *InfluxDB) Ping(timeout time.Duration) (time.Duration, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
now := time.Now()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// timeout
|
||||
return time.Duration(0), fmt.Errorf("timeout")
|
||||
default:
|
||||
succ, err := influx.client.Ping(ctx)
|
||||
delta := time.Now().Sub(now)
|
||||
if err != nil {
|
||||
return delta, err
|
||||
}
|
||||
if !succ {
|
||||
return delta, fmt.Errorf("no reply from server")
|
||||
}
|
||||
return delta, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Write a measurement into the database
|
||||
func (influx *InfluxDB) Write(org, bucket, measurement string, tags map[string]string, fields map[string]interface{}) error {
|
||||
writeAPI := influx.client.WriteAPIBlocking(org, bucket)
|
||||
p := influxdb2.NewPoint(measurement, tags, fields, time.Now())
|
||||
err := writeAPI.WritePoint(context.Background(), p)
|
||||
return err
|
||||
}
|
1747
cmd/meteod/meteod.go
1747
cmd/meteod/meteod.go
File diff suppressed because it is too large
Load diff
70
cmd/meteod/mqtt.go
Normal file
70
cmd/meteod/mqtt.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
// MqttReceive is the message receive callback
|
||||
type MqttReceive func(msg mqtt.Message)
|
||||
|
||||
// MQTT client structure
|
||||
type MQTT struct {
|
||||
client mqtt.Client
|
||||
address string
|
||||
port int
|
||||
ClientID string
|
||||
Verbose bool
|
||||
Callback MqttReceive
|
||||
}
|
||||
|
||||
// Connect connects the MQTT instance to the given server
|
||||
func (mq *MQTT) Connect(address string, port int) error {
|
||||
mq.address = address
|
||||
mq.port = port
|
||||
opts := mqtt.NewClientOptions()
|
||||
opts.AddBroker(fmt.Sprintf("tcp://%s:%d", address, port))
|
||||
opts.KeepAlive = 30
|
||||
opts.AutoReconnect = true
|
||||
opts.ClientID = mq.ClientID
|
||||
opts.ResumeSubs = true
|
||||
opts.OnConnect = func(client mqtt.Client) {
|
||||
if mq.Verbose {
|
||||
fmt.Printf("mqtt connected: %s:%d\n", mq.address, mq.port)
|
||||
}
|
||||
if err := mq.subscribe("meteo/#", mq.Callback); err != nil {
|
||||
if mq.Verbose {
|
||||
fmt.Fprintf(os.Stderr, "Error subscribing to mqtt topic: %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
opts.OnReconnecting = func(client mqtt.Client, opts *mqtt.ClientOptions) {
|
||||
if mq.Verbose {
|
||||
fmt.Println("mqtt reconnecting")
|
||||
}
|
||||
}
|
||||
|
||||
opts.OnConnectionLost = func(client mqtt.Client, err error) {
|
||||
if mq.Verbose {
|
||||
fmt.Fprintf(os.Stderr, "mqtt connection lost: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
mq.client = mqtt.NewClient(opts)
|
||||
token := mq.client.Connect()
|
||||
for !token.WaitTimeout(30 * time.Second) {
|
||||
}
|
||||
return token.Error()
|
||||
}
|
||||
|
||||
// Subscribe to a given topic with the given callback function
|
||||
func (mq *MQTT) subscribe(topic string, callback MqttReceive) error {
|
||||
token := mq.client.Subscribe(topic, 0, func(client mqtt.Client, msg mqtt.Message) {
|
||||
callback(msg)
|
||||
})
|
||||
token.Wait()
|
||||
return token.Error()
|
||||
}
|
11
go.mod
11
go.mod
|
@ -1,15 +1,10 @@
|
|||
module github.com/grisu48/meteo
|
||||
|
||||
go 1.11
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/eclipse/paho.mqtt.golang v1.2.0
|
||||
github.com/gorilla/mux v1.7.4
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab
|
||||
github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/eclipse/paho.mqtt.golang v1.3.5
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.12.1
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0
|
||||
)
|
||||
|
|
106
go.sum
106
go.sum
|
@ -1,30 +1,106 @@
|
|||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0=
|
||||
github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts=
|
||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU=
|
||||
github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y=
|
||||
github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=
|
||||
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4 h1:G2ztCwXov8mRvP0ZfjE6nAlaCX2XbykaeHdbT6KwDz0=
|
||||
github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4/go.mod h1:2RvX5ZjVtsznNZPEt4xwJXNJrM3VTZoQf7V6gk0ysvs=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.12.1 h1:RrjoDNyBGFYvjKfjmtIyYAn6GY/SrtocSo4RPlt+Lng=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.12.1/go.mod h1:YteV91FiQxRdccyJ2cHvj2f/5sq4y4Njqu1fQzsQCOU=
|
||||
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
|
||||
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -1,506 +0,0 @@
|
|||
package meteo
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
/* Basic datapoint */
|
||||
type DataPoint struct {
|
||||
Timestamp int64
|
||||
Station int
|
||||
Temperature float32
|
||||
Humidity float32
|
||||
Pressure float32
|
||||
}
|
||||
|
||||
/* Station */
|
||||
type Station struct {
|
||||
Id int
|
||||
Name string
|
||||
Location string
|
||||
Description string
|
||||
}
|
||||
|
||||
/* Ombrometer station reading */
|
||||
type Rain struct {
|
||||
Station int
|
||||
Timestamp int64
|
||||
Millimeters float32
|
||||
}
|
||||
|
||||
type Persistence struct {
|
||||
con *sql.DB
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
Token string
|
||||
Station int
|
||||
}
|
||||
|
||||
type Lightning struct {
|
||||
Station int
|
||||
Timestamp int64
|
||||
Distance float32
|
||||
}
|
||||
|
||||
func OpenDb(filename string) (Persistence, error) {
|
||||
db := Persistence{}
|
||||
con, err := sql.Open("sqlite3", filename)
|
||||
if err != nil {
|
||||
return db, err
|
||||
}
|
||||
db.con = con
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (db *Persistence) Close() {
|
||||
db.con.Close()
|
||||
}
|
||||
|
||||
/** Get the highest id from the given table */
|
||||
func (db *Persistence) getHighestId(table string, id string) (int, error) {
|
||||
rows, err := db.con.Query("SELECT `" + id + "` FROM `" + table + "` ORDER BY `" + id + "` DESC LIMIT 1")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
if rows.Next() {
|
||||
var uid int
|
||||
rows.Scan(&uid)
|
||||
return uid, nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
/* Prepare database, i.e. create tables ecc.
|
||||
*/
|
||||
func (db *Persistence) Prepare() error {
|
||||
_, err := db.con.Exec("CREATE TABLE IF NOT EXISTS `stations` (`id` INT PRIMARY KEY, `name` VARCHAR(64), `location` TEXT, `description` TEXT);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.con.Exec("CREATE TABLE IF NOT EXISTS `tokens` (`token` VARCHAR(32) PRIMARY KEY, `station` INT);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.con.Exec("CREATE TABLE IF NOT EXISTS `lightnings` (`station` INT, `timestamp` INT, `distance` REAL, PRIMARY KEY(`station`,`timestamp`));")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.con.Exec("CREATE TABLE IF NOT EXISTS `ombrometers` (`id` INT PRIMARY KEY, `name` VARCHAR(64), `location` TEXT, `description` TEXT);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/** Get the station, the token is assigned to. Returns Token.Station = 0 if not token is found */
|
||||
func (db *Persistence) GetToken(token string) (Token, error) {
|
||||
ret := Token{Station: 0}
|
||||
stmt, err := db.con.Prepare("SELECT `token`,`station` FROM `tokens` WHERE `token` = ? LIMIT 1")
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.Query(token)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
defer rows.Close()
|
||||
if rows.Next() {
|
||||
rows.Scan(&ret.Token, &ret.Station)
|
||||
return ret, nil
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (db *Persistence) GetStationTokens(station int) ([]Token, error) {
|
||||
ret := make([]Token, 0)
|
||||
stmt, err := db.con.Prepare("SELECT `token`,`station` FROM `tokens` WHERE `station` = ? LIMIT 1")
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.Query(station)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
tok := Token{}
|
||||
rows.Scan(&tok.Token, &tok.Station)
|
||||
ret = append(ret, tok)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (db *Persistence) InsertToken(token Token) error {
|
||||
stmt, err := db.con.Prepare("INSERT OR IGNORE INTO `tokens` (`token`,`station`) VALUES (?,?);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(token.Token, token.Station)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Persistence) RemoveToken(token Token) error {
|
||||
stmt, err := db.con.Prepare("DELETE FROM `tokens` WHERE `token` = ?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(token.Token)
|
||||
return err
|
||||
}
|
||||
|
||||
/** Inserts the given station and assign the ID to the given station parameter */
|
||||
func (db *Persistence) InsertStation(station *Station) error {
|
||||
id := station.Id
|
||||
var err error
|
||||
|
||||
if id == 0 {
|
||||
id, err = db.getHighestId("stations", "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id += 1
|
||||
}
|
||||
|
||||
// Create table
|
||||
tablename := "station_" + strconv.Itoa(id)
|
||||
_, err = db.con.Exec("CREATE TABLE IF NOT EXISTS `" + tablename + "` (`timestamp` INT PRIMARY KEY, `temperature` FLOAT, `humidity` FLOAT, `pressure` FLOAT);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stmt, err := db.con.Prepare("INSERT INTO `stations` (`id`, `name`, `location`, `description`) VALUES (?,?,?,?);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(id, station.Name, station.Location, station.Description)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
station.Id = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Persistence) GetStations() ([]Station, error) {
|
||||
stations := make([]Station, 0)
|
||||
|
||||
rows, err := db.con.Query("SELECT `id`,`name`,`location`,`description` FROM `stations`")
|
||||
if err != nil {
|
||||
return stations, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
station := Station{}
|
||||
rows.Scan(&station.Id, &station.Name, &station.Location, &station.Description)
|
||||
stations = append(stations, station)
|
||||
}
|
||||
|
||||
return stations, nil
|
||||
}
|
||||
|
||||
func (db *Persistence) ExistsStation(id int) (bool, error) {
|
||||
stmt, err := db.con.Prepare("SELECT `id` FROM `stations` WHERE `id` = ? LIMIT 1")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.Query(id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return rows.Next(), nil
|
||||
}
|
||||
|
||||
func (db *Persistence) GetStation(id int) (Station, error) {
|
||||
station := Station{}
|
||||
|
||||
stmt, err := db.con.Prepare("SELECT `id`,`name`,`location`,`description` FROM `stations` WHERE `id` = ? LIMIT 1")
|
||||
if err != nil {
|
||||
return station, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.Query(id)
|
||||
if err != nil {
|
||||
return station, err
|
||||
}
|
||||
defer rows.Close()
|
||||
if rows.Next() {
|
||||
station := Station{}
|
||||
rows.Scan(&station.Id, &station.Name, &station.Location, &station.Description)
|
||||
return station, nil
|
||||
} else {
|
||||
station.Id = 0
|
||||
return station, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Persistence) UpdateStation(station Station) error {
|
||||
stmt, err := db.con.Prepare("UPDATE `stations` SET `name`=?,`location`=?,`description`=? WHERE `id` = ?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(station.Name, station.Location, station.Description, station.Id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Persistence) GetLastDataPoints(station int, limit int) ([]DataPoint, error) {
|
||||
datapoints := make([]DataPoint, 0)
|
||||
tablename := "station_" + strconv.Itoa(station)
|
||||
stmt, err := db.con.Prepare("SELECT `timestamp`,`temperature`,`humidity`,`pressure` FROM `" + tablename + "` ORDER BY `timestamp` DESC LIMIT ?")
|
||||
if err != nil {
|
||||
return datapoints, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.Query(limit)
|
||||
if err != nil {
|
||||
return datapoints, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
datapoint := DataPoint{}
|
||||
rows.Scan(&datapoint.Timestamp, &datapoint.Temperature, &datapoint.Humidity, &datapoint.Pressure)
|
||||
datapoint.Station = station
|
||||
datapoints = append(datapoints, datapoint)
|
||||
}
|
||||
|
||||
return datapoints, nil
|
||||
}
|
||||
|
||||
/** Query given station within the given timespan. If limit <=0, the limit is set to 100000 */
|
||||
func (db *Persistence) QueryStation(station int, t_min int64, t_max int64, limit int64, offset int64) ([]DataPoint, error) {
|
||||
datapoints := make([]DataPoint, 0)
|
||||
tablename := "station_" + strconv.Itoa(station)
|
||||
sql := "SELECT `timestamp`,`temperature`,`humidity`,`pressure` FROM `" + tablename + "` WHERE `timestamp` >= ? AND `timestamp` <= ? ORDER BY `timestamp` ASC LIMIT ? OFFSET ?"
|
||||
stmt, err := db.con.Prepare(sql)
|
||||
if err != nil {
|
||||
return datapoints, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
if limit <= 0 {
|
||||
limit = 100000
|
||||
}
|
||||
rows, err := stmt.Query(t_min, t_max, limit, offset)
|
||||
if err != nil {
|
||||
return datapoints, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
datapoint := DataPoint{}
|
||||
rows.Scan(&datapoint.Timestamp, &datapoint.Temperature, &datapoint.Humidity, &datapoint.Pressure)
|
||||
datapoint.Station = station
|
||||
datapoints = append(datapoints, datapoint)
|
||||
}
|
||||
|
||||
return datapoints, nil
|
||||
}
|
||||
|
||||
/** Inserts the given datapoint to the database */
|
||||
func (db *Persistence) InsertDataPoint(dp DataPoint) error {
|
||||
tablename := "station_" + strconv.Itoa(dp.Station)
|
||||
stmt, err := db.con.Prepare("INSERT OR REPLACE INTO `" + tablename + "` (`timestamp`,`temperature`,`humidity`,`pressure`) VALUES (?,?,?,?);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(dp.Timestamp, dp.Temperature, dp.Humidity, dp.Pressure)
|
||||
return err
|
||||
}
|
||||
|
||||
/** Inserts the given lightning to the database */
|
||||
func (db *Persistence) InsertLightning(light Lightning) error {
|
||||
stmt, err := db.con.Prepare("INSERT OR IGNORE INTO `lightnings` (`station`,`timestamp`,`distance`) VALUES (?,?,?);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(light.Station, light.Timestamp, light.Distance)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Persistence) GetLightnings(limit int, offset int) ([]Lightning, error) {
|
||||
ret := make([]Lightning, 0)
|
||||
stmt, err := db.con.Prepare("SELECT `station`,`timestamp`,`distance` FROM `lightnings` ORDER BY `timestamp` DESC LIMIT ? OFFSET ?")
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.Query(limit, offset)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
lightning := Lightning{}
|
||||
rows.Scan(&lightning.Station, &lightning.Timestamp, &lightning.Distance)
|
||||
ret = append(ret, lightning)
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (db *Persistence) GetOmbrometers() ([]Station, error) {
|
||||
stations := make([]Station, 0)
|
||||
rows, err := db.con.Query("SELECT `id`,`name`,`location`,`description` FROM `ombrometers`")
|
||||
if err != nil {
|
||||
return stations, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
station := Station{}
|
||||
rows.Scan(&station.Id, &station.Name, &station.Location, &station.Description)
|
||||
stations = append(stations, station)
|
||||
}
|
||||
|
||||
return stations, nil
|
||||
}
|
||||
|
||||
/** Inserts the given ombrometer station and assign the ID to the given station parameter */
|
||||
func (db *Persistence) InsertOmbrometer(station *Station) error {
|
||||
id := station.Id
|
||||
var err error
|
||||
|
||||
if id == 0 {
|
||||
id, err = db.getHighestId("ombrometers", "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id++
|
||||
}
|
||||
|
||||
// Create table
|
||||
tablename := "ombrometer_" + strconv.Itoa(id)
|
||||
_, err = db.con.Exec("CREATE TABLE IF NOT EXISTS `" + tablename + "` (`timestamp` INT PRIMARY KEY, `millimeter` FLOAT);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stmt, err := db.con.Prepare("INSERT INTO `ombrometers` (`id`, `name`, `location`, `description`) VALUES (?,?,?,?);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(id, station.Name, station.Location, station.Description)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
station.Id = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Persistence) UpdateOmbrometer(station Station) error {
|
||||
stmt, err := db.con.Prepare("UPDATE `ombrometers` SET `name`=?,`location`=?,`description`=? WHERE `id` = ?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(station.Name, station.Location, station.Description, station.Id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Persistence) GetOmbrometer(id int) (Station, error) {
|
||||
station := Station{}
|
||||
|
||||
stmt, err := db.con.Prepare("SELECT `id`,`name`,`location`,`description` FROM `ombrometers` WHERE `id` = ? LIMIT 1")
|
||||
if err != nil {
|
||||
return station, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.Query(id)
|
||||
if err != nil {
|
||||
return station, err
|
||||
}
|
||||
defer rows.Close()
|
||||
if rows.Next() {
|
||||
station := Station{}
|
||||
rows.Scan(&station.Id, &station.Name, &station.Location, &station.Description)
|
||||
return station, nil
|
||||
} else {
|
||||
station.Id = 0
|
||||
return station, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetOmbrometerReadings returns the most recent readings from the given station
|
||||
func (db *Persistence) GetOmbrometerReadings(station int, limit int) ([]Rain, error) {
|
||||
datapoints := make([]Rain, 0)
|
||||
tablename := "ombrometer_" + strconv.Itoa(station)
|
||||
stmt, err := db.con.Prepare("SELECT `timestamp`,`millimeter` FROM `" + tablename + "` ORDER BY `timestamp` DESC LIMIT ?")
|
||||
if err != nil {
|
||||
return datapoints, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.Query(limit)
|
||||
if err != nil {
|
||||
return datapoints, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
datapoint := Rain{}
|
||||
rows.Scan(&datapoint.Timestamp, &datapoint.Millimeters)
|
||||
datapoint.Station = station
|
||||
datapoints = append(datapoints, datapoint)
|
||||
}
|
||||
|
||||
return datapoints, nil
|
||||
}
|
||||
|
||||
// GetOmbrometerLastReading returns the last reading of the given station or an empty new Rain object
|
||||
func (db *Persistence) GetOmbrometerLastReading(station int) (Rain, error) {
|
||||
dps, err := db.GetOmbrometerReadings(station, 1)
|
||||
if err != nil || len(dps) == 0 {
|
||||
return Rain{Station: station}, err
|
||||
} else {
|
||||
return dps[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
// QueryOmbrometer queries the given station within the given timespan. If limit <=0, the limit is set to 100000
|
||||
func (db *Persistence) QueryOmbrometer(station int, t_min int64, t_max int64, limit int64, offset int64) ([]Rain, error) {
|
||||
datapoints := make([]Rain, 0)
|
||||
tablename := "ombrometer_" + strconv.Itoa(station)
|
||||
sql := "SELECT `timestamp`,`millimeter` FROM `" + tablename + "` WHERE `timestamp` >= ? AND `timestamp` <= ? ORDER BY `timestamp` ASC LIMIT ? OFFSET ?"
|
||||
stmt, err := db.con.Prepare(sql)
|
||||
if err != nil {
|
||||
return datapoints, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
if limit <= 0 {
|
||||
limit = 100000
|
||||
}
|
||||
rows, err := stmt.Query(t_min, t_max, limit, offset)
|
||||
if err != nil {
|
||||
return datapoints, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
datapoint := Rain{}
|
||||
rows.Scan(&datapoint.Timestamp, &datapoint.Millimeters)
|
||||
datapoint.Station = station
|
||||
datapoints = append(datapoints, datapoint)
|
||||
}
|
||||
|
||||
return datapoints, nil
|
||||
}
|
||||
|
||||
// InsertRainMeasurement inserts a single rain measurement into the database
|
||||
func (db *Persistence) InsertRainMeasurement(dp Rain) error {
|
||||
tablename := "ombrometer_" + strconv.Itoa(dp.Station)
|
||||
stmt, err := db.con.Prepare("INSERT OR REPLACE INTO `" + tablename + "` (`timestamp`,`millimeter`) VALUES (?,?);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(dp.Timestamp, dp.Millimeters)
|
||||
return err
|
||||
}
|
|
@ -1,681 +0,0 @@
|
|||
package meteo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var testDatabase = "_test_meteod_.db"
|
||||
var db Persistence
|
||||
|
||||
func randomInt(min int, max int) int {
|
||||
if min > max {
|
||||
return randomInt(max, min)
|
||||
}
|
||||
if min == max {
|
||||
return min
|
||||
}
|
||||
return min + rand.Int()%(max-min)
|
||||
}
|
||||
|
||||
func randomFloat32(min float32, max float32) float32 {
|
||||
if min > max {
|
||||
return randomFloat32(max, min)
|
||||
}
|
||||
if min == max {
|
||||
return min
|
||||
}
|
||||
return min + rand.Float32()*(max-min)
|
||||
}
|
||||
|
||||
func randomString(n int) string {
|
||||
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
if _, err := os.Stat(filename); err == nil {
|
||||
return true
|
||||
} else if os.IsNotExist(err) {
|
||||
return false
|
||||
} else {
|
||||
return false // Assume it's a zombie or something
|
||||
}
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Initialisation
|
||||
fmt.Println("Initializing test run ... ")
|
||||
seed := time.Now().UnixNano()
|
||||
rand.Seed(seed)
|
||||
fmt.Printf("\tRandom seed %d\n", seed)
|
||||
|
||||
if fileExists(testDatabase) {
|
||||
fmt.Fprintln(os.Stderr, "Test file already exists: "+testDatabase)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("\tFilename: %s\n", testDatabase)
|
||||
var err error
|
||||
db, err = OpenDb(testDatabase)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error setting up database: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Run tests
|
||||
ret := m.Run()
|
||||
|
||||
// Cleanup
|
||||
db.Close()
|
||||
os.Remove(testDatabase)
|
||||
os.Exit(ret)
|
||||
}
|
||||
|
||||
func checkStation(station Station) bool {
|
||||
ref := station
|
||||
sec, err := db.GetStation(station.Id)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Database error: %s\n", err)
|
||||
return false
|
||||
}
|
||||
return ref == sec
|
||||
}
|
||||
|
||||
func TestStations(t *testing.T) {
|
||||
t.Log("Setting up database")
|
||||
err := db.Prepare()
|
||||
if err != nil {
|
||||
t.Fatal("Error preparing database")
|
||||
}
|
||||
|
||||
t.Log("Ensure database is empty")
|
||||
stations, err := db.GetStations()
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(stations) > 0 {
|
||||
t.Error(fmt.Sprintf("Fetched %d stations from expected empty database", len(stations)))
|
||||
}
|
||||
|
||||
// Insert stations
|
||||
t.Log("Inserting stations")
|
||||
station1 := Station{Id: 0, Name: "Station 1", Description: "First test station"}
|
||||
station2 := Station{Id: 0, Name: "Station 2", Description: "Second test station"}
|
||||
|
||||
err = db.InsertStation(&station1)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
stations, err = db.GetStations()
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(stations) != 1 {
|
||||
t.Error(fmt.Sprintf("Fetched %d stations but expected 1", len(stations)))
|
||||
}
|
||||
if !checkStation(station1) {
|
||||
t.Error("Comparison after inserting station 1 failed")
|
||||
}
|
||||
err = db.InsertStation(&station2)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
stations, err = db.GetStations()
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(stations) != 2 {
|
||||
t.Error(fmt.Sprintf("Fetched %d stations but expected 2", len(stations)))
|
||||
}
|
||||
if !checkStation(station1) {
|
||||
t.Error("Comparison after inserting station 2 failed")
|
||||
}
|
||||
|
||||
t.Log("Checking for existing stations")
|
||||
exists, err := db.ExistsStation(station1.Id)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Error("Station 1 reported as not existing")
|
||||
}
|
||||
exists, err = db.ExistsStation(station2.Id)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Error("Station 2 reported as not existing")
|
||||
}
|
||||
exists, err = db.ExistsStation(station1.Id + station2.Id)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if exists {
|
||||
t.Error("Station 1+2 reported as existing")
|
||||
}
|
||||
|
||||
t.Log("Inserting station with certain ID")
|
||||
station3 := Station{Id: 55, Name: "Station 55"}
|
||||
err = db.InsertStation(&station3)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if station3.Id != 55 {
|
||||
t.Fatal("Inserting station_55 with id 55, but ID has been reset")
|
||||
}
|
||||
if !checkStation(station3) {
|
||||
t.Error("Comparison after inserting station 55 failed")
|
||||
}
|
||||
|
||||
t.Log("Updating station information")
|
||||
station3.Name = "Station 55_1"
|
||||
station3.Location = "Nowhere"
|
||||
station3.Description = "Updated description"
|
||||
err = db.UpdateStation(station3)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
station3_clone, err := db.GetStation(station3.Id)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if station3 != station3_clone {
|
||||
t.Error("Comparison after updating station failed")
|
||||
}
|
||||
if station3_clone.Name != "Station 55_1" {
|
||||
t.Error("Update station name failed")
|
||||
}
|
||||
if station3_clone.Location != "Nowhere" {
|
||||
t.Error("Update station location failed")
|
||||
}
|
||||
if station3_clone.Description != "Updated description" {
|
||||
t.Error("Update station description failed")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDatapoints(t *testing.T) {
|
||||
progressionPoints := 100
|
||||
|
||||
t.Log("Setting up database")
|
||||
err := db.Prepare()
|
||||
if err != nil {
|
||||
t.Fatal("Error preparing database")
|
||||
}
|
||||
|
||||
t.Log("Inserting test stations")
|
||||
station1 := Station{Id: 0, Name: "Station 1", Description: "First test station"}
|
||||
station2 := Station{Id: 0, Name: "Station 2", Description: "Second test station"}
|
||||
err = db.InsertStation(&station1)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
err = db.InsertStation(&station2)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
|
||||
dps, err := db.GetLastDataPoints(station1.Id, 1000)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(dps) != 0 {
|
||||
t.Fatal("Station 1 contains datapoints, when being created")
|
||||
}
|
||||
dps, err = db.GetLastDataPoints(station2.Id, 1000)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(dps) != 0 {
|
||||
t.Fatal("Station 2 contains datapoints, when being created")
|
||||
}
|
||||
|
||||
t.Log("Inserting test datapoints")
|
||||
now := time.Now().Unix()
|
||||
dp := DataPoint{
|
||||
Timestamp: now - 100,
|
||||
Station: station1.Id,
|
||||
Temperature: 12,
|
||||
Humidity: 69,
|
||||
Pressure: 94501,
|
||||
}
|
||||
err = db.InsertDataPoint(dp)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
dps, err = db.GetLastDataPoints(station1.Id, 1000)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(dps) != 1 {
|
||||
t.Fatal(fmt.Sprintf("Station 1 contains %d datapoints, when expecting 1", len(dps)))
|
||||
}
|
||||
if dps[0] != dp {
|
||||
t.Error("Fetched datapoint doesn't match inserted datapoint")
|
||||
}
|
||||
dps, err = db.GetLastDataPoints(station2.Id, 1000)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(dps) != 0 {
|
||||
t.Fatal("Station 2 contains datapoints, but is expected to be empty")
|
||||
}
|
||||
|
||||
dp = DataPoint{
|
||||
Timestamp: now + 10,
|
||||
Station: station1.Id,
|
||||
Temperature: 13,
|
||||
Humidity: 68,
|
||||
Pressure: 94500,
|
||||
}
|
||||
err = db.InsertDataPoint(dp)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
dps, err = db.GetLastDataPoints(station1.Id, 1000)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(dps) != 2 {
|
||||
t.Fatal(fmt.Sprintf("Station 1 contains %d datapoints, when expecting 2", len(dps)))
|
||||
}
|
||||
dps, err = db.GetLastDataPoints(station1.Id, 1)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(dps) != 1 {
|
||||
t.Fatal(fmt.Sprintf("Station 1 contains %d datapoints, when only fetching 1", len(dps)))
|
||||
}
|
||||
if dps[0] != dp {
|
||||
t.Error("Fetched datapoint doesn't match inserted datapoint")
|
||||
}
|
||||
|
||||
t.Log("Make a random progression on station 2")
|
||||
t0 := now - 10000
|
||||
dp = DataPoint{
|
||||
Timestamp: t0,
|
||||
Station: station2.Id,
|
||||
Temperature: randomFloat32(5, 30),
|
||||
Humidity: randomFloat32(40, 80),
|
||||
Pressure: randomFloat32(101300-500, 101300+500),
|
||||
}
|
||||
err = db.InsertDataPoint(dp)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
for i := 0; i < progressionPoints-1; i++ {
|
||||
dp.Timestamp += int64(randomFloat32(1, 10))
|
||||
dp.Temperature += randomFloat32(-2, 2)
|
||||
dp.Humidity += randomFloat32(-1, 1)
|
||||
dp.Pressure += randomFloat32(-50, 50)
|
||||
err = db.InsertDataPoint(dp)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
}
|
||||
t.Log("Fetching queries on station 2")
|
||||
dps, err = db.QueryStation(station2.Id, now-10000, dp.Timestamp, int64(progressionPoints*2), 0)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(dps) != progressionPoints {
|
||||
t.Error(fmt.Sprintf("Querystation yielded %d points, but %d points are expected", len(dps), progressionPoints))
|
||||
}
|
||||
dp1 := dps[1]
|
||||
dps, err = db.QueryStation(station2.Id, dps[2].Timestamp, dps[5].Timestamp, 10, 0)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(dps) != 4 {
|
||||
t.Error(fmt.Sprintf("Querystation[2] yielded %d points, but %d points are expected", len(dps), 4))
|
||||
}
|
||||
|
||||
last, err := db.GetLastDataPoints(station2.Id, 1)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
t_l := last[0].Timestamp
|
||||
dps, err = db.QueryStation(station2.Id, t0, t_l, 2, 1)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(dps) != 2 {
|
||||
t.Error(fmt.Sprintf("Querystation[3] yielded %d points, but %d points are expected", len(dps), 2))
|
||||
}
|
||||
if dps[0] != dp1 {
|
||||
t.Error("Offset 1 doesn't returned dp[1]")
|
||||
}
|
||||
}
|
||||
|
||||
func checkToken(token Token) bool {
|
||||
tok1, err := db.GetToken(token.Token)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Database error: ", err)
|
||||
return false
|
||||
}
|
||||
return token == tok1
|
||||
}
|
||||
|
||||
func TestTokens(t *testing.T) {
|
||||
t.Log("Setting up database")
|
||||
err := db.Prepare()
|
||||
if err != nil {
|
||||
t.Fatal("Error preparing database")
|
||||
}
|
||||
|
||||
t.Log("Inserting test stations")
|
||||
station1 := Station{Id: 0, Name: "Station 1", Description: "First test station"}
|
||||
station2 := Station{Id: 0, Name: "Station 2", Description: "Second test station"}
|
||||
err = db.InsertStation(&station1)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
err = db.InsertStation(&station2)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
|
||||
tok1 := Token{Token: randomString(32), Station: station1.Id}
|
||||
err = db.InsertToken(tok1)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
tok2 := Token{Token: randomString(32), Station: station2.Id}
|
||||
err = db.InsertToken(tok2)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
|
||||
tokens, err := db.GetStationTokens(station1.Id)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(tokens) != 1 {
|
||||
t.Error(fmt.Sprintf("Fetched %d tokens from station 1, but expected 1", len(tokens)))
|
||||
}
|
||||
if !checkToken(tok1) {
|
||||
fmt.Errorf("Checktoken failed (token1) ")
|
||||
}
|
||||
tokens, err = db.GetStationTokens(station2.Id)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(tokens) != 1 {
|
||||
t.Error(fmt.Sprintf("Fetched %d tokens from station 2, but expected 1", len(tokens)))
|
||||
}
|
||||
if !checkToken(tok2) {
|
||||
fmt.Errorf("Checktoken failed (token2) ")
|
||||
}
|
||||
|
||||
// Test remove tokens
|
||||
err = db.RemoveToken(tok1)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
tokens, err = db.GetStationTokens(station1.Id)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(tokens) != 0 {
|
||||
t.Error(fmt.Sprintf("Fetched %d tokens from station 1, but expected 0", len(tokens)))
|
||||
}
|
||||
tokens, err = db.GetStationTokens(station2.Id)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(tokens) != 1 {
|
||||
t.Error(fmt.Sprintf("Fetched %d tokens from station 2, but expected 1", len(tokens)))
|
||||
}
|
||||
err = db.InsertToken(tok1)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
tokens, err = db.GetStationTokens(station1.Id)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(tokens) != 1 {
|
||||
t.Error(fmt.Sprintf("Fetched %d tokens from station 1, but expected 1", len(tokens)))
|
||||
}
|
||||
if !checkToken(tok1) {
|
||||
fmt.Errorf("Checktoken failed (token1) ")
|
||||
}
|
||||
if !checkToken(tok2) {
|
||||
fmt.Errorf("Checktoken failed (token2) ")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLightnings(t *testing.T) {
|
||||
seriesPoints := 100
|
||||
|
||||
lightnings, err := db.GetLightnings(1000, 0)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(lightnings) != 0 {
|
||||
t.Error("Nonempty lightning table")
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
t.Log("Inserting first lightning")
|
||||
lightning := Lightning{Timestamp: now, Station: 1, Distance: 0}
|
||||
err = db.InsertLightning(lightning)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
lightnings, err = db.GetLightnings(1000, 0)
|
||||
if len(lightnings) != 1 {
|
||||
t.Error("Failed to fetch first lightning")
|
||||
}
|
||||
if lightnings[0] != lightning {
|
||||
t.Error("First lightning mismatched")
|
||||
}
|
||||
|
||||
t.Log("Inserting lightning at same time")
|
||||
err = db.InsertLightning(Lightning{Timestamp: now, Station: 2, Distance: 1.024})
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
lightnings, err = db.GetLightnings(1000, 0)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(lightnings) != 2 {
|
||||
t.Error("Failed to fetch second lightning")
|
||||
}
|
||||
// Although now explicitly required for now the lightnings preserve the last-in first-out order, so this is fine
|
||||
if lightnings[1].Timestamp != now {
|
||||
t.Error("Second lightning Timestamp mismatched")
|
||||
}
|
||||
if lightnings[1].Station != 2 {
|
||||
t.Error("Second lightning Station mismatched")
|
||||
}
|
||||
if lightnings[1].Distance != 1.024 {
|
||||
t.Error("Second lightning Distance mismatched")
|
||||
}
|
||||
|
||||
t.Log("Inserting lightning series")
|
||||
for i := 0; i < seriesPoints; i++ {
|
||||
err = db.InsertLightning(Lightning{Timestamp: now + int64(i), Station: 3, Distance: 5.0 + float32(i)*0.1})
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
}
|
||||
// Test to fetch all
|
||||
lightnings, err = db.GetLightnings(seriesPoints+2, 0)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(lightnings) != seriesPoints+2 {
|
||||
t.Errorf("Fetching %d lightnings returned %d", seriesPoints+2, len(lightnings))
|
||||
}
|
||||
|
||||
// Test only the given last subseries
|
||||
lightnings, err = db.GetLightnings(seriesPoints, 0)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(lightnings) != seriesPoints {
|
||||
t.Errorf("Fetching %d lightnings returned %d", seriesPoints, len(lightnings))
|
||||
}
|
||||
for i := 0; i < seriesPoints; i++ {
|
||||
lightning := lightnings[i]
|
||||
|
||||
i = seriesPoints - i - 1 // i is here in reverse order
|
||||
timestamp := now + int64(i) // Expected timestamp
|
||||
distance := float32(5.0 + float32(i)*0.1) // Expected distance
|
||||
fmt.Printf("%d %d %f\n", timestamp, 3, distance)
|
||||
if lightning.Station != 3 {
|
||||
t.Errorf("Lightning series Station mismatched (%d != %d)", lightning.Station, 3)
|
||||
}
|
||||
if lightning.Timestamp != timestamp {
|
||||
t.Errorf("Lightning series Timestamp mismatched (%d != %d)", lightning.Timestamp, timestamp)
|
||||
}
|
||||
if lightning.Distance != distance {
|
||||
t.Errorf("Lightning series Distance mismatched (%f != %f)", lightning.Distance, distance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOmbrometers(t *testing.T) {
|
||||
seriesPoints := 100
|
||||
|
||||
ombrometers, err := db.GetOmbrometers()
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(ombrometers) != 0 {
|
||||
t.Error("Not empty ombrometer list at beginning")
|
||||
}
|
||||
ombrometer1 := Station{Name: "Ombrometer 1", Location: "Location 1", Description: "First ombrometer"}
|
||||
err = db.InsertOmbrometer(&ombrometer1)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
log.Printf("Inserted ombrometer 1 with ID %d", ombrometer1.Id)
|
||||
|
||||
ombrometers, err = db.GetOmbrometers()
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(ombrometers) != 1 {
|
||||
t.Error("Got empty ombrometer after inserting one")
|
||||
}
|
||||
if ombrometer1 != ombrometers[0] {
|
||||
t.Error("First ombrometer mismatch")
|
||||
}
|
||||
if ombrometer1.Id != ombrometers[0].Id {
|
||||
t.Error("Ombrometer Id mismatch")
|
||||
}
|
||||
if ombrometer1.Name != ombrometers[0].Name {
|
||||
t.Error("Ombrometer Name mismatch")
|
||||
}
|
||||
if ombrometer1.Location != ombrometers[0].Location {
|
||||
t.Error("Ombrometer Location mismatch")
|
||||
}
|
||||
if ombrometer1.Description != ombrometers[0].Description {
|
||||
t.Error("Ombrometer Description mismatch")
|
||||
}
|
||||
|
||||
ombrometer2 := Station{Name: "Ombrometer 2", Location: "Location 2", Description: "Second ombrometer"}
|
||||
err = db.InsertOmbrometer(&ombrometer2)
|
||||
log.Printf("Inserted ombrometer 2 with ID %d", ombrometer2.Id)
|
||||
ombrometers, err = db.GetOmbrometers()
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(ombrometers) != 2 {
|
||||
t.Errorf("Fetched %d ombrometers instead of two after inserting another one", len(ombrometers))
|
||||
}
|
||||
|
||||
log.Println("Updating ombrometer station")
|
||||
ombrometer2.Name = "Updated Ombrometer 2"
|
||||
ombrometer2.Location = "New Location 2"
|
||||
ombrometer2.Description = "Improved second ombrometer"
|
||||
err = db.UpdateOmbrometer(ombrometer2)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
ombro2, err := db.GetOmbrometer(ombrometer2.Id)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if ombro2 != ombrometer2 {
|
||||
t.Error("Ombrometer 2 update failed")
|
||||
}
|
||||
|
||||
log.Println("Test ombrometer readings")
|
||||
reading, err := db.GetOmbrometerLastReading(ombrometer1.Id)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if reading.Timestamp != 0 || reading.Millimeters != 0 {
|
||||
t.Error("Ombrometer 1 returned ghost reading")
|
||||
}
|
||||
readings, err := db.GetOmbrometerReadings(ombrometer1.Id, 10)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(readings) != 0 {
|
||||
t.Error("Ombrometer 1 returned ghost readings")
|
||||
}
|
||||
reading.Millimeters = 1
|
||||
reading.Timestamp = time.Now().Unix()
|
||||
reading.Station = ombrometer1.Id
|
||||
err = db.InsertRainMeasurement(reading)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
reading2, err := db.GetOmbrometerLastReading(ombrometer1.Id)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if reading2 != reading {
|
||||
t.Error("Ombrometer 1 returned false reading")
|
||||
}
|
||||
readings, err = db.GetOmbrometerReadings(ombrometer2.Id, 10)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(readings) != 0 {
|
||||
t.Error("Ombrometer 2 returned ghost readings")
|
||||
}
|
||||
reading.Station = ombrometer2.Id
|
||||
now := time.Now().Unix()
|
||||
for i := 0; i < seriesPoints; i++ {
|
||||
reading.Millimeters = float32(i) * 0.1
|
||||
reading.Timestamp = now + int64(i)
|
||||
err = db.InsertRainMeasurement(reading)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
readings, err = db.GetOmbrometerReadings(ombrometer2.Id, seriesPoints)
|
||||
if err != nil {
|
||||
t.Fatal("Database error: ", err)
|
||||
}
|
||||
if len(readings) != seriesPoints {
|
||||
t.Errorf("Ombrometer 2 returned %d instead of %d readings", len(readings), seriesPoints)
|
||||
}
|
||||
// Check if expected values
|
||||
for i, reading := range readings {
|
||||
i = seriesPoints - i - 1 // Inverted output from database!
|
||||
if reading.Timestamp != now+int64(i) {
|
||||
t.Errorf("Reading %d timestamp mismtach: %d != %d", i, reading.Timestamp, now+int64(i))
|
||||
}
|
||||
if reading.Millimeters != float32(i)*0.1 {
|
||||
t.Errorf("Reading %d millimeters mismtach: %f != %f", i, reading.Millimeters, float32(i)*0.1)
|
||||
}
|
||||
}
|
||||
log.Println("Rain series OK")
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
[mqtt]
|
||||
remote = "127.0.0.1"
|
||||
|
||||
[influxdb]
|
||||
remote = "http://127.0.0.1:8086"
|
||||
username = "meteo"
|
||||
password = "meteo"
|
||||
database = "meteo"
|
12
meteo.toml
12
meteo.toml
|
@ -1,12 +0,0 @@
|
|||
DefaultRemote = "meteo" # Default server if nothing is given
|
||||
|
||||
|
||||
[Remotes.meteo]
|
||||
Remote = "https://localhost/meteo"
|
||||
|
||||
# Declare ranges for individual station (for color output)
|
||||
[Ranges.meteo]
|
||||
# Temperature range [ err_low, warn_low, warn_high, err_high ]
|
||||
# Ranges must be float (e.g. 1.0 instead of 1)
|
||||
temp_range = [ 10.0, 22.0, 30.0, 40.0 ]
|
||||
hum_range = [ 10.0, 10.0, 50.0, 80.0 ]
|
12
meteod.conf.example
Normal file
12
meteod.conf.example
Normal file
|
@ -0,0 +1,12 @@
|
|||
[mqtt]
|
||||
remote = "127.0.0.1"
|
||||
clientid = "meteod"
|
||||
|
||||
[influxdb]
|
||||
remote = "http://127.0.0.1:8086"
|
||||
organization = "meteo"
|
||||
bucket = "meteo"
|
||||
token = ""
|
||||
|
||||
[http]
|
||||
bind_addr = "127.0.0.1:4042"
|
|
@ -1,16 +0,0 @@
|
|||
[Unit]
|
||||
Description=Meteo Daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=meteo
|
||||
WorkingDirectory=/home/meteo/meteo
|
||||
ExecStart=/home/meteo/meteo/meteod
|
||||
RestartSec=10
|
||||
Restart=always
|
||||
StandardOutput=null
|
||||
StandardError=file:/var/log/meteod.err
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -1,9 +0,0 @@
|
|||
PushDelay = 60
|
||||
Database = "meteod.db"
|
||||
MQTT = "suse-meteod@asgard.home:1883"
|
||||
|
||||
[Webserver]
|
||||
port = 8802
|
||||
Bindaddr = "127.0.0.1"
|
||||
#QueryLimit = 1000
|
||||
AllowEdit = true
|
6
utils/.gitignore
vendored
6
utils/.gitignore
vendored
|
@ -1,6 +0,0 @@
|
|||
# Database files
|
||||
*.db
|
||||
*.sql
|
||||
|
||||
# Python
|
||||
*.pyc
|
|
@ -1,241 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Simple python script to migrate data from a mysql database to a sqlite database
|
||||
|
||||
import os
|
||||
import sys
|
||||
import MySQLdb
|
||||
import sqlite3
|
||||
|
||||
|
||||
class Station :
|
||||
def __init__(self, s_id=0, name="", location="", description="") :
|
||||
self.id = s_id
|
||||
self.name = name
|
||||
self.location = location
|
||||
self.description = description
|
||||
|
||||
def __str__(self) : return "(%2d) %s, %s, %s" % (self.id, self.name, self.location, self.description)
|
||||
|
||||
class Token :
|
||||
def __init__(self, token="", station=0) :
|
||||
self.token = token
|
||||
self.station = station
|
||||
|
||||
class DataPoint :
|
||||
def __init__(self, timestamp=0, station=0, temperature=0.0, humidity=0.0, pressure=0.0) :
|
||||
self.timestamp = timestamp
|
||||
self.station = station
|
||||
self.temperature = temperature
|
||||
self.humidity = humidity
|
||||
self.pressure = pressure
|
||||
|
||||
|
||||
|
||||
class MyMeteo :
|
||||
'''
|
||||
Class for accessing the deprecated meteo mysql database
|
||||
'''
|
||||
def __init__(self, hostname, database, username, password) :
|
||||
self.hostname = hostname
|
||||
self.database = database
|
||||
|
||||
self._db = MySQLdb.connect(hostname, username, password, database)
|
||||
|
||||
def db_version(self) :
|
||||
cursor = self._db.cursor()
|
||||
cursor.execute("SELECT VERSION()")
|
||||
return cursor.fetchone()
|
||||
|
||||
def close(self) :
|
||||
self._db.close()
|
||||
|
||||
def stations(self) :
|
||||
ret = []
|
||||
cursor = self._db.cursor()
|
||||
cursor.execute("SELECT `id`,`name`,`location`,`description` FROM `stations`")
|
||||
rows = cursor.fetchall()
|
||||
for row in rows :
|
||||
ret.append(Station(row[0], row[1], row[2], row[3]))
|
||||
return ret
|
||||
|
||||
def tokens(self) :
|
||||
ret = []
|
||||
cursor = self._db.cursor()
|
||||
cursor.execute("SELECT `token`,`station` FROM `tokens`")
|
||||
rows = cursor.fetchall()
|
||||
for row in rows :
|
||||
ret.append(Token(row[0], row[1]))
|
||||
return ret
|
||||
|
||||
def count_datapoints(self, station) :
|
||||
ret = []
|
||||
cursor = self._db.cursor()
|
||||
cursor.execute("SELECT count(*) AS `count` FROM `station_%d`" % (int(station)))
|
||||
return int(cursor.fetchone()[0])
|
||||
|
||||
def datapoints(self, station, limit=10000, offset=0) :
|
||||
ret = []
|
||||
cursor = self._db.cursor()
|
||||
cursor.execute("SELECT `timestamp`,`temperature`,`humidity`,`pressure` FROM `station_%d` ORDER BY `timestamp` ASC LIMIT %d OFFSET %d;" % (int(station), int(limit), int(offset)))
|
||||
rows = cursor.fetchall()
|
||||
for row in rows : ret.append(DataPoint(float(row[0]), station, float(row[1]), float(row[2]), float(row[3])))
|
||||
return ret
|
||||
|
||||
|
||||
class MeteoDB :
|
||||
def __init__(self, filename) :
|
||||
self.filename = filename
|
||||
self._con = sqlite3.connect(filename)
|
||||
|
||||
|
||||
def prepare(self) :
|
||||
c = self._con.cursor()
|
||||
c.execute("CREATE TABLE IF NOT EXISTS `stations` (`id` INT PRIMARY KEY, `name` VARCHAR(64), `location` TEXT, `description` TEXT);")
|
||||
c.execute("CREATE TABLE IF NOT EXISTS `tokens` (`token` VARCHAR(32) PRIMARY KEY, `station` INT);")
|
||||
|
||||
|
||||
def insertStation(self, station) :
|
||||
c = self._con.cursor()
|
||||
c.execute("INSERT INTO `stations` (`id`,`name`,`location`,`description`) VALUES (?,?,?,?);", (station.id, station.name, station.location, station.description))
|
||||
c.execute("CREATE TABLE IF NOT EXISTS `station_%d` (`timestamp` INT PRIMARY KEY, `temperature` REAL, `humidity` REAL, `pressure` REAL);" % (int(station.id)))
|
||||
|
||||
def insertToken(self, token) :
|
||||
c = self._con.cursor()
|
||||
c.execute("INSERT INTO `tokens` (`token`,`station`) VALUES (?,?);", (token.token, token.station))
|
||||
|
||||
def insertDatapoint(self, dp) :
|
||||
c = self._con.cursor()
|
||||
c.execute("INSERT OR REPLACE INTO `station_%d` (`timestamp`,`temperature`,`humidity`,`pressure`) VALUES (?,?,?,?);" % (int(dp.station)), (dp.timestamp, dp.temperature, dp.humidity, dp.pressure))
|
||||
|
||||
def count_datapoints(self, station) :
|
||||
ret = []
|
||||
cursor = self._con.cursor()
|
||||
cursor.execute("SELECT count(*) AS `count` FROM `station_%d`" % (int(station)))
|
||||
return int(cursor.fetchone()[0])
|
||||
|
||||
def commit(self): self._con.commit()
|
||||
|
||||
def close(self) : self._con.close()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def getPassword() :
|
||||
try :
|
||||
import getpass
|
||||
return getpass.getpass("Password> ")
|
||||
except ImportError as e:
|
||||
sys.stderr.write("ImportError: %s\n" % str(e))
|
||||
sys.stderr.flush()
|
||||
print("Falling back to default input")
|
||||
print("WARNING: Password will be shown in cleartext on prompt")
|
||||
return input("Password (unprotected)> ")
|
||||
|
||||
if __name__ == "__main__" :
|
||||
dbfilename = None
|
||||
db_hostname, db_database, db_username, db_password = "", "", "", ""
|
||||
confirm = True
|
||||
|
||||
args = sys.argv[1:]
|
||||
for arg in filter(lambda x : x[0] == '-', args) :
|
||||
if arg == "-y" or arg == "--yes" :
|
||||
confirm = False
|
||||
args.remove("-y")
|
||||
elif arg == "-h" or arg == "--help" :
|
||||
print("meteo Migration utility")
|
||||
print(" Migrate your meteo database from MySQL to SQLite3")
|
||||
print("")
|
||||
print("Usage: %s [HOSTNAME DATABASE USERNAME] [SQLITE3]" % sys.argv[0])
|
||||
print(" if [HOSTNAME DATABASE USERNAME] is given, then those values will be used for the mysql connection")
|
||||
print(" the password will be still asked")
|
||||
print(" if SQLITE3 is given, this will be the filename for the destination database")
|
||||
print("")
|
||||
print("OPTIONS")
|
||||
print(" -h, --help Print this help message")
|
||||
print(" -y, --yes Don't prompt for confirmation before transferring")
|
||||
print("")
|
||||
print("2019, Felix Niederwanger")
|
||||
sys.exit(0)
|
||||
else :
|
||||
raise ValueError("Illegal argument: " + arg)
|
||||
|
||||
args = list(filter(lambda x : x[0] != '-', args))
|
||||
|
||||
if len(sys.argv) > 1 : dbfilename = sys.argv[1]
|
||||
if len(sys.argv) > 4 :
|
||||
db_hostname = sys.argv[1]
|
||||
db_database = sys.argv[2]
|
||||
db_username = sys.argv[3]
|
||||
dbfilename = sys.argv[4]
|
||||
db_password = getPassword()
|
||||
else :
|
||||
print("Configuring MySQL Database:")
|
||||
db_hostname = input("Hostname> ")
|
||||
db_database = input("Database> ")
|
||||
db_username = input("Username> ")
|
||||
db_password = getPassword()
|
||||
if dbfilename is None :
|
||||
print("Configuring sqlite database:")
|
||||
dbfilename = input("Filename> ")
|
||||
|
||||
# Connect to MySQL
|
||||
db, sq = None, None
|
||||
try :
|
||||
print("Connecting to %s@%s/%s ... " % (db_username, db_hostname, db_database))
|
||||
db = MyMeteo(db_hostname, db_database, db_username, db_password)
|
||||
print("Database connected : %s " % db.db_version())
|
||||
stations = db.stations()
|
||||
print("Fetched %d stations:" % (len(stations)))
|
||||
datapoints = 0
|
||||
for station in stations :
|
||||
dps = db.count_datapoints(station.id)
|
||||
datapoints += dps
|
||||
print("\t%s [%d datapoints]" % (station, dps))
|
||||
print("Counter %d datapoints in all stations" % (datapoints))
|
||||
tokens = db.tokens()
|
||||
print("Fetched %d tokens" % (len(tokens)))
|
||||
|
||||
|
||||
while confirm :
|
||||
yes = input("Continue? [Y/n] ").lower()
|
||||
if yes in ["no", "n"] : sys.exit(1)
|
||||
if yes in ["yes", "y"] : break
|
||||
|
||||
print("Creating %s ... " % (dbfilename))
|
||||
sq = MeteoDB(dbfilename)
|
||||
sq.prepare()
|
||||
print("\tImporting stations ... ")
|
||||
for station in stations : sq.insertStation(station)
|
||||
sq.commit()
|
||||
print("\tImporting tokens ... ")
|
||||
for token in tokens : sq.insertToken(token)
|
||||
sq.commit()
|
||||
print("\tImporting datapoints ... ")
|
||||
bunch = 1000
|
||||
for station in stations :
|
||||
count = db.count_datapoints(station.id)
|
||||
counter = 0
|
||||
sys.stdout.write("\t\t%s - %d datapoints ... " % (station.name, count))
|
||||
sys.stdout.flush()
|
||||
while counter < count :
|
||||
for dp in db.datapoints(station.id, bunch, counter) :
|
||||
sq.insertDatapoint(dp)
|
||||
counter += bunch
|
||||
sq.commit()
|
||||
count2 = sq.count_datapoints(station.id)
|
||||
if count2 != count :
|
||||
sys.stderr.write("Imported %d datapoints, but source holds %d datapoints\n" % (count2, count))
|
||||
sys.exit(1)
|
||||
sys.stdout.write("ok (%d records counted)\n" % (count2))
|
||||
|
||||
print("Import completed")
|
||||
|
||||
finally :
|
||||
if not db is None : db.close()
|
||||
if not sq is None : sq.close()
|
||||
|
82
web/api.html
82
web/api.html
|
@ -1,82 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>meteo</title>
|
||||
<LINK href="asset/meteo.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>meteo | Request API</h1>
|
||||
|
||||
<p><a href="dashboard">[Dashboard]</a> <a href="api.html">[Request API]</a></p>
|
||||
|
||||
<h2>Request API</h2>
|
||||
|
||||
<p>Welcome! This page documents the usage of the internal meteo Request API</p>
|
||||
<p>The request API supports the following main items:
|
||||
<ul>
|
||||
<li><a href="#Stations">Stations</a></li>
|
||||
<li><a href="#Station">Individual Station</a></li>
|
||||
<li><a href="#Current">Current Readings</a></li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<a name="Stations"><h3>Stations</h3></a>
|
||||
|
||||
<p>The link <b>/stations</b> serves an overview of the known stations for the system<br/>
|
||||
The output is a commented CSV of the following format:
|
||||
|
||||
<pre>
|
||||
# Id,Name,Location,Description
|
||||
1,test,virtual,Test station 1
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
<a name="Station"><h3>Station</h3></a>
|
||||
|
||||
<p><b>/station/<i>id</i></b> is there to request informations from a specific station, identified by it's <i>id</i>.</p>
|
||||
|
||||
<p>The following request types exist:
|
||||
<ul>
|
||||
<li><b>/station/<i>id</i></b> Fetch current readings as CSV (e.g. <b>/station/1</b>)</li>
|
||||
<li><b>/station/<i>id</i>/<i>year</i>.csv</b> Fetch the data of the given <i>year</i> as CSV (e.g. <b>/station/1/2019.csv</b>)</li>
|
||||
<li><b>/station/<i>id</i>/<i>year</i>/<i>month</i>.csv</b> Fetch the data of the given <i>year</i> and <i>month</i> as CSV (e.g. <b>/station/1/2019/08.csv</b>)</li>
|
||||
<li><b>/station/<i>id</i>/<i>year</i>/<i>month</i>/<i>day</i>.csv</b> Fetch the data of the given <i>year</i>, <i>month</i> and <i>day</i> as CSV (e.g. <b>/station/1/2019/08/02.csv</b>)</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<a name="Current"><h3>Current Readings</h3></a>
|
||||
|
||||
<p>The link <b>/current</b> serves the current readings as CSV<br/>
|
||||
<b>/current.csv</b> servers the current readings as CSV file</p>
|
||||
|
||||
<p>The output of <b>/current</b> looks like the following:
|
||||
|
||||
<pre>
|
||||
# Station, Timestamp, Temperature, Humidity, Pressure
|
||||
1,1562242860,24.10,9.00,94388.00
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
<a name="MQTT"><h3>MQTT</h3></a>
|
||||
|
||||
<p><em>meteod</em> can be configured, to use MQTT.<br/>If a MQTT channel is configured, <em>meteod</em> connects to the broker and listens on the <em>meteo/#</em> channel for messages</p>
|
||||
|
||||
<p>
|
||||
A typical message is JSON formatted and looks like the following
|
||||
<pre>
|
||||
{"node":5,"name":"Lightning","t":20.41,"hum":70.51,"p":95409.73}
|
||||
{"node":5,"name":"Lightning","t":20.41,"hum":70.48,"p":95410.53}
|
||||
{"node":1,"name":"Kitchen","t":27.97,"hum":48.60,"eCO2":400.00,"tVOC":0.00}
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If a MQTT broker has been defined and <em>meteod</em> received data via HTTP, it also pushes them as JSON to MQTT.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
MQTT is considered as trusted channel. If a new station appears via MQTT, <em>meteod</em> inserts the station into the database. For more control and protection against missuse, please use http!
|
||||
</p>
|
||||
|
||||
</body>
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* DOM element rendering detection
|
||||
* https://davidwalsh.name/detect-node-insertion
|
||||
*/
|
||||
@keyframes chartjs-render-animation {
|
||||
from { opacity: 0.99; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.chartjs-render-monitor {
|
||||
animation: chartjs-render-animation 0.001s;
|
||||
}
|
||||
|
||||
/*
|
||||
* DOM element resizing detection
|
||||
* https://github.com/marcj/css-element-queries
|
||||
*/
|
||||
.chartjs-size-monitor,
|
||||
.chartjs-size-monitor-expand,
|
||||
.chartjs-size-monitor-shrink {
|
||||
position: absolute;
|
||||
direction: ltr;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.chartjs-size-monitor-expand > div {
|
||||
position: absolute;
|
||||
width: 1000000px;
|
||||
height: 1000000px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.chartjs-size-monitor-shrink > div {
|
||||
position: absolute;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
14680
web/asset/Chart.js
vendored
14680
web/asset/Chart.js
vendored
File diff suppressed because it is too large
Load diff
|
@ -1,22 +0,0 @@
|
|||
body {
|
||||
background-color: lightgray;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding-left: 5px;
|
||||
}
|
||||
h2 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
h3 {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding-left: 10px;
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>meteo</title>
|
||||
<LINK href="asset/meteo.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>meteo | Dashboard</h1>
|
||||
|
||||
<p><a href="dashboard">[Dashboard]</a> <a href="lightnings">[Lightnings]</a> <a href="ombrometers">[Ombrometers]</a> <a href="api.html">[Request API]</a></p>
|
||||
|
||||
|
||||
{{if not .}}
|
||||
<p>No meteo stations so far</p>
|
||||
{{else}}
|
||||
<table border="1">
|
||||
<tr><td><b>Station</b></td><td><b>Description</b></td><td><b>Location</b></td><td><b>Temperature</b></td><td><b>Humidity</b></td><td><b>Pressure</b></td><td>Overview</td></tr>
|
||||
{{range .}}
|
||||
<tr><td><a href="dashboard/{{.Id}}">{{.Name}}</a></td><td>{{.Description}}</td><td>{{.Location}}</td><td>{{.T}} °C</td><td>{{.Hum}} rel. humidity</td><td>{{.P}} hPa</td><td><a href="dashboard/{{.Id}}?timespan=day">[Day]</a> <a href="dashboard/{{.Id}}?timespan=week">[Week]</a> <a href="dashboard/{{.Id}}?timespan=month">[Month]</a> <a href="dashboard/{{.Id}}?timespan=year">[Year]</a></td></tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
|
||||
<div class="footer">
|
||||
<p><a href="https://github.com/grisu48/meteo">meteo</a>, 2020</p>
|
||||
</div>
|
||||
</html>
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
<div style="width:95%;"><canvas id="canvas"></canvas></div>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
window.chartColors = {
|
||||
green: 'rgb(9, 190, 3)',
|
||||
blue: 'rgb(2, 0, 70)',
|
||||
};
|
||||
|
||||
|
||||
var lineChartData = {
|
||||
labels: [
|
||||
{{range .}}'{{.Time}}',{{end}}
|
||||
],
|
||||
datasets: [{
|
||||
label: 'Temperature',
|
||||
borderColor: window.chartColors.blue,
|
||||
backgroundColor: window.chartColors.blue,
|
||||
fill: false,
|
||||
data: [{{range .}}{{.Temperature}},{{end}}],
|
||||
yAxisID: 'y-axis-1',
|
||||
}, {
|
||||
label: 'Humidity',
|
||||
borderColor: window.chartColors.green,
|
||||
backgroundColor: window.chartColors.green,
|
||||
fill: false,
|
||||
data: [{{range .}}{{.Humidity}},{{end}}],
|
||||
yAxisID: 'y-axis-2'
|
||||
}]
|
||||
};
|
||||
|
||||
window.onload = function() {
|
||||
var ctx = document.getElementById('canvas').getContext('2d');
|
||||
window.myLine = Chart.Line(ctx, {
|
||||
data: lineChartData,
|
||||
options: {
|
||||
responsive: true,
|
||||
hoverMode: 'index',
|
||||
stacked: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'meteo - Station chart'
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance
|
||||
display: true,
|
||||
position: 'left',
|
||||
id: 'y-axis-1',
|
||||
}, {
|
||||
type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance
|
||||
display: true,
|
||||
position: 'right',
|
||||
id: 'y-axis-2',
|
||||
|
||||
// grid line settings
|
||||
gridLines: {
|
||||
drawOnChartArea: false, // only want the grid lines for one axis to show up
|
||||
},
|
||||
}],
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
|
@ -1,26 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>meteo</title>
|
||||
<LINK href="asset/meteo.css" rel="stylesheet" type="text/css" />
|
||||
<meta http-equiv="refresh" content="5" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>meteo | Lightnings</h1>
|
||||
|
||||
<p><a href="dashboard">[Dashboard]</a> <a href="lightnings">[Lightnings]</a> <a href="ombrometers">[Ombrometers]</a> <a href="api.html">[Request API]</a></p>
|
||||
|
||||
<p>Displays the last recorded lightnings. Page is automatically refreshed every 5 seconds</p>
|
||||
|
||||
{{if not .}}
|
||||
<p>No lightnings detected so far</p>
|
||||
{{else}}
|
||||
<table border="1">
|
||||
<tr><td><b>Station</b></td><td><b>Time</b></td><td><b>Timestamp</b></td><td><b>Distance</b></td></tr>
|
||||
{{range .}}
|
||||
<tr><td>{{if not .StationName}} Station {{.Station}}{{else}}<a href="dashboard/{{.Station}}">{{.StationName}}</a>{{end}}</td><td>{{.Ago}}</td><td>{{.DateTime}}</td><td>{{.Distance}} km</td></tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
</html>
|
|
@ -1,24 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>meteo</title>
|
||||
<LINK href="../asset/meteo.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>meteo | Ombrometer Station</h1>
|
||||
|
||||
<p><a href="../dashboard">[Dashboard]</a> <a href="../lightnings">[Lightnings]</a> <a href="../ombrometers">[Ombrometers]</a> <a href="../api.html">[Request API]</a> | <a href="{{.Ombrometer.Id}}/edit">[Edit ombrometer]</a></p>
|
||||
|
||||
{{if not .Data}}
|
||||
<p>No recorded rain measurements so far</p>
|
||||
{{else}}
|
||||
<table border="1">
|
||||
<tr><td><b>Timestamp</b></td><td><b>Acount</b></td></tr>
|
||||
{{range .Data}}
|
||||
<tr><td>{{.Timestamp}}</td><td>{{.Millimeters}} mm</td></tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
|
||||
</html>
|
|
@ -1,26 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>meteo</title>
|
||||
<LINK href="asset/meteo.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>meteo | Ombrometers</h1>
|
||||
|
||||
<p><a href="../dashboard">[Dashboard]</a> <a href="../lightnings">[Lightnings]</a> <a href="../ombrometers">[Ombrometers]</a> <a href="../api.html">[Request API]</a></p>
|
||||
|
||||
<FORM action="create" method="POST">
|
||||
|
||||
<table border="1">
|
||||
<tr><td>Name</td><td><input type="text" name="name" value=""></td></tr>
|
||||
<tr><td>Description</td><td><input type="text" name="desc" value=""></td></tr>
|
||||
<tr><td>Location</td><td><input type="text" name="location" value=""></td></tr>
|
||||
</table>
|
||||
|
||||
<p><input type="submit" value="Create"></p>
|
||||
|
||||
|
||||
</FORM>
|
||||
|
||||
</html>
|
|
@ -1,26 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>meteo</title>
|
||||
<LINK href="asset/meteo.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>meteo | Ombrometers</h1>
|
||||
|
||||
<p><a href="../dashboard">[Dashboard]</a> <a href="../lightnings">[Lightnings]</a> <a href="../ombrometers">[Ombrometers]</a> <a href="../api.html">[Request API]</a></p>
|
||||
|
||||
<FORM action="edit" method="POST">
|
||||
|
||||
<table border="1">
|
||||
<tr><td>Name</td><td><input type="text" name="name" value="{{.Name}}"></td></tr>
|
||||
<tr><td>Description</td><td><input type="text" name="desc" value="{{.Description}}"></td></tr>
|
||||
<tr><td>Location</td><td><input type="text" name="location" value="{{.Location}}"></td></tr>
|
||||
</table>
|
||||
|
||||
<p><input type="submit" value="Edit"><input type="reset" value="Reset"></p>
|
||||
|
||||
|
||||
</FORM>
|
||||
|
||||
</html>
|
|
@ -1,24 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>meteo</title>
|
||||
<LINK href="asset/meteo.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>meteo | Ombrometers</h1>
|
||||
|
||||
<p><a href="dashboard">[Dashboard]</a> <a href="lightnings">[Lightnings]</a> <a href="ombrometers">[Ombrometers]</a> <a href="api.html">[Request API]</a> | <a href="ombrometers/create">[Create ombrometer]</a></p>
|
||||
|
||||
|
||||
{{if not .}}
|
||||
<p>No ombrometer stations so far</p>
|
||||
{{else}}
|
||||
<table border="1">
|
||||
<tr><td><b>Station</b></td><td><b>Description</b></td><td><b>Location</b></td></tr>
|
||||
{{range .}}
|
||||
<tr><td><a href="ombrometer/{{.Id}}">{{.Name}}</a></td><td>{{.Description}}</td><td>{{.Location}}</td></tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
</html>
|
|
@ -1,21 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>meteo</title>
|
||||
<script src="../asset/Chart.js"></script>
|
||||
<LINK href="../asset/meteo.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>meteo | Station</h1>
|
||||
|
||||
<p><a href="../dashboard">[Dashboard]</a> <a href="lightnings">[Lightnings]</a> <a href="ombrometers">[Ombrometers]</a> <a href="../api.html">[Request API]</a> | <a href="../dashboard/{{.Id}}/edit">[Edit station]</a></p>
|
||||
|
||||
<table border="1">
|
||||
<tr><td>{{.Name}}</td><td>{{.Description}}</td><td>{{.T}} °C</td><td>{{.Hum}} rel. humidity</td><td>{{.P}} hPa</td></tr>
|
||||
</table>
|
||||
|
||||
<FORM method="GET">
|
||||
<p>Goto <input type="date" name="date" value="{{.SelectedDate}}"> <input type="submit" value="Go"> | <a href="../dashboard/{{.Id}}?timespan=day">[Day]</a> <a href="../dashboard/{{.Id}}?timespan=week">[Week]</a> <a href="../dashboard/{{.Id}}?timespan=month">[Month]</a> <a href="../dashboard/{{.Id}}?timespan=year">[Year]</a></p>
|
||||
</FORM>
|
||||
</html>
|
|
@ -1,27 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>meteo</title>
|
||||
<script src="../../asset/Chart.js"></script>
|
||||
<LINK href="../../asset/meteo.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>meteo | Station</h1>
|
||||
|
||||
<p><a href="../../dashboard">[Dashboard]</a> <a href="lightnings">[Lightnings]</a> <a href="ombrometers">[Ombrometers]</a> <a href="../../api.html">[Request API]</a> | <a href="../{{.Id}}">[Back to station]</a></p>
|
||||
|
||||
<h2>Edit station</h2>
|
||||
|
||||
<FORM ACTION="edit" METHOD="POST">
|
||||
|
||||
<table border="1">
|
||||
<tr><td>Name</td><td><input type="text" name="name" value="{{.Name}}"></td></tr>
|
||||
<tr><td>Description</td><td><input type="text" name="desc" value="{{.Description}}"></td></tr>
|
||||
<tr><td>Location</td><td><input type="text" name="location" value="{{.Location}}"></td></tr>
|
||||
</table>
|
||||
|
||||
<p><input type="submit" value="Edit"><input type="reset" value="Reset"></p>
|
||||
|
||||
</FORM>
|
||||
</html>
|
Loading…
Reference in a new issue