Basic UI working state. Still needs clean and Assetfs to keep all dependencies in the application
This commit is contained in:
committed by
ncthompson
parent
f189ff0442
commit
8267e71f18
24
cmd/guimock/main.go
Normal file
24
cmd/guimock/main.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/hpdvanwyk/invertergui/webgui"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
addr := flag.String("addr", ":8080", "TCP address to listen on.")
|
||||||
|
mk2 := NewMk2Mock()
|
||||||
|
gui := webgui.NewWebGui(mk2)
|
||||||
|
|
||||||
|
http.Handle("/ws", http.HandlerFunc(gui.ServeHub))
|
||||||
|
http.Handle("/", gui)
|
||||||
|
http.Handle("/js/controller.js", http.HandlerFunc(gui.ServeJS))
|
||||||
|
http.Handle("/munin", http.HandlerFunc(gui.ServeMuninHTTP))
|
||||||
|
http.Handle("/muninconfig", http.HandlerFunc(gui.ServeMuninConfigHTTP))
|
||||||
|
http.Handle("/metrics", promhttp.Handler())
|
||||||
|
log.Fatal(http.ListenAndServe(*addr, nil))
|
||||||
|
}
|
||||||
90
cmd/guimock/mock.go
Normal file
90
cmd/guimock/mock.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hpdvanwyk/invertergui/mk2if"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mock struct {
|
||||||
|
c chan *mk2if.Mk2Info
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMk2Mock() mk2if.Mk2If {
|
||||||
|
tmp := &mock{
|
||||||
|
c: make(chan *mk2if.Mk2Info, 1),
|
||||||
|
}
|
||||||
|
go tmp.genMockValues()
|
||||||
|
return tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
func genBaseLeds(state mk2if.LEDstate) map[mk2if.Led]mk2if.LEDstate {
|
||||||
|
return map[mk2if.Led]mk2if.LEDstate{
|
||||||
|
mk2if.LedMain: state,
|
||||||
|
mk2if.LedAbsorption: state,
|
||||||
|
mk2if.LedBulk: state,
|
||||||
|
mk2if.LedFloat: state,
|
||||||
|
mk2if.LedInverter: state,
|
||||||
|
mk2if.LedOverload: state,
|
||||||
|
mk2if.LedLowBattery: state,
|
||||||
|
mk2if.LedTemperature: state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mock) GetMk2Info() *mk2if.Mk2Info {
|
||||||
|
return &mk2if.Mk2Info{
|
||||||
|
OutCurrent: 2.0,
|
||||||
|
InCurrent: 2.3,
|
||||||
|
OutVoltage: 230.0,
|
||||||
|
InVoltage: 230.1,
|
||||||
|
BatVoltage: 25,
|
||||||
|
BatCurrent: -10,
|
||||||
|
InFrequency: 50,
|
||||||
|
OutFrequency: 50,
|
||||||
|
ChargeState: 1,
|
||||||
|
Errors: nil,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
LEDs: genBaseLeds(mk2if.LedOff),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mock) C() chan *mk2if.Mk2Info {
|
||||||
|
return m.c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mock) Close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mock) genMockValues() {
|
||||||
|
mult := 1.0
|
||||||
|
ledState := mk2if.LedOff
|
||||||
|
for {
|
||||||
|
input := &mk2if.Mk2Info{
|
||||||
|
OutCurrent: 2.0 * mult,
|
||||||
|
InCurrent: 2.3 * mult,
|
||||||
|
OutVoltage: 230.0 * mult,
|
||||||
|
InVoltage: 230.1 * mult,
|
||||||
|
BatVoltage: 25 * mult,
|
||||||
|
BatCurrent: -10 * mult,
|
||||||
|
InFrequency: 50 * mult,
|
||||||
|
OutFrequency: 50 * mult,
|
||||||
|
ChargeState: 1 * mult,
|
||||||
|
Errors: nil,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Valid: true,
|
||||||
|
LEDs: genBaseLeds(ledState),
|
||||||
|
}
|
||||||
|
|
||||||
|
ledState = (ledState + 1) % 3
|
||||||
|
|
||||||
|
mult = mult - 0.1
|
||||||
|
if mult < 0 {
|
||||||
|
mult = 1.0
|
||||||
|
}
|
||||||
|
fmt.Printf("Sending\n")
|
||||||
|
m.c <- input
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,14 +32,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"github.com/hpdvanwyk/invertergui/mk2if"
|
|
||||||
"github.com/hpdvanwyk/invertergui/webgui"
|
|
||||||
"github.com/mikepb/go-serial"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/hpdvanwyk/invertergui/mk2if"
|
||||||
|
"github.com/hpdvanwyk/invertergui/webgui"
|
||||||
|
"github.com/mikepb/go-serial"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -81,6 +82,8 @@ func main() {
|
|||||||
|
|
||||||
gui := webgui.NewWebGui(mk2)
|
gui := webgui.NewWebGui(mk2)
|
||||||
http.Handle("/", gui)
|
http.Handle("/", gui)
|
||||||
|
http.Handle("/ws", http.HandlerFunc(gui.ServeHub))
|
||||||
|
http.Handle("/js/controller.js", http.HandlerFunc(gui.ServeJS))
|
||||||
http.Handle("/munin", http.HandlerFunc(gui.ServeMuninHTTP))
|
http.Handle("/munin", http.HandlerFunc(gui.ServeMuninHTTP))
|
||||||
http.Handle("/muninconfig", http.HandlerFunc(gui.ServeMuninConfigHTTP))
|
http.Handle("/muninconfig", http.HandlerFunc(gui.ServeMuninConfigHTTP))
|
||||||
http.Handle("/metrics", promhttp.Handler())
|
http.Handle("/metrics", promhttp.Handler())
|
||||||
|
|||||||
280
frontend/index.html
Normal file
280
frontend/index.html
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://bootswatch.com/4/darkly/bootstrap.min.css"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<title>Victron Multiplus Monitor</title>
|
||||||
|
<style>
|
||||||
|
.dot-off {
|
||||||
|
height: 25px;
|
||||||
|
width: 25px;
|
||||||
|
background-color: #bbb;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-green {
|
||||||
|
height: 25px;
|
||||||
|
width: 25px;
|
||||||
|
background-color: #2aed4e;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.dot-red {
|
||||||
|
height: 25px;
|
||||||
|
width: 25px;
|
||||||
|
background-color: #ed3d34;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.blink-red {
|
||||||
|
height: 25px;
|
||||||
|
width: 25px;
|
||||||
|
background-color: #ed3d34;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
animation: blinkingRedDot 1s infinite;
|
||||||
|
}
|
||||||
|
@keyframes blinkingRedDot {
|
||||||
|
0% {
|
||||||
|
background-color: #ed3d34;
|
||||||
|
}
|
||||||
|
49% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
99% {
|
||||||
|
background-color: #ed3d34;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: #ed3d34;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.blink-green {
|
||||||
|
height: 25px;
|
||||||
|
width: 25px;
|
||||||
|
background-color: #2aed4e;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
animation: blinkingGreenDot 1s infinite;
|
||||||
|
}
|
||||||
|
@keyframes blinkingGreenDot {
|
||||||
|
0% {
|
||||||
|
background-color: #2aed4e;
|
||||||
|
}
|
||||||
|
49% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
99% {
|
||||||
|
background-color: #2aed4e;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: #2aed4e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
|
||||||
|
<script src="js/controller.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.onload = function() {
|
||||||
|
loadContent();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="display-4 text-center">Inverter GUI</h1>
|
||||||
|
<div class="container" id="app">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm p-auto">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Output Current</h5>
|
||||||
|
<blockquote class="blockquote">{{ output_current }} A</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Output Voltage</h5>
|
||||||
|
<blockquote class="blockquote">{{ output_voltage }} V</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Output Frequency</h5>
|
||||||
|
<blockquote class="blockquote">
|
||||||
|
{{ output_frequency }} Hz
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Output Power</h5>
|
||||||
|
<blockquote class="blockquote">{{ output_power }} W</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm p-auto">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Input Current</h5>
|
||||||
|
<blockquote class="blockquote">{{ input_current }} A</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Input Voltage</h5>
|
||||||
|
<blockquote class="blockquote">{{ input_voltage }} V</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Input Frequency</h5>
|
||||||
|
<blockquote class="blockquote">
|
||||||
|
{{ input_frequency }} Hz
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Input Power</h5>
|
||||||
|
<blockquote class="blockquote">{{ input_power }} W</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm p-auto">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Battery Current</h5>
|
||||||
|
<blockquote class="blockquote">
|
||||||
|
{{ battery_current }} A
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Battery Voltage</h5>
|
||||||
|
<blockquote class="blockquote">
|
||||||
|
{{ battery_voltage }} V
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Battery Charge</h5>
|
||||||
|
<blockquote class="blockquote">{{ battery_charge }} %</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Battery Power</h5>
|
||||||
|
<blockquote class="blockquote">{{ battery_power }} W</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm p-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Mains</h5>
|
||||||
|
<span v-bind:class="[led_mains]"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm p-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Absorption</h5>
|
||||||
|
<span v-bind:class="[led_absorb]"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm p-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Bulk</h5>
|
||||||
|
<span v-bind:class="[led_bulk]"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm p-3">
|
||||||
|
<div class="card text-center ">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Float</h5>
|
||||||
|
<span v-bind:class="[led_float]"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm p-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Inverter</h5>
|
||||||
|
<span v-bind:class="[led_inverter]"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm p-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Overload</h5>
|
||||||
|
<span v-bind:class="[led_overload]"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm p-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Low Battery</h5>
|
||||||
|
<span v-bind:class="[led_bat_low]"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm p-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Temperature</h5>
|
||||||
|
<span v-bind:class="[led_over_temp]"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
69
frontend/js/controller.js
Normal file
69
frontend/js/controller.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
function loadContent() {
|
||||||
|
var conn;
|
||||||
|
|
||||||
|
var app = new Vue({
|
||||||
|
el: "#app",
|
||||||
|
data: {
|
||||||
|
output_current: null,
|
||||||
|
output_voltage: 0,
|
||||||
|
output_frequency: 0,
|
||||||
|
output_power: 0,
|
||||||
|
input_current: 0,
|
||||||
|
input_voltage: 0,
|
||||||
|
input_frequency: 0,
|
||||||
|
input_power: 0,
|
||||||
|
battery_current: 0,
|
||||||
|
battery_voltage: 0,
|
||||||
|
battery_charge: 0,
|
||||||
|
battery_power: 0,
|
||||||
|
led_mains: "dot-off",
|
||||||
|
led_absorb: "dot-off",
|
||||||
|
led_bulk: "dot-off",
|
||||||
|
led_float: "dot-off",
|
||||||
|
led_inverter: "dot-off",
|
||||||
|
led_overload: "dot-off",
|
||||||
|
led_bat_low: "dot-off",
|
||||||
|
led_over_temp: "dot-off"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window["WebSocket"]) {
|
||||||
|
conn = new WebSocket("ws://" + document.location.host + "/ws");
|
||||||
|
conn.onclose = function(evt) {
|
||||||
|
var item = document.createElement("a");
|
||||||
|
item.innerHTML = "<b>Connection closed.</b>";
|
||||||
|
};
|
||||||
|
|
||||||
|
conn.onmessage = function(evt) {
|
||||||
|
var update = JSON.parse(evt.data);
|
||||||
|
console.log(update);
|
||||||
|
app.output_current = update.output_current;
|
||||||
|
app.output_voltage = update.output_voltage;
|
||||||
|
app.output_frequency = update.output_frequency;
|
||||||
|
app.output_power = update.output_power;
|
||||||
|
|
||||||
|
app.input_current = update.input_current;
|
||||||
|
app.input_voltage = update.input_voltage;
|
||||||
|
app.input_frequency = update.input_frequency;
|
||||||
|
app.input_power = update.input_power;
|
||||||
|
|
||||||
|
app.battery_charge = update.battery_charge;
|
||||||
|
app.battery_voltage = update.battery_voltage;
|
||||||
|
app.battery_power = update.battery_power;
|
||||||
|
app.battery_current = update.battery_current;
|
||||||
|
|
||||||
|
leds = update.led_map;
|
||||||
|
app.led_mains = leds.led_mains;
|
||||||
|
app.led_absorb = leds.led_absorb;
|
||||||
|
app.led_bulk = leds.led_bulk;
|
||||||
|
app.led_float = leds.led_float;
|
||||||
|
app.led_inverter = leds.led_inverter;
|
||||||
|
app.led_overload = leds.led_overload;
|
||||||
|
app.led_bat_low = leds.led_bat_low;
|
||||||
|
app.led_over_temp = leds.led_over_temp;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
var item = document.createElement("a");
|
||||||
|
item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
|
||||||
|
}
|
||||||
|
}
|
||||||
1
go.mod
1
go.mod
@@ -1,6 +1,7 @@
|
|||||||
module github.com/hpdvanwyk/invertergui
|
module github.com/hpdvanwyk/invertergui
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/gorilla/websocket v1.4.0
|
||||||
github.com/mikepb/go-serial v0.0.0-20180731022703-d5134cecf05a
|
github.com/mikepb/go-serial v0.0.0-20180731022703-d5134cecf05a
|
||||||
github.com/prometheus/client_golang v0.9.2
|
github.com/prometheus/client_golang v0.9.2
|
||||||
)
|
)
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -2,6 +2,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLM
|
|||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/mikepb/go-serial v0.0.0-20180731022703-d5134cecf05a h1:dCv1LIqgkmFyW9NTRX16FANqPkGcyCZAPV54TMGUV+I=
|
github.com/mikepb/go-serial v0.0.0-20180731022703-d5134cecf05a h1:dCv1LIqgkmFyW9NTRX16FANqPkGcyCZAPV54TMGUV+I=
|
||||||
|
|||||||
@@ -30,16 +30,70 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||||||
|
|
||||||
package webgui
|
package webgui
|
||||||
|
|
||||||
var htmlTemplate string = `<html>
|
var htmlTemplate string = `<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<meta http-equiv="refresh" content="5">
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||||
|
|
||||||
<title>Victron Multiplus Monitor</title>
|
<title>Victron Multiplus Monitor</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
Quote
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<blockquote class="blockquote mb-0">
|
||||||
|
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.</p>
|
||||||
|
<footer class="blockquote-footer">Someone famous in <cite title="Source Title">Source Title</cite></footer>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
Quote
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<blockquote class="blockquote mb-0">
|
||||||
|
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.</p>
|
||||||
|
<footer class="blockquote-footer">Someone famous in <cite title="Source Title">Source Title</cite></footer>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
Quote
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<blockquote class="blockquote mb-0">
|
||||||
|
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.</p>
|
||||||
|
<footer class="blockquote-footer">Someone famous in <cite title="Source Title">Source Title</cite></footer>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
One of three columns
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
One of three columns
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{if .Error}} <p>Errors encountered: </p>
|
{{if .Error}} <p>Errors encountered: </p>
|
||||||
{{range .Error}}
|
{{range .Error}}
|
||||||
<dt> {{.}}</dt>
|
<dt> {{.}}</dt>
|
||||||
|
|||||||
135
webgui/webgui.go
135
webgui/webgui.go
@@ -32,37 +32,41 @@ package webgui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/hpdvanwyk/invertergui/mk2if"
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hpdvanwyk/invertergui/mk2if"
|
||||||
|
"github.com/hpdvanwyk/invertergui/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LedOff = "dot-off"
|
||||||
|
LedRed = "dot-red"
|
||||||
|
BlinkRed = "blink-red"
|
||||||
|
LedGreen = "dot-green"
|
||||||
|
BlinkGreen = "blink-green"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebGui struct {
|
type WebGui struct {
|
||||||
respChan chan *mk2if.Mk2Info
|
respChan chan *mk2if.Mk2Info
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
template *template.Template
|
|
||||||
|
|
||||||
muninRespChan chan muninData
|
muninRespChan chan muninData
|
||||||
poller mk2if.Mk2If
|
poller mk2if.Mk2If
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
hub *websocket.Hub
|
||||||
|
|
||||||
pu *prometheusUpdater
|
pu *prometheusUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebGui(source mk2if.Mk2If) *WebGui {
|
func NewWebGui(source mk2if.Mk2If) *WebGui {
|
||||||
w := new(WebGui)
|
w := new(WebGui)
|
||||||
w.respChan = make(chan *mk2if.Mk2Info)
|
|
||||||
w.muninRespChan = make(chan muninData)
|
w.muninRespChan = make(chan muninData)
|
||||||
w.stopChan = make(chan struct{})
|
w.stopChan = make(chan struct{})
|
||||||
var err error
|
|
||||||
w.template, err = template.New("thegui").Parse(htmlTemplate)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
w.poller = source
|
w.poller = source
|
||||||
w.pu = newPrometheusUpdater()
|
w.pu = newPrometheusUpdater()
|
||||||
|
w.hub = websocket.NewHub()
|
||||||
|
|
||||||
w.wg.Add(1)
|
w.wg.Add(1)
|
||||||
go w.dataPoll()
|
go w.dataPoll()
|
||||||
@@ -70,44 +74,55 @@ func NewWebGui(source mk2if.Mk2If) *WebGui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type templateInput struct {
|
type templateInput struct {
|
||||||
Error []error
|
Error []error `json:"errors"`
|
||||||
|
|
||||||
Date string
|
Date string `json:"date"`
|
||||||
|
|
||||||
OutCurrent string
|
OutCurrent string `json:"output_current"`
|
||||||
OutVoltage string
|
OutVoltage string `json:"output_voltage"`
|
||||||
OutPower string
|
OutPower string `json:"output_power"`
|
||||||
|
|
||||||
InCurrent string
|
InCurrent string `json:"input_current"`
|
||||||
InVoltage string
|
InVoltage string `json:"input_voltage"`
|
||||||
InPower string
|
InPower string `json:"input_power"`
|
||||||
|
|
||||||
InMinOut string
|
InMinOut string
|
||||||
|
|
||||||
BatVoltage string
|
BatVoltage string `json:"battery_voltage"`
|
||||||
BatCurrent string
|
BatCurrent string `json:"battery_current"`
|
||||||
BatPower string
|
BatPower string `json:"battery_power"`
|
||||||
BatCharge string
|
BatCharge string `json:"battery_charge"`
|
||||||
|
|
||||||
InFreq string
|
InFreq string `json:"input_frequency"`
|
||||||
OutFreq string
|
OutFreq string `json:"output_frequency"`
|
||||||
|
/*
|
||||||
|
MainsLed string `json:"main_led"`
|
||||||
|
AbsorbLed string `json:"absorb_led"`
|
||||||
|
BulkLed string `json:"bulk_led"`
|
||||||
|
FloatLed string `json:"float_led"`
|
||||||
|
InverterLed string `json:"inverter_led"`
|
||||||
|
|
||||||
Leds []string
|
OverloadLed string `json:"overload_led"`
|
||||||
|
LowBatLed string `json:"low_bat_led"`
|
||||||
|
OverTempLed string `json:"over_temp_led"`
|
||||||
|
*/
|
||||||
|
LedMap map[string]string `json:"led_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebGui) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
func (w *WebGui) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
statusErr := <-w.respChan
|
http.ServeFile(rw, r, "./frontend/index.html")
|
||||||
|
|
||||||
tmpInput := buildTemplateInput(statusErr)
|
|
||||||
|
|
||||||
err := w.template.Execute(rw, tmpInput)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ledName(nameInt int) string {
|
func (w *WebGui) ServeJS(rw http.ResponseWriter, r *http.Request) {
|
||||||
name, ok := mk2if.LedNames[nameInt]
|
http.ServeFile(rw, r, "./frontend/js/controller.js")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebGui) ServeHub(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
w.hub.ServeHTTP(rw, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledName(led mk2if.Led) string {
|
||||||
|
name, ok := mk2if.LedNames[led]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "Unknown led"
|
return "Unknown led"
|
||||||
}
|
}
|
||||||
@@ -121,24 +136,44 @@ func buildTemplateInput(status *mk2if.Mk2Info) *templateInput {
|
|||||||
tmpInput := &templateInput{
|
tmpInput := &templateInput{
|
||||||
Error: status.Errors,
|
Error: status.Errors,
|
||||||
Date: status.Timestamp.Format(time.RFC1123Z),
|
Date: status.Timestamp.Format(time.RFC1123Z),
|
||||||
OutCurrent: fmt.Sprintf("%.3f", status.OutCurrent),
|
OutCurrent: fmt.Sprintf("%.2f", status.OutCurrent),
|
||||||
OutVoltage: fmt.Sprintf("%.3f", status.OutVoltage),
|
OutVoltage: fmt.Sprintf("%.2f", status.OutVoltage),
|
||||||
OutPower: fmt.Sprintf("%.3f", outPower),
|
OutPower: fmt.Sprintf("%.2f", outPower),
|
||||||
InCurrent: fmt.Sprintf("%.3f", status.InCurrent),
|
InCurrent: fmt.Sprintf("%.2f", status.InCurrent),
|
||||||
InVoltage: fmt.Sprintf("%.3f", status.InVoltage),
|
InVoltage: fmt.Sprintf("%.2f", status.InVoltage),
|
||||||
InFreq: fmt.Sprintf("%.3f", status.InFrequency),
|
InFreq: fmt.Sprintf("%.2f", status.InFrequency),
|
||||||
OutFreq: fmt.Sprintf("%.3f", status.OutFrequency),
|
OutFreq: fmt.Sprintf("%.2f", status.OutFrequency),
|
||||||
InPower: fmt.Sprintf("%.3f", inPower),
|
InPower: fmt.Sprintf("%.2f", inPower),
|
||||||
|
|
||||||
InMinOut: fmt.Sprintf("%.3f", inPower-outPower),
|
InMinOut: fmt.Sprintf("%.2f", inPower-outPower),
|
||||||
|
|
||||||
BatCurrent: fmt.Sprintf("%.3f", status.BatCurrent),
|
BatCurrent: fmt.Sprintf("%.2f", status.BatCurrent),
|
||||||
BatVoltage: fmt.Sprintf("%.3f", status.BatVoltage),
|
BatVoltage: fmt.Sprintf("%.2f", status.BatVoltage),
|
||||||
BatPower: fmt.Sprintf("%.3f", status.BatVoltage*status.BatCurrent),
|
BatPower: fmt.Sprintf("%.2f", status.BatVoltage*status.BatCurrent),
|
||||||
BatCharge: fmt.Sprintf("%.3f", status.ChargeState*100),
|
BatCharge: fmt.Sprintf("%.2f", status.ChargeState*100),
|
||||||
|
|
||||||
|
LedMap: map[string]string{},
|
||||||
|
}
|
||||||
|
for k, v := range status.LEDs {
|
||||||
|
if k == mk2if.LedOverload || k == mk2if.LedTemperature || k == mk2if.LedLowBattery {
|
||||||
|
switch v {
|
||||||
|
case mk2if.LedOn:
|
||||||
|
tmpInput.LedMap[ledName(k)] = LedRed
|
||||||
|
case mk2if.LedBlink:
|
||||||
|
tmpInput.LedMap[ledName(k)] = BlinkRed
|
||||||
|
default:
|
||||||
|
tmpInput.LedMap[ledName(k)] = LedOff
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch v {
|
||||||
|
case mk2if.LedOn:
|
||||||
|
tmpInput.LedMap[ledName(k)] = LedGreen
|
||||||
|
case mk2if.LedBlink:
|
||||||
|
tmpInput.LedMap[ledName(k)] = BlinkGreen
|
||||||
|
default:
|
||||||
|
tmpInput.LedMap[ledName(k)] = LedOff
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for i := range status.LedListOn {
|
|
||||||
tmpInput.Leds = append(tmpInput.Leds, ledName(status.LedListOn[i]))
|
|
||||||
}
|
}
|
||||||
return tmpInput
|
return tmpInput
|
||||||
}
|
}
|
||||||
@@ -160,8 +195,8 @@ func (w *WebGui) dataPoll() {
|
|||||||
if s.Valid {
|
if s.Valid {
|
||||||
calcMuninValues(&muninValues, s)
|
calcMuninValues(&muninValues, s)
|
||||||
w.pu.updatePrometheus(s)
|
w.pu.updatePrometheus(s)
|
||||||
|
w.hub.Broadcast(buildTemplateInput(s))
|
||||||
}
|
}
|
||||||
case w.respChan <- s:
|
|
||||||
case w.muninRespChan <- muninValues:
|
case w.muninRespChan <- muninValues:
|
||||||
zeroMuninValues(&muninValues)
|
zeroMuninValues(&muninValues)
|
||||||
case <-w.stopChan:
|
case <-w.stopChan:
|
||||||
|
|||||||
93
websocket/client.go
Normal file
93
websocket/client.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package websocket
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Time allowed to write a message to the peer.
|
||||||
|
writeWait = 10 * time.Second
|
||||||
|
|
||||||
|
// Time allowed to read the next pong message from the peer.
|
||||||
|
pongWait = 60 * time.Second
|
||||||
|
|
||||||
|
// Send pings to peer with this period. Must be less than pongWait.
|
||||||
|
pingPeriod = (pongWait * 9) / 10
|
||||||
|
|
||||||
|
// Maximum message size allowed from peer.
|
||||||
|
maxMessageSize = 512
|
||||||
|
)
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is a middleman between the websocket connection and the hub.
|
||||||
|
type Client struct {
|
||||||
|
hub *Hub
|
||||||
|
|
||||||
|
// The websocket connection.
|
||||||
|
conn *websocket.Conn
|
||||||
|
|
||||||
|
// Buffered channel of outbound messages.
|
||||||
|
send chan []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// writePump pumps messages from the hub to the websocket connection.
|
||||||
|
//
|
||||||
|
// A goroutine running writePump is started for each connection. The
|
||||||
|
// application ensures that there is at most one writer to a connection by
|
||||||
|
// executing all writes from this goroutine.
|
||||||
|
func (c *Client) writePump() {
|
||||||
|
ticker := time.NewTicker(pingPeriod)
|
||||||
|
defer func() {
|
||||||
|
ticker.Stop()
|
||||||
|
c.conn.Close()
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message, ok := <-c.send:
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
|
if !ok {
|
||||||
|
// The hub closed the channel.
|
||||||
|
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := c.conn.NextWriter(websocket.TextMessage)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(message)
|
||||||
|
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-ticker.C:
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
|
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeWs handles websocket requests from the peer.
|
||||||
|
func (h *Hub) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := &Client{hub: h, conn: conn, send: make(chan []byte, 256)}
|
||||||
|
client.hub.register <- client
|
||||||
|
|
||||||
|
// Allow collection of memory referenced by the caller by doing all work in
|
||||||
|
// new goroutines.
|
||||||
|
go client.writePump()
|
||||||
|
}
|
||||||
64
websocket/hub.go
Normal file
64
websocket/hub.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package websocket
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hub maintains the set of active clients and broadcasts messages to the
|
||||||
|
// clients.
|
||||||
|
type Hub struct {
|
||||||
|
// Registered clients.
|
||||||
|
clients map[*Client]bool
|
||||||
|
|
||||||
|
// Inbound messages from the clients.
|
||||||
|
broadcast chan []byte
|
||||||
|
|
||||||
|
// Register requests from the clients.
|
||||||
|
register chan *Client
|
||||||
|
|
||||||
|
// Unregister requests from clients.
|
||||||
|
unregister chan *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHub() *Hub {
|
||||||
|
tmp := &Hub{
|
||||||
|
broadcast: make(chan []byte),
|
||||||
|
register: make(chan *Client),
|
||||||
|
unregister: make(chan *Client),
|
||||||
|
clients: make(map[*Client]bool),
|
||||||
|
}
|
||||||
|
go tmp.run()
|
||||||
|
return tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) Broadcast(message interface{}) error {
|
||||||
|
payload, err := json.Marshal(message)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.broadcast <- []byte(payload)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) run() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case client := <-h.register:
|
||||||
|
h.clients[client] = true
|
||||||
|
case client := <-h.unregister:
|
||||||
|
if _, ok := h.clients[client]; ok {
|
||||||
|
delete(h.clients, client)
|
||||||
|
close(client.send)
|
||||||
|
}
|
||||||
|
case message := <-h.broadcast:
|
||||||
|
for client := range h.clients {
|
||||||
|
select {
|
||||||
|
case client.send <- message:
|
||||||
|
default:
|
||||||
|
close(client.send)
|
||||||
|
delete(h.clients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user