Moved the webapp to a webpack with some typescript, updated server wtih new endpoints

This commit is contained in:
Daniel Ledda
2020-11-14 01:06:29 +01:00
parent 6dba158ff0
commit 4e57a8eb47
21 changed files with 11118 additions and 20964 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
.idea .idea
node_modules

View File

@@ -4,6 +4,7 @@ import (
"database/sql" "database/sql"
_ "github.com/Freeaqingme/golang-sql-driver-mysql" _ "github.com/Freeaqingme/golang-sql-driver-mysql"
"fmt" "fmt"
"time"
) )
var ClimateDb *sql.DB var ClimateDb *sql.DB
@@ -66,10 +67,10 @@ func getLastSnapshotRecordFromDb() (*SnapshotRecord, error) {
return &snapshotRecord, nil return &snapshotRecord, nil
} }
func getSnapshotRecordsFromDb(minuteAgo int) ([]*SnapshotRecord, error) { func getSnapshotRecordsFromDb(dateSince time.Time) ([]*SnapshotRecord, error) {
snapshots := make([]*SnapshotRecord, 0) snapshots := make([]*SnapshotRecord, 0)
query := "SELECT `%s`, `%s`, `%s`, `%s`, `%s` FROM `snapshots` WHERE `%s` > date_sub(now(), interval %v minute) ORDER BY `id` DESC;" query := "SELECT `%s`, `%s`, `%s`, `%s`, `%s` FROM `snapshots` WHERE `%s` > %s ORDER BY `id` DESC;"
rows, err := ClimateDb.Query(fmt.Sprintf(query, "id", "temp", "humidity", "co2", "time", "time", minuteAgo)) rows, err := ClimateDb.Query(fmt.Sprintf(query, "id", "temp", "humidity", "co2", "time", "time", dateSince.String()))
if err != nil { if err != nil {
return nil, fmt.Errorf("couldn't execute select query: %w", err) return nil, fmt.Errorf("couldn't execute select query: %w", err)
} }

View File

@@ -13,16 +13,23 @@ type SnapshotPayload struct {
type SnapshotRecord struct { type SnapshotRecord struct {
Id int `json:"id"` Id int `json:"id"`
SnapshotSubmission JsonSnapshotSubmission
} }
type SnapshotSubmission struct { type JsonSnapshotSubmission struct {
Timestamp string `json:"time"` Timestamp string `json:"time"`
Temp float32 `json:"temp"` Temp float32 `json:"temp"`
Humidity float32 `json:"humidity"` Humidity float32 `json:"humidity"`
Co2 float32 `json:"co2"` Co2 float32 `json:"co2"`
} }
type SnapshotSubmission struct {
Timestamp interface{}
Temp interface{}
Humidity interface{}
Co2 interface{}
}
func createSnapshotSubFromJsonBodyStream(jsonBodyStream io.ReadCloser) (*SnapshotSubmission, error) { func createSnapshotSubFromJsonBodyStream(jsonBodyStream io.ReadCloser) (*SnapshotSubmission, error) {
var snapshotSub SnapshotSubmission var snapshotSub SnapshotSubmission
body, err := ioutil.ReadAll(jsonBodyStream) body, err := ioutil.ReadAll(jsonBodyStream)

View File

@@ -1,161 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ledda's Room Climate</title>
<script src="/climate/static/Chart.bundle.min.js"></script>
</head>
<body>
<div class="chart-container" style="position: relative; height:40vh; width:80vw; margin: auto">
<canvas id="myChart"></canvas>
</div>
<script type="application/javascript">
const ROOT_URL = "climate/";
const humidityColor = 'rgb(45,141,45)';
const tempColor = 'rgb(0,134,222)';
const co2Color = 'rgb(194,30,30)';
let minutesDisplayed = 60;
async function getData() {
let urlEndpoint = "/" + ROOT_URL + "data/";
const pathname = window.location.pathname;
if (pathname.includes("since")) {
minutesDisplayed = Number(pathname.slice(pathname.lastIndexOf("/") + 1));
urlEndpoint += "since/" + minutesDisplayed;
}
const data = await fetch(urlEndpoint);
return transformData(await data.json());
}
async function initChart() {
const ctx = document.getElementById('myChart').getContext('2d');
const {humidity, temp, co2} = await getData();
const myChart = Chart.Line(ctx, {
data: {
datasets: [{
label: 'Humidity',
data: humidity,
borderColor: humidityColor,
fill: false,
id: 'humidity',
yAxisID: 'y-axis-3',
}, {
label: 'Temperature',
data: temp,
borderColor: tempColor,
fill: false,
id: 'temp',
yAxisID: 'y-axis-2',
}, {
label: 'Co2 Concentration',
data: co2,
borderColor: co2Color,
fill: false,
id: 'co2',
yAxisID: 'y-axis-1',
}]
},
options: {
stacked: false,
title: {
display: true,
text: 'Ledda\'s Room Climate',
},
scales: {
xAxes: [{
type: 'time',
time: {
unit: 'second'
}
}],
yAxes: [{
type: 'linear',
display: true,
position: 'right',
id: 'y-axis-1',
ticks: {
fontColor: co2Color,
suggestedMin: 400,
suggestedMax: 1100,
},
}, {
type: 'linear',
display: true,
position: 'left',
id: 'y-axis-2',
ticks: {
fontColor: tempColor,
suggestedMin: 10,
suggestedMax: 35,
},
gridLines: {
drawOnChartArea: false,
},
}, {
type: 'linear',
display: true,
position: 'left',
id: 'y-axis-3',
ticks: {
fontColor: humidityColor,
suggestedMin: 15,
suggestedMax: 85,
},
gridLines: {
drawOnChartArea: false,
},
}],
}
},
});
startFetchTimeout(myChart);
}
function transformData(json) {
const humidity = [];
const co2 = [];
const temp = [];
for (let i = 0; i < json.snapshots.length; i++) {
const snapshot = json.snapshots[json.snapshots.length - i - 1];
co2.push({x: snapshot.time, y: snapshot.co2});
temp.push({x: snapshot.time, y: snapshot.temp});
humidity.push({x: snapshot.time, y: snapshot.humidity});
}
return {humidity, co2, temp};
}
function startFetchTimeout(chart) {
setInterval(async () => {
const newDatum = await fetch("/" + ROOT_URL + "data/last/");
const snapshot = (await newDatum.json()).snapshots[0];
const latestTime = chart.data.datasets[0].data[chart.data.datasets[0].data.length - 1].x;
if (snapshot.time !== latestTime) {
removeExpiredData(chart, snapshot.time);
insertSnapshot(chart, snapshot);
}
}, 30 * 1000);
}
function insertSnapshot(chart, snapshot) {
chart.data.datasets[0].data.push({x: snapshot.time, y: snapshot.humidity});
chart.data.datasets[1].data.push({x: snapshot.time, y: snapshot.temp});
chart.data.datasets[2].data.push({x: snapshot.time, y: snapshot.co2});
chart.update();
}
function removeExpiredData(chart, latestTime) {
for (let i = 0; i < chart.data.datasets[0].data.length; i++) {
if ((Date.parse(latestTime) - Date.parse(chart.data.datasets[0].data[i].x)) > minutesDisplayed * 60000) {
chart.data.datasets[0].data.splice(i, 1);
chart.data.datasets[1].data.splice(i, 1);
chart.data.datasets[2].data.splice(i, 1);
}
}
}
initChart();
</script>
</body>
</html>

28
climate-pinger.py Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/local/bin/python3
import adafruit_dht
import mh_z19
from sys import stderr
from board import D4
from datetime import datetime
try:
dhtDevice = adafruit_dht.DHT22(D4)
temp = dhtDevice.temperature
humidity = dhtDevice.humidity
co2 = mh_z19.read()
if co2 is not None:
co2 = co2['co2']
else:
raise RuntimeError()
print(
'Time:', str(datetime.now()),
'Temp:', temp,
'Humidity:', humidity,
'CO2:', co2,
sep='\n',
)
except Exception as error:
print('err:', error, file=sys.stderr)
dhtDevice.exit()

BIN
climate-server Executable file

Binary file not shown.

View File

@@ -6,7 +6,11 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"log" "log"
"net/http" "net/http"
"os"
"os/exec"
"strconv" "strconv"
"strings"
"time"
) )
const DEBUG = true const DEBUG = true
@@ -38,20 +42,23 @@ func startServer() {
port := "8001" port := "8001"
r := mux.NewRouter() r := mux.NewRouter()
r.HandleFunc("/", showCharts).Methods("GET") r.HandleFunc("/", showCharts).Methods("GET")
r.HandleFunc("/since/{mins}", showCharts).Methods("GET") r.HandleFunc("/", showCharts).Methods("GET").Queries("show-minutes", "{[0-9]+}")
r.HandleFunc("/data/", sendData).Methods("GET")
r.HandleFunc("/data/since/{mins}", sendData).Methods("GET")
r.HandleFunc("/data/last/", sendLastSnapshot).Methods("GET")
r.HandleFunc("/", saveSnapshot).Methods("POST") r.HandleFunc("/", saveSnapshot).Methods("POST")
r.HandleFunc("/data", sendData).Methods("GET")
r.HandleFunc("/data", sendData).Methods("GET").Queries("since", "")
r.HandleFunc("/data/now", createAndSendSnapshot).Methods("GET")
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static/")))) r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static/"))))
http.Handle("/", r) http.Handle("/", r)
fmt.Printf("Listening on port %s...\n", port) fmt.Printf("Listening on port %s...\n", port)
quitSnapshot := make(chan int, 1)
go saveSnapshotOnInterval(30, quitSnapshot)
log.Fatal(http.ListenAndServe(":"+port, nil)) log.Fatal(http.ListenAndServe(":"+port, nil))
} }
func showCharts(w http.ResponseWriter, r *http.Request) { func showCharts(w http.ResponseWriter, r *http.Request) {
if countStr := mux.Vars(r)["mins"]; countStr != "" { minutes := r.FormValue("show-minutes")
if _, err := strconv.ParseInt(countStr, 10, 0); err != nil { if minutes != "" {
if _, err := strconv.ParseInt(minutes, 10, 0); err != nil {
http.Redirect(w, r, "/" + ROOT_URL + "/", 303) http.Redirect(w, r, "/" + ROOT_URL + "/", 303)
} }
} }
@@ -59,15 +66,16 @@ func showCharts(w http.ResponseWriter, r *http.Request) {
} }
func sendData(w http.ResponseWriter, r *http.Request) { func sendData(w http.ResponseWriter, r *http.Request) {
var count int64 = 60 dateSince := time.Now()
if vars := mux.Vars(r); vars["mins"] != "" { sinceQuery := r.FormValue("since")
newCount, err := strconv.ParseInt(vars["mins"], 10, 0) if sinceQuery != "" {
newDateSince, err := time.Parse(time.RFC3339Nano, sinceQuery)
if err != nil { if err != nil {
http.Redirect(w, r, "/" + ROOT_URL + "/", 303) http.Redirect(w, r, "/" + ROOT_URL + "/", 303)
} }
count = newCount dateSince = newDateSince
} }
records, err := getSnapshotRecordsFromDb(int(count)) records, err := getSnapshotRecordsFromDb(dateSince)
if internalErrorOnErr(fmt.Errorf("couldn't get snapshots from db: %w", err), w, r) { return } if internalErrorOnErr(fmt.Errorf("couldn't get snapshots from db: %w", err), w, r) { return }
json, err := createJsonFromSnapshotRecords(records) json, err := createJsonFromSnapshotRecords(records)
if internalErrorOnErr(fmt.Errorf("couldn't create json from snapshots: %w", err), w, r) { return } if internalErrorOnErr(fmt.Errorf("couldn't create json from snapshots: %w", err), w, r) { return }
@@ -75,10 +83,13 @@ func sendData(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, string(json)) fmt.Fprintf(w, string(json))
} }
func sendLastSnapshot(w http.ResponseWriter, r *http.Request) { func createAndSendSnapshot(w http.ResponseWriter, r *http.Request) {
record, err := getLastSnapshotRecordFromDb() snapshot, err := getSnapshotFromPythonScript()
if internalErrorOnErr(fmt.Errorf("couldn't get last snapshot from db: %w", err), w, r) { return } if internalErrorOnErr(fmt.Errorf("couldn't create a snapshot: %w", err), w, r) { return }
json, err := createJsonFromSnapshotRecords([]*SnapshotRecord{record}) _, err = writeSnapshotToDb(snapshot)
if internalErrorOnErr(fmt.Errorf("couldn't save snapshot to db: %w", err), w, r) { return }
dbSnapshotRecord, err := getLastSnapshotRecordFromDb()
json, err := createJsonFromSnapshotRecords([]*SnapshotRecord{dbSnapshotRecord})
if internalErrorOnErr(fmt.Errorf("couldn't create json from last snapshots: %w", err), w, r) { return } if internalErrorOnErr(fmt.Errorf("couldn't create json from last snapshots: %w", err), w, r) { return }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, string(json)) fmt.Fprintf(w, string(json))
@@ -92,6 +103,54 @@ func saveSnapshot(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "{id: %v}", newId) fmt.Fprintf(w, "{id: %v}", newId)
} }
func saveSnapshotOnInterval(seconds int64, stop chan int) {
var err error = nil
for {
if err != nil {
log.Println(err.Error())
}
snapshot, err := getSnapshotFromPythonScript()
if err != nil {
continue
}
_, err = writeSnapshotToDb(snapshot)
if err != nil {
continue
}
select {
case <-stop:
break
default:
time.Sleep(time.Duration(seconds) * time.Second)
}
}
}
func getSnapshotFromPythonScript() (*SnapshotSubmission, error) {
process := exec.Command("climate-pinger.py")
process.Stdout = os.Stdout
process.Stderr = os.Stderr
err := process.Run()
if err != nil {
return nil, err
}
output, err := process.Output()
if err != nil {
return nil, err
}
tokens := strings.Split(string(output), "\n")
if len(tokens) != 4 {
return nil, errors.New(fmt.Sprintf("Strange python output: %s", output))
}
snapshot := SnapshotSubmission{
Timestamp: tokens[0],
Temp: tokens[1],
Humidity: tokens[2],
Co2: tokens[3],
}
return &snapshot, nil
}
func internalErrorOnErr(err error, w http.ResponseWriter, r *http.Request) bool { func internalErrorOnErr(err error, w http.ResponseWriter, r *http.Request) bool {
if errors.Unwrap(err) != nil { if errors.Unwrap(err) != nil {
errorMessage := "Internal Server Error!" errorMessage := "Internal Server Error!"

12
go.mod Normal file
View File

@@ -0,0 +1,12 @@
module djledda.de/ledda/climate-server
go 1.15
require (
github.com/Freeaqingme/golang-sql-driver-mysql v1.0.3
github.com/go-sql-driver/mysql v1.5.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.1
github.com/warthog618/gpio v1.0.0 // indirect
)

186
go.sum Normal file
View File

@@ -0,0 +1,186 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Freeaqingme/golang-sql-driver-mysql v1.0.3 h1:4Pcqr4AXBl6kB5NX7F5XK65XHx+6Hv6r5qBnpZSULt0=
github.com/Freeaqingme/golang-sql-driver-mysql v1.0.3/go.mod h1:CnKoDB/Q8PTUA5qdVFcqULeAe9Vkbl/YtqKm9HPhl5A=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.15+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.11.2/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/warthog618/config v0.4.1/go.mod h1:IzcIkVay6dCubN3WBAJzPuqHyE1fTPxICvKTQ/2JA9g=
github.com/warthog618/gpio v1.0.0 h1:jk16Fu1fLnUbqhC7O7Og/LerYegZYMYDQeXZYKbP6Zg=
github.com/warthog618/gpio v1.0.0/go.mod h1:3yuGbOkcAcs8/pRFEnCnN7Qt2S+TkISbFXM+5gliAZM=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.2.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.48.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1937
webapp/dist/main.js vendored Normal file

File diff suppressed because one or more lines are too long

8487
webapp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
webapp/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "climate-ranger-frontend",
"version": "0.0.1",
"description": "Frontend for displaying info about room climate",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"dev": "webpack-dev-server"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@webpack-cli/init": "^1.0.3",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"css-loader": "^5.0.1",
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.0.3",
"ts-loader": "^8.0.10",
"typescript": "^4.0.5",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0",
"webpack-dev-server": "^3.11.0",
"prettier": "^2.1.2"
},
"dependencies": {
"@types/chart.js": "^2.9.27",
"chart.js": "^2.9.4"
}
}

110
webapp/src/ClimateChart.ts Normal file
View File

@@ -0,0 +1,110 @@
import Chart, {ChartPoint} from "chart.js";
import {generateClimateChartConfig} from "./climateChartConfig";
interface Snapshot {
id: number,
temp: number,
humidity: number,
co2: number,
time: string,
}
interface SnapshotRecords {
snapshots: Snapshot[]
}
class ClimateChart {
private readonly urlEndpoint: string;
private chart: Chart | null;
private latestSnapshot: Snapshot | null;
private onLoadedCallback: () => void = () => {};
constructor(
private readonly rootUrl: string,
private readonly canvasId: string,
private readonly minutesDisplayed: number = 60
) {
if (minutesDisplayed > 0 && Math.floor(minutesDisplayed) == minutesDisplayed) {
this.minutesDisplayed = minutesDisplayed;
} else {
throw new Error(`invalid minutes passed to display in chart: ${minutesDisplayed}`);
}
this.urlEndpoint = this.rootUrl + "data/";
this.urlEndpoint += "since/" + this.minutesDisplayed;
this.initChart()
}
onLoaded(callback: () => void) {
this.onLoadedCallback = callback;
}
private static isCanvas(el: HTMLElement): el is HTMLCanvasElement {
return el.tagName === "canvas";
}
private async getInitialDataBlob(): Promise<SnapshotRecords> {
const data = await fetch(this.urlEndpoint);
return await data.json();
}
private async initChart() {
const canvasElement = document.getElementById(this.canvasId);
let ctx: CanvasRenderingContext2D;
if (ClimateChart.isCanvas(canvasElement)) {
ctx = canvasElement.getContext('2d');
} else {
throw new Error(`improper HTML element passed, needed type canvas, got ${canvasElement.tagName}`);
}
const payload = await this.getInitialDataBlob();
this.latestSnapshot = payload.snapshots[payload.snapshots.length - 1];
this.chart = new Chart(ctx, generateClimateChartConfig(this.jsonToChartPoints(payload)));
setInterval(() => this.updateFromServer(), 30 * 1000);
this.onLoadedCallback();
}
private jsonToChartPoints(json: SnapshotRecords): {humidity: ChartPoint[], temp: ChartPoint[], co2: ChartPoint[]} {
const humidity = [];
const co2 = [];
const temp = [];
for (let i = 0; i < json.snapshots.length; i++) {
const snapshot = json.snapshots[json.snapshots.length - i - 1];
co2.push({x: snapshot.time, y: snapshot.co2});
temp.push({x: snapshot.time, y: snapshot.temp});
humidity.push({x: snapshot.time, y: snapshot.humidity});
}
return {humidity, co2, temp};
}
private async updateFromServer() {
const currentTime = (new Date(this.latestSnapshot.time)).toISOString();
const url = "/" + this.rootUrl + "data?since=" + currentTime;
const payload: SnapshotRecords = await (await fetch(url)).json();
if (payload.snapshots.length > 0) {
this.latestSnapshot = payload.snapshots[payload.snapshots.length - 1];
this.removeExpiredData(currentTime);
this.insertSnapshots(...payload.snapshots);
this.chart.update();
}
}
private insertSnapshots(...snapshots: Snapshot[]) {
for (const snapshot of snapshots) {
this.chart.data.datasets[0].data.push(snapshot.humidity);
this.chart.data.datasets[1].data.push(snapshot.temp);
this.chart.data.datasets[2].data.push(snapshot.co2);
}
}
private removeExpiredData(latestTime: string) {
for (let i = 0; i < this.chart.data.datasets[0].data.length; i++) {
const timeOnPoint = (this.chart.data.datasets[0].data[i] as ChartPoint).x as string;
const timeElapsedSincePoint = Date.parse(latestTime) - Date.parse(timeOnPoint);
if (timeElapsedSincePoint > this.minutesDisplayed * 60000) {
this.chart.data.datasets[0].data.splice(i, 1);
this.chart.data.datasets[1].data.splice(i, 1);
this.chart.data.datasets[2].data.splice(i, 1);
}
}
}
}
export default ClimateChart;

View File

@@ -0,0 +1,94 @@
import {ChartConfiguration, ChartPoint, TimeUnit} from "chart.js";
interface ClimateChartSettings {
humidity: ChartPoint[];
temp: ChartPoint[];
co2: ChartPoint[];
colors?: {
humidity?: string;
temp?: string;
co2?: string;
}
}
const defaultHumidityColor = 'rgb(45,141,45)';
const defaultTempColor = 'rgb(0,134,222)';
const defaultCo2Color = 'rgb(194,30,30)';
export function generateClimateChartConfig(settings: ClimateChartSettings): ChartConfiguration {
return {
type: 'line',
data: {
datasets: [{
label: 'Humidity',
data: settings.humidity,
borderColor: settings.colors?.humidity ?? defaultHumidityColor,
fill: false,
yAxisID: 'y-axis-3',
}, {
label: 'Temperature',
data: settings.temp,
borderColor: settings.colors?.temp ?? defaultTempColor,
fill: false,
yAxisID: 'y-axis-2',
}, {
label: 'Co2 Concentration',
data: settings.co2,
borderColor: settings.colors?.co2 ?? defaultCo2Color,
fill: false,
yAxisID: 'y-axis-1',
}]
},
options: {
title: {
display: true,
text: 'Ledda\'s Room Climate',
},
scales: {
xAxes: [{
type: 'time',
time: {
unit: 'second' as TimeUnit
}
}],
yAxes: [{
type: 'linear',
display: true,
position: 'right',
id: 'y-axis-1',
ticks: {
fontColor: settings.colors?.co2 ?? defaultCo2Color,
suggestedMin: 400,
suggestedMax: 1100,
},
}, {
type: 'linear',
display: true,
position: 'left',
id: 'y-axis-2',
ticks: {
fontColor: settings.colors?.temp ?? defaultTempColor,
suggestedMin: 10,
suggestedMax: 35,
},
gridLines: {
drawOnChartArea: false,
},
}, {
type: 'linear',
display: true,
position: 'left',
id: 'y-axis-3',
ticks: {
fontColor: settings.colors?.humidity ?? defaultHumidityColor,
suggestedMin: 15,
suggestedMax: 85,
},
gridLines: {
drawOnChartArea: false,
},
}],
}
},
}
}

27
webapp/src/main.ts Normal file
View File

@@ -0,0 +1,27 @@
import ClimateChart from "./ClimateChart";
const ROOT_URL: string = "climate/";
const CHART_DOM_ID: string = "myChart";
function createClimateChart() {
const pathname = window.location.pathname;
let minutesDisplayed = 60;
const argsStart = pathname.search(/\?minute-span=/);
if (argsStart !== -1) {
const parsedMins = Number(pathname[12]);
if (!isNaN(parsedMins) && parsedMins > 0) {
minutesDisplayed = parsedMins;
}
}
return new ClimateChart(ROOT_URL, CHART_DOM_ID, minutesDisplayed);
}
const overlay = document.createElement('div');
overlay.innerText = 'Loading data...';
overlay.className = 'overlay';
document.getRootNode().appendChild(overlay);
const climateChart = createClimateChart();
climateChart.onLoaded(() => {
overlay.classList.add('hidden');
})

14
webapp/static/charts.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ledda's Room Climate</title>
<link href="/climate/static/styles.css" rel="stylesheet" />
<script src="/climate/static/main.js"></script>
</head>
<body>
<div class="chart-container" style="position: relative; height:40vh; width:80vw; margin: auto">
<canvas id="myChart"></canvas>
</div>
</body>
</html>

15
webapp/static/styles.css Normal file
View File

@@ -0,0 +1,15 @@
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: white;
opacity: 50%;
display: table-cell;
text-align: center;
}
.hidden {
display: none;
}

10
webapp/tsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"allowJs": true,
"moduleResolution": "Node",
}
}

