Initial commit

This commit is contained in:
Jan Zípek 2022-08-13 23:33:50 +02:00
commit f6de3fdfbf
Signed by: kamen
GPG Key ID: A17882625B33AC31
20 changed files with 702 additions and 0 deletions

6
.env.example Normal file
View File

@ -0,0 +1,6 @@
GIN_MODE=debug
DATABASE_URL=./sensors.sqlite3
PORT=8083
BIND_IP=localhost
AUTH_USERNAME=admin
AUTH_PASSWORD=password

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.env
basic-sensor-receiver.exe
basic-sensor-receiver

34
app/database.go Normal file
View File

@ -0,0 +1,34 @@
package app
import "database/sql"
func initializeDb(databaseUrl string) *sql.DB {
db, err := sql.Open("sqlite3", databaseUrl)
if err != nil {
panic(err)
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS sensor_values (
timestamp INTEGER NOT NULL,
sensor TEXT NOT NULL,
value REAL NOT NULL
);`)
if err != nil {
panic(err)
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS sensor_config (
sensor TEXT NOT NULL,
key TEXT NOT NULL,
value REAL NOT NULL,
PRIMARY KEY (sensor, key)
);`)
if err != nil {
panic(err)
}
return db
}

46
app/sensor_config.go Normal file
View File

@ -0,0 +1,46 @@
package app
import (
"net/http"
"github.com/gin-gonic/gin"
)
type sensorConfigValue struct {
Value string `json:"value"`
}
func (s *Server) HandlePutSensorConfig() gin.HandlerFunc {
return func(c *gin.Context) {
var configValue sensorConfigValue
sensor := c.Param("sensor")
key := c.Param("key")
if err := c.BindJSON(&configValue); err != nil {
c.AbortWithError(400, err)
return
}
if err := s.Services.SensorConfig.SetValue(sensor, key, configValue.Value); err != nil {
c.AbortWithError(500, err)
return
}
c.Writer.WriteHeader(http.StatusOK)
}
}
func (s *Server) GetSensorConfig() gin.HandlerFunc {
return func(c *gin.Context) {
sensor := c.Param("sensor")
config, err := s.Services.SensorConfig.GetValues(sensor)
if err != nil {
c.AbortWithError(500, err)
return
}
c.JSON(http.StatusOK, config)
}
}

56
app/sensor_values.go Normal file
View File

@ -0,0 +1,56 @@
package app
import (
"net/http"
"github.com/gin-gonic/gin"
)
type postSensorValueBody struct {
Value float64 `json:"value"`
}
type getSensorQuery struct {
From int64 `form:"from"`
To int64 `form:"to"`
}
func (s *Server) HandlePostSensorValues() gin.HandlerFunc {
return func(c *gin.Context) {
var newValue postSensorValueBody
sensor := c.Param("sensor")
if err := c.BindJSON(&newValue); err != nil {
c.AbortWithError(400, err)
return
}
if _, err := s.Services.SensorValues.Push(sensor, newValue.Value); err != nil {
c.AbortWithError(400, err)
return
}
c.JSON(http.StatusCreated, newValue)
}
}
func (s *Server) HandleGetSensorValues() gin.HandlerFunc {
return func(c *gin.Context) {
var query getSensorQuery
sensor := c.Param("sensor")
if err := c.BindQuery(&query); err != nil {
c.AbortWithError(500, err)
return
}
values, err := s.Services.SensorValues.GetList(sensor, query.From, query.To)
if err != nil {
c.AbortWithError(500, err)
return
}
c.JSON(http.StatusOK, values)
}
}

20
app/sensors.go Normal file
View File

@ -0,0 +1,20 @@
package app
import (
"net/http"
"github.com/gin-gonic/gin"
)
func (s *Server) GetSensors() gin.HandlerFunc {
return func(c *gin.Context) {
sensors, err := s.Services.Sensors.GetList()
if err != nil {
c.AbortWithError(500, err)
return
}
c.JSON(http.StatusOK, sensors)
}
}

25
app/server.go Normal file
View File

@ -0,0 +1,25 @@
package app
import (
"basic-sensor-receiver/config"
"basic-sensor-receiver/services"
"database/sql"
)
type Server struct {
DB *sql.DB
Config *config.Config
Services *services.Services
}
func InitializeServer() *Server {
server := Server{}
server.Config = config.LoadConfig()
server.DB = initializeDb(server.Config.DatabaseUrl)
ctx := services.Context{DB: server.DB, Config: server.Config}
server.Services = services.InitializeServices(&ctx)
return &server
}

26
client/css/style.css Normal file
View File

