Moved the webapp to a webpack with some typescript, updated server wtih new endpoints
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
.idea
|
||||
node_modules
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"database/sql"
|
||||
_ "github.com/Freeaqingme/golang-sql-driver-mysql"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ClimateDb *sql.DB
|
||||
@@ -66,10 +67,10 @@ func getLastSnapshotRecordFromDb() (*SnapshotRecord, error) {
|
||||
return &snapshotRecord, nil
|
||||
}
|
||||
|
||||
func getSnapshotRecordsFromDb(minuteAgo int) ([]*SnapshotRecord, error) {
|
||||
func getSnapshotRecordsFromDb(dateSince time.Time) ([]*SnapshotRecord, error) {
|
||||
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;"
|
||||
rows, err := ClimateDb.Query(fmt.Sprintf(query, "id", "temp", "humidity", "co2", "time", "time", minuteAgo))
|
||||
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", dateSince.String()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't execute select query: %w", err)
|
||||
}
|
||||
|
||||
11
Snapshot.go
11
Snapshot.go
@@ -13,16 +13,23 @@ type SnapshotPayload struct {
|
||||
|
||||
type SnapshotRecord struct {
|
||||
Id int `json:"id"`
|
||||
SnapshotSubmission
|
||||
JsonSnapshotSubmission
|
||||
}
|
||||
|
||||
type SnapshotSubmission struct {
|
||||
type JsonSnapshotSubmission struct {
|
||||
Timestamp string `json:"time"`
|
||||
Temp float32 `json:"temp"`
|
||||
Humidity float32 `json:"humidity"`
|
||||
Co2 float32 `json:"co2"`
|
||||
}
|
||||
|
||||
type SnapshotSubmission struct {
|
||||
Timestamp interface{}
|
||||
Temp interface{}
|
||||
Humidity interface{}
|
||||
Co2 interface{}
|
||||
}
|
||||
|
||||
func createSnapshotSubFromJsonBodyStream(jsonBodyStream io.ReadCloser) (*SnapshotSubmission, error) {
|
||||
var snapshotSub SnapshotSubmission
|
||||
body, err := ioutil.ReadAll(jsonBodyStream)
|
||||
|
||||
161
charts.html
161
charts.html
@@ -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
28
climate-pinger.py
Normal 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
BIN
climate-server
Executable file
Binary file not shown.
@@ -6,7 +6,11 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DEBUG = true
|
||||
@@ -38,20 +42,23 @@ func startServer() {
|
||||
port := "8001"
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", showCharts).Methods("GET")
|
||||
r.HandleFunc("/since/{mins}", showCharts).Methods("GET")
|
||||
r.HandleFunc("/data/", sendData).Methods("GET")
|
||||
r.HandleFunc("/data/since/{mins}", sendData).Methods("GET")
|
||||
r.HandleFunc("/data/last/", sendLastSnapshot).Methods("GET")
|
||||
r.HandleFunc("/", showCharts).Methods("GET").Queries("show-minutes", "{[0-9]+}")
|
||||
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/"))))
|
||||
http.Handle("/", r)
|
||||
fmt.Printf("Listening on port %s...\n", port)
|
||||
quitSnapshot := make(chan int, 1)
|
||||
go saveSnapshotOnInterval(30, quitSnapshot)
|
||||
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||
}
|
||||
|
||||
func showCharts(w http.ResponseWriter, r *http.Request) {
|
||||
if countStr := mux.Vars(r)["mins"]; countStr != "" {
|
||||
if _, err := strconv.ParseInt(countStr, 10, 0); err != nil {
|
||||
minutes := r.FormValue("show-minutes")
|
||||
if minutes != "" {
|
||||
if _, err := strconv.ParseInt(minutes, 10, 0); err != nil {
|
||||
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) {
|
||||
var count int64 = 60
|
||||
if vars := mux.Vars(r); vars["mins"] != "" {
|
||||
newCount, err := strconv.ParseInt(vars["mins"], 10, 0)
|
||||
dateSince := time.Now()
|
||||
sinceQuery := r.FormValue("since")
|
||||
if sinceQuery != "" {
|
||||
newDateSince, err := time.Parse(time.RFC3339Nano, sinceQuery)
|
||||
if err != nil {
|
||||
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 }
|
||||
json, err := createJsonFromSnapshotRecords(records)
|
||||
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))
|
||||
}
|
||||
|
||||
func sendLastSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
record, err := getLastSnapshotRecordFromDb()
|
||||
if internalErrorOnErr(fmt.Errorf("couldn't get last snapshot from db: %w", err), w, r) { return }
|
||||
json, err := createJsonFromSnapshotRecords([]*SnapshotRecord{record})
|
||||
func createAndSendSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
snapshot, err := getSnapshotFromPythonScript()
|
||||
if internalErrorOnErr(fmt.Errorf("couldn't create a snapshot: %w", err), w, r) { return }
|
||||
_, 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 }
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, string(json))
|
||||
@@ -92,6 +103,54 @@ func saveSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
if errors.Unwrap(err) != nil {
|
||||
errorMessage := "Internal Server Error!"
|
||||
|
||||
12
go.mod
Normal file
12
go.mod
Normal 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
186
go.sum
Normal 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=
|
||||
20776
static/Chart.bundle.js
20776
static/Chart.bundle.js
File diff suppressed because it is too large
Load Diff
7
static/Chart.bundle.min.js
vendored
7
static/Chart.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
1937
webapp/dist/main.js
vendored
Normal file
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
8487
webapp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
webapp/package.json
Normal file
30
webapp/package.json
Normal 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
110
webapp/src/ClimateChart.ts
Normal 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;
|
||||
94
webapp/src/climateChartConfig.ts
Normal file
94
webapp/src/climateChartConfig.ts
Normal 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
27
webapp/src/main.ts
Normal 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
14
webapp/static/charts.html
Normal 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
15
webapp/static/styles.css
Normal 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
10
webapp/tsconfig.json
Normal 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
80
webapp/webpack.config.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user