80
webapp/webpack.config.js Normal file
View File

@@ -0,0 +1,80 @@
const path = require('path');
const webpack = require('webpack');
/*
* SplitChunksPlugin is enabled by default and replaced
* deprecated CommonsChunkPlugin. It automatically identifies modules which
* should be splitted of chunk by heuristics using module duplication count and
* module category (i. e. node_modules). And splits the chunks…
*
* It is safe to remove "splitChunks" from the generated configuration
* and was added as an educational example.
*
* https://webpack.js.org/plugins/split-chunks-plugin/
*
*/
/*
* We've enabled TerserPlugin for you! This minifies your app
* in order to load faster and run less javascript.
*
* https://github.com/webpack-contrib/terser-webpack-plugin
*
*/
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/main.ts',
plugins: [new webpack.ProgressPlugin()],
module: {
rules: [{
test: /\.(ts|tsx)$/,
loader: 'ts-loader',
include: [path.resolve(__dirname, 'src')],
exclude: [/node_modules/]
}, {
test: /.css$/,
use: [{
loader: "style-loader"
}, {
loader: "css-loader",
options: {
sourceMap: true
}
}]
}]
},
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
optimization: {
minimizer: [new TerserPlugin()],
splitChunks: {
cacheGroups: {
vendors: {
priority: -10,
test: /[\\/]node_modules[\\/]/
}
},
chunks: 'async',
minChunks: 1,
minSize: 30000,
name: false
}
}
}