Finished?!

This commit is contained in:
Daniel Ledda
2020-11-14 14:46:44 +01:00
parent 7b03628f57
commit 7b7b066d1d
14 changed files with 11697 additions and 105 deletions

2
climate-pinger.py Normal file → Executable file
View File

@@ -1,4 +1,4 @@
#!/usr/local/bin/python3 #!/usr/bin/python3
import adafruit_dht import adafruit_dht
import mh_z19 import mh_z19

BIN
climate-ranger Executable file

Binary file not shown.

Binary file not shown.

6
go.mod
View File

@@ -1,12 +1,8 @@
module djledda.de/ledda/climate-server module climate-ranger
go 1.15 go 1.15
require ( require (
github.com/Freeaqingme/golang-sql-driver-mysql v1.0.3 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/mux v1.8.0
github.com/gorilla/websocket v1.4.1
github.com/warthog618/gpio v1.0.0 // indirect
) )

50
main.go Normal file
View File

@@ -0,0 +1,50 @@
package main
import (
"fmt"
"log"
"os"
)
const DEBUG = true
var mainLogger *log.Logger
var pythonLogger *log.Logger
var logFile *os.File
func main() {
err := setup()
defer teardown()
if err != nil {
panic(err)
}
quitSnapshot := make(chan int, 1)
go saveSnapshotOnInterval(1, quitSnapshot)
startServer()
}
func setup() error {
err := InitDb()
if err != nil {
return err
}
logFile, err = os.OpenFile("./server-log", os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666)
if err != nil {
fmt.Println("Logger failed to initialise, this session will not be logged: ", err.Error())
mainLogger = log.New(os.Stdout, "Main: ", 0)
pythonLogger = log.New(os.Stdout, "Python Process: ", 0)
return nil
}
mainLogger = log.New(logFile, "Main: ", log.Ldate | log.Ltime | log.Lshortfile)
pythonLogger = log.New(logFile, "Python Process: ", log.Ldate | log.Ltime | log.Lshortfile)
mainLogger.Printf("Session started")
return nil
}
func teardown() {
CloseDb()
err := logFile.Close()
if err != nil {
panic(err)
}
}