@ -0,0 +1,26 @@
body {
background: #eee;
}
#sensors-container {
display: grid;
grid-template-columns: 1fr 1fr;
}
.sensor {
background: #fff;
border-radius: 0.5rem;
margin: 0.5rem;
box-shadow: 0px 10px 15px -3px rgba(0,0,0,0.1);
overflow: hidden;
}
.sensor .header {
display: flex;
align-items: center;
padding: 0.5rem;
}
.sensor .header .actions {
margin-left: auto;
}

15
client/index.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sensors</title>
<link rel="stylesheet" href="css/style.css" />
<script src="https://cdn.plot.ly/plotly-2.14.0.min.js"></script>
<script src="js/lighterhtml@4.2.0.min.js"></script>
</head>
<body>
<div id="sensors-container"></div>
<script src="js/index.js"></script>
</body>
</html>

91
client/js/index.js Normal file
View File

@ -0,0 +1,91 @@
const { html, render } = lighterhtml;
const $container = document.getElementById("sensors-container");
function load() {
fetch("/api/sensors")
.then((r) => r.json())
.then((sensors) => {
const components = sensors.map((sensor) => createSensor(sensor));
components.forEach((component) =>
$container.appendChild(component.container)
);
});
}
function createSensor(sensor) {
const container = document.createElement("div");
container.className = "sensor";
const header = document.createElement("div");
header.className = "header";
const body = document.createElement("div");
body.className = "body";
const renderHeader = () => {
render(
header,
html`
<div class="name">${sensor.config?.name ?? sensor.sensor}</div>
<div class="actions">
<button class="config">Config</button>
<button class="refresh" onClick=${refreshValues}>Refresh</button>
</div>
`
);
};
const renderBody = (range, values) => {
const { from, to } = range;
Plotly.newPlot(
body,
[
{
type: "lines",
x: values.map((v) => new Date(v.timestamp * 1000)),
y: values.map((v) => v.value),
},
],
{
xaxis: { range: [from, to], type: "date" },
margin: {
l: 50,
r: 20,
b: 60,
t: 20,
pad: 5,
},
},
{
responsive: true,
}
);
};
const refreshValues = () => {
const from = new Date(Date.now() - 5 * 24 * 3600 * 1000);
const to = new Date();
fetch(
`/api/sensors/${sensor.sensor}/values?from=${Math.round(
from.getTime() / 1000
)}&to=${Math.round(to.getTime() / 1000)}`
)
.then((r) => r.json())
.then((values) => renderBody({ from, to }, values));
};
renderHeader();
refreshValues();
container.appendChild(header);
container.appendChild(body);
return {
container,
};
}
load();

7
client/js/lighterhtml@4.2.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

34
config/config.go Normal file
View File

@ -0,0 +1,34 @@
package config
import (
"os"
"github.com/golobby/dotenv"
)
type Config struct {
Mode string `env:"GIN_MODE"`
DatabaseUrl string `env:"DATABASE_URL"`
Port int `env:"PORT"`
Ip string `env:"BIND_IP"`
AuthUsername string `env:"AUTH_USERNAME"`
AuthPassword string `env:"AUTH_PASSWORD"`
}
func LoadConfig() *Config {
config := Config{}
file, err := os.Open(".env")
if err != nil {
panic(err)
}
err = dotenv.NewDecoder(file).Decode(&config)
if err != nil {
panic(err)
}
return &config
}

31
go.mod Normal file
View File

@ -0,0 +1,31 @@
module basic-sensor-receiver
go 1.17
require (
github.com/gin-gonic/gin v1.8.1
github.com/golobby/dotenv v1.3.1
github.com/mattn/go-sqlite3 v1.14.14
)
require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.0 // indirect
github.com/goccy/go-json v0.9.10 // indirect
github.com/golobby/cast v1.3.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
golang.org/x/net v0.0.0-20220812174116-3211cb980234 // indirect
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

105
go.sum Normal file
View File

@ -0,0 +1,105 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc=
github.com/goccy/go-json v0.9.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golobby/cast v1.3.0 h1:8nM9nYU5Pzi1LWXwISx0xhW/7oWXPt9r0hdTC1nnPSI=
github.com/golobby/cast v1.3.0/go.mod h1:WCusT3z1fzp4XVBUGbWy61insoQS8CPJHNTQwlW8qnM=
github.com/golobby/dotenv v1.3.1 h1:BvQyNuOQITmIXNHpQ/FUG2gZcUGmcGMyODMeUfiKkeU=
github.com/golobby/dotenv v1.3.1/go.mod h1:EWUdOzuDlA1g4hdjo++WD37DhNZw33Oce8ryH3liZTQ=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw=
github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E=
golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