11516
server-log Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,65 +4,42 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"html/template"
"log" "log"
"net/http" "net/http"
"os"
"os/exec" "os/exec"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
const DEBUG = true const ROOT_URL = ""
const ROOT_URL = "climate/"
func main() {
err := setup()
defer teardown()
if err == nil {
startServer()
} else {
fmt.Println(err)
}
}
func setup() error {
err := InitDb()
if err != nil {
return err
}
return nil
}
func teardown() {
CloseDb()
}
func startServer() { func startServer() {
port := "8001" port := "8001"
r := mux.NewRouter() router := mux.NewRouter()
r.HandleFunc("/", showCharts).Methods("GET") router.HandleFunc("/", showCharts).Methods("GET")
r.HandleFunc("/", showCharts).Methods("GET").Queries("show-minutes", "{[0-9]+}") router.HandleFunc("/", showCharts).Methods("GET").Queries("show-minutes", "{[0-9]+}")
r.HandleFunc("/", saveSnapshot).Methods("POST") router.HandleFunc("/", saveSnapshot).Methods("POST")
r.HandleFunc("/data", sendData).Methods("GET") router.HandleFunc("/data", sendData).Methods("GET")
r.HandleFunc("/data", sendData).Methods("GET").Queries("since", "") router.HandleFunc("/data", sendData).Methods("GET").Queries("since", "")
r.HandleFunc("/data/now", createAndSendSnapshot).Methods("GET") router.HandleFunc("/data/now", createAndSendSnapshot).Methods("GET")
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("webapp/dist/")))) router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("webapp/dist/"))))
fmt.Printf("Listening on port %s...\n", port) fmt.Printf("Listening on port %s...\n", port)
quitSnapshot := make(chan int, 1) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), router))
go saveSnapshotOnInterval(30, quitSnapshot)
log.Fatal(http.ListenAndServe(":"+port, r))
} }
func showCharts(w http.ResponseWriter, r *http.Request) { func showCharts(w http.ResponseWriter, r *http.Request) {
minutes := r.FormValue("show-minutes") minutes := r.FormValue("show-minutes")
if minutes != "" { if minutes != "" {
if _, err := strconv.ParseInt(minutes, 10, 0); err != nil { if _, err := strconv.ParseInt(minutes, 10, 0); err != nil {
http.Redirect(w, r, "/" + ROOT_URL + "/", 303) http.Redirect(w, r, ROOT_URL, 303)
} }
} }
fmt.Println("Hello, anyoneo tehre?") templater, err := template.ParseFiles("webapp/dist/charts.html")
http.ServeFile(w, r, "./webapp/dist/charts.html") if internalErrorOnErr(fmt.Errorf("couldn't parse the template: %w", err), w, r) { return }
err = templater.Execute(w, ROOT_URL)
internalErrorOnErr(err, w, r)
} }
func sendData(w http.ResponseWriter, r *http.Request) { func sendData(w http.ResponseWriter, r *http.Request) {
@@ -71,7 +48,7 @@ func sendData(w http.ResponseWriter, r *http.Request) {
if sinceQuery != "" { if sinceQuery != "" {
newDateSince, err := time.Parse(time.RFC3339Nano, 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)
} }
dateSince = newDateSince dateSince = newDateSince
} }
@@ -80,7 +57,8 @@ func sendData(w http.ResponseWriter, r *http.Request) {
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 }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, string(json)) _, err = fmt.Fprintf(w, string(json))
internalErrorOnErr(err, w, r)
} }
func createAndSendSnapshot(w http.ResponseWriter, r *http.Request) { func createAndSendSnapshot(w http.ResponseWriter, r *http.Request) {
@@ -92,7 +70,8 @@ func createAndSendSnapshot(w http.ResponseWriter, r *http.Request) {
json, err := createJsonFromSnapshotRecords([]*SnapshotRecord{dbSnapshotRecord}) 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)) _, err = fmt.Fprintf(w, string(json))
internalErrorOnErr(err, w, r)
} }
func saveSnapshot(w http.ResponseWriter, r *http.Request) { func saveSnapshot(w http.ResponseWriter, r *http.Request) {
@@ -100,22 +79,18 @@ func saveSnapshot(w http.ResponseWriter, r *http.Request) {
if internalErrorOnErr(fmt.Errorf("couldn't create snapshot from JSON: %w", err), w, r) { return } if internalErrorOnErr(fmt.Errorf("couldn't create snapshot from JSON: %w", err), w, r) { return }
newId, err := writeSnapshotToDb(snapshotSub) newId, err := writeSnapshotToDb(snapshotSub)
if internalErrorOnErr(fmt.Errorf("couldn't submit snapshot into the database: %w", err), w, r) { return } if internalErrorOnErr(fmt.Errorf("couldn't submit snapshot into the database: %w", err), w, r) { return }
fmt.Fprintf(w, "{id: %v}", newId) _, err = fmt.Fprintf(w, "{id: %v}", newId)
internalErrorOnErr(err, w, r)
} }
func saveSnapshotOnInterval(seconds int64, stop chan int) { func saveSnapshotOnInterval(seconds int64, stop chan int) {
var err error = nil
for { for {
if err != nil {
log.Println(err.Error())
}
snapshot, err := getSnapshotFromPythonScript() snapshot, err := getSnapshotFromPythonScript()
if err != nil { if err == nil {
continue
}
_, err = writeSnapshotToDb(snapshot) _, err = writeSnapshotToDb(snapshot)
}
if err != nil { if err != nil {
continue pythonLogger.Println(err.Error())
} }
select { select {
case <-stop: case <-stop:
@@ -127,16 +102,13 @@ func saveSnapshotOnInterval(seconds int64, stop chan int) {
} }
func getSnapshotFromPythonScript() (*SnapshotSubmission, error) { func getSnapshotFromPythonScript() (*SnapshotSubmission, error) {
process := exec.Command("climate-pinger.py") output, err := exec.Command("./climate-pinger.py").CombinedOutput()
process.Stdout = os.Stdout
process.Stderr = os.Stderr
err := process.Run()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf(
} "Error:\n\t%s%w",
output, err := process.Output() strings.Replace(string(output), "\n", "\n\t", -1),
if err != nil { err,
return nil, err )
} }
tokens := strings.Split(string(output), "\n") tokens := strings.Split(string(output), "\n")
if len(tokens) != 4 { if len(tokens) != 4 {
@@ -160,10 +132,14 @@ func internalErrorOnErr(err error, w http.ResponseWriter, r *http.Request) bool
r.URL, r.URL,
err.Error()) err.Error())
} }
fmt.Println(errorMessage) logError(errorMessage)
http.Error(w, errorMessage, 500) http.Error(w, errorMessage, 500)
return true return true
} }
return false return false
} }
func logError(errorMessage string) {
fmt.Println(errorMessage)
mainLogger.Println(errorMessage)
}