33
main.go Normal file
View File

@ -0,0 +1,33 @@
package main
import (
"basic-sensor-receiver/app"
"fmt"
"github.com/gin-gonic/gin"
_ "github.com/mattn/go-sqlite3"
)
func main() {
server := app.InitializeServer()
gin.SetMode(server.Config.Mode)
router := gin.Default()
protected := router.Group("/", gin.BasicAuth(gin.Accounts{
server.Config.AuthUsername: server.Config.AuthPassword,
}))
protected.StaticFile("/", "client/index.html")
protected.Static("/js", "client/js")
protected.Static("/css", "client/css")
protected.GET("/api/sensors", server.GetSensors())
protected.GET("/api/sensors/:sensor/values", server.HandleGetSensorValues())
protected.POST("/api/sensors/:sensor/values", server.HandlePostSensorValues())
protected.GET("/api/sensors/:sensor/config", server.GetSensorConfig())
protected.PUT("/api/sensors/:sensor/config/:key", server.HandlePutSensorConfig())
router.Run(fmt.Sprintf("%s:%d", server.Config.Ip, server.Config.Port))
}

BIN
sensors.sqlite3 Normal file

Binary file not shown.

View File

@ -0,0 +1,41 @@
package services
type SensorConfigService struct {
ctx *Context
}
func (s *SensorConfigService) SetValue(sensor string, key string, value string) error {
_, err := s.ctx.DB.Exec("INSERT OR REPLACE INTO sensor_config (sensor, key, value) VALUES (?, ?, ?)", sensor, key, value)
return err
}
func (s *SensorConfigService) GetValues(sensor string) (map[string]string, error) {
var key string
var value string
config := make(map[string]string)
rows, err := s.ctx.DB.Query("SELECT key, value FROM sensor_config WHERE sensor = ?", sensor)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&key, &value)
if err != nil {
return nil, err
}
config[key] = value
}
err = rows.Err()
if err != nil {
return nil, err
}
return config, nil
}

View File

@ -0,0 +1,52 @@
package services
import "time"
type SensorValuesService struct {
ctx *Context
}
type sensorValue struct {
Timestamp int64 `json:"timestamp"`
Value float64 `json:"value"`
}
func (s *SensorValuesService) Push(sensor string, value float64) (int64, error) {
res, err := s.ctx.DB.Exec("INSERT INTO sensor_values (timestamp, sensor, value) VALUES (?, ?, ?)", time.Now().Unix(), sensor, value)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
func (s *SensorValuesService) GetList(sensor string, from int64, to int64) ([]sensorValue, error) {
var value float64
var timestamp int64
var values []sensorValue
rows, err := s.ctx.DB.Query("SELECT timestamp, value FROM sensor_values WHERE sensor = ? AND timestamp > ? AND timestamp < ? ORDER BY timestamp ASC", sensor, from, to)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&timestamp, &value)
if err != nil {
return nil, err
}
values = append(values, sensorValue{Timestamp: timestamp, Value: value})
}
err = rows.Err()
if err != nil {
return nil, err
}
return values, nil
}

View File

@ -0,0 +1,47 @@
package services
type SensorsService struct {
ctx *Context
}
type sensorItem struct {
Sensor string `json:"sensor"`
LastUpdate string `json:"lastUpdate"`
Config map[string]string `json:"config"`
}
func (s *SensorsService) GetList() ([]sensorItem, error) {
var sensors []sensorItem
var sensor string
var lastUpdate string
rows, err := s.ctx.DB.Query("SELECT sensor, MAX(timestamp) last_update FROM sensor_values GROUP BY sensor")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&sensor, &lastUpdate)
if err != nil {
return nil, err
}
config, err := s.ctx.Services.SensorConfig.GetValues(sensor)
if err != nil {
return nil, err
}
sensors = append(sensors, sensorItem{Sensor: sensor, LastUpdate: lastUpdate, Config: config})
}
err = rows.Err()
if err != nil {
return nil, err
}
return sensors, nil
}

30
services/services.go Normal file
View File

@ -0,0 +1,30 @@
package services
import (
"basic-sensor-receiver/config"
"database/sql"
)
type Services struct {
SensorConfig *SensorConfigService
SensorValues *SensorValuesService
Sensors *SensorsService
}
type Context struct {
DB *sql.DB
Config *config.Config
Services *Services
}
func InitializeServices(ctx *Context) *Services {
services := Services{}
ctx.Services = &services
services.SensorConfig = &SensorConfigService{ctx: ctx}
services.SensorValues = &SensorValuesService{ctx: ctx}
services.Sensors = &SensorsService{ctx: ctx}
return &services
}