View File

@@ -3,11 +3,11 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Ledda's Room Climate</title> <title>Ledda's Room Climate</title>
<link href="/climate/static/styles.css" rel="stylesheet" /> <link href="{{.}}/static/styles.css" rel="stylesheet" />
<script src="/climate/static/main.js"></script> <script src="{{.}}/static/main.js"></script>
</head> </head>
<body> <body id="root">
<div class="chart-container" style="position: relative; height:40vh; width:80vw; margin: auto"> <div class="chart-container center">
<canvas id="myChart"></canvas> <canvas id="myChart"></canvas>
</div> </div>
</body> </body>

4
webapp/dist/main.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +1,36 @@
html, body {
margin: 0;
height: 100%;
}
.overlay { .overlay {
background-color: rgba(255, 255, 255, 0.5);
transition: opacity, 1s;
opacity: 100%;
}
.center {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: white; display: flex;
opacity: 50%; margin: auto;
display: table-cell; align-items: center;
text-align: center; justify-content: space-around;
}
.center > * {
align-content: center;
} }
.hidden { .hidden {
display: none; opacity: 0%;
}
.chart-container {
padding: 10vw;
width: calc(100% - 20vw);
height: calc(100% - 20vw);
} }

View File

@@ -18,6 +18,7 @@ class ClimateChart {
private chart: Chart | null; private chart: Chart | null;
private latestSnapshot: Snapshot | null; private latestSnapshot: Snapshot | null;
private onLoadedCallback: () => void = () => {}; private onLoadedCallback: () => void = () => {};
private onErrorCallback: (e: Error) => void = () => {};
constructor( constructor(
private readonly rootUrl: string, private readonly rootUrl: string,
private readonly canvasId: string, private readonly canvasId: string,
@@ -30,21 +31,30 @@ class ClimateChart {
} }
this.urlEndpoint = this.rootUrl + "data/"; this.urlEndpoint = this.rootUrl + "data/";
this.urlEndpoint += "since/" + this.minutesDisplayed; this.urlEndpoint += "since/" + this.minutesDisplayed;
this.initChart() this.initChart().catch((e) => {this.onErrorCallback(e);});
} }
onLoaded(callback: () => void) { onLoaded(callback: () => void) {
this.onLoadedCallback = callback; this.onLoadedCallback = callback;
} }
onErrored(callback: (e: Error) => void) {
this.onErrorCallback = callback;
}
private static isCanvas(el: HTMLElement): el is HTMLCanvasElement { private static isCanvas(el: HTMLElement): el is HTMLCanvasElement {
return el.tagName === "canvas"; return el.tagName === "CANVAS";
} }
private async getInitialDataBlob(): Promise<SnapshotRecords> { private async getInitialDataBlob(): Promise<SnapshotRecords> {
try {
const data = await fetch(this.urlEndpoint); const data = await fetch(this.urlEndpoint);
return await data.json(); return await data.json();
} }
catch (e) {
throw e;
}
}
private async initChart() { private async initChart() {
const canvasElement = document.getElementById(this.canvasId); const canvasElement = document.getElementById(this.canvasId);
@@ -54,12 +64,18 @@ class ClimateChart {
} else { } else {
throw new Error(`improper HTML element passed, needed type canvas, got ${canvasElement.tagName}`); throw new Error(`improper HTML element passed, needed type canvas, got ${canvasElement.tagName}`);
} }
this.chart = new Chart(ctx, generateClimateChartConfig({}));
try {
const payload = await this.getInitialDataBlob(); const payload = await this.getInitialDataBlob();
this.latestSnapshot = payload.snapshots[payload.snapshots.length - 1]; this.latestSnapshot = payload.snapshots[payload.snapshots.length - 1];
this.chart = new Chart(ctx, generateClimateChartConfig(this.jsonToChartPoints(payload))); this.insertSnapshots(...payload.snapshots);
setInterval(() => this.updateFromServer(), 30 * 1000); setInterval(() => this.updateFromServer(), 30 * 1000);
this.onLoadedCallback(); this.onLoadedCallback();
} }
catch (e) {
this.onErrorCallback(new Error(`Server error: ${e}`));
}
}
private jsonToChartPoints(json: SnapshotRecords): {humidity: ChartPoint[], temp: ChartPoint[], co2: ChartPoint[]} { private jsonToChartPoints(json: SnapshotRecords): {humidity: ChartPoint[], temp: ChartPoint[], co2: ChartPoint[]} {
const humidity = []; const humidity = [];
@@ -77,6 +93,7 @@ class ClimateChart {
private async updateFromServer() { private async updateFromServer() {
const currentTime = (new Date(this.latestSnapshot.time)).toISOString(); const currentTime = (new Date(this.latestSnapshot.time)).toISOString();
const url = "/" + this.rootUrl + "data?since=" + currentTime; const url = "/" + this.rootUrl + "data?since=" + currentTime;
try {
const payload: SnapshotRecords = await (await fetch(url)).json(); const payload: SnapshotRecords = await (await fetch(url)).json();
if (payload.snapshots.length > 0) { if (payload.snapshots.length > 0) {
this.latestSnapshot = payload.snapshots[payload.snapshots.length - 1]; this.latestSnapshot = payload.snapshots[payload.snapshots.length - 1];
@@ -85,6 +102,10 @@ class ClimateChart {
this.chart.update(); this.chart.update();
} }
} }
catch (e) {
this.onErrorCallback(new Error(`Server error: ${e}`));
}
}
private insertSnapshots(...snapshots: Snapshot[]) { private insertSnapshots(...snapshots: Snapshot[]) {
for (const snapshot of snapshots) { for (const snapshot of snapshots) {

View File

@@ -1,9 +1,9 @@
import {ChartConfiguration, ChartPoint, TimeUnit} from "chart.js"; import {ChartConfiguration, ChartPoint, TimeUnit} from "chart.js";
interface ClimateChartSettings { interface ClimateChartSettings {
humidity: ChartPoint[]; humidity?: ChartPoint[];
temp: ChartPoint[]; temp?: ChartPoint[];
co2: ChartPoint[]; co2?: ChartPoint[];
colors?: { colors?: {
humidity?: string; humidity?: string;
temp?: string; temp?: string;

View File

@@ -1,10 +1,13 @@
import ClimateChart from "./ClimateChart"; import ClimateChart from "./ClimateChart";
const ROOT_URL: string = "climate/";
const CHART_DOM_ID: string = "myChart"; const CHART_DOM_ID: string = "myChart";
let rootUrl: string = "";
function createClimateChart() { function createClimateChart() {
const pathname = window.location.pathname; const pathname = window.location.pathname;
if (pathname !== "/") {
rootUrl += pathname.match(/\/[^?\s]*/)[0];
}
let minutesDisplayed = 60; let minutesDisplayed = 60;
const argsStart = pathname.search(/\?minute-span=/); const argsStart = pathname.search(/\?minute-span=/);
if (argsStart !== -1) { if (argsStart !== -1) {
@@ -13,15 +16,24 @@ function createClimateChart() {
minutesDisplayed = parsedMins; minutesDisplayed = parsedMins;
} }
} }
return new ClimateChart(ROOT_URL, CHART_DOM_ID, minutesDisplayed); return new ClimateChart(rootUrl, CHART_DOM_ID, minutesDisplayed);
} }
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.innerText = 'Loading data...'; overlay.classList.add('overlay', 'center');
overlay.className = 'overlay'; const textContainer = document.createElement('span');
document.getRootNode().appendChild(overlay); textContainer.innerText = 'Loading data...';
overlay.appendChild(textContainer);
document.onreadystatechange = (e) => {
document.getElementById("root").appendChild(overlay);
const climateChart = createClimateChart(); const climateChart = createClimateChart();
climateChart.onLoaded(() => { climateChart.onLoaded(() => {
overlay.classList.add('hidden'); overlay.classList.add('hidden');
}) });
climateChart.onErrored((e) => {
overlay.classList.remove('hidden');
textContainer.innerText = `An error occurred: ${e}\nTry restarting the page.`;
});
document.onreadystatechange = () => {};
};