Use mk2if instead of datasource as data source.

This removes support for the python based mk2daemon.
This commit is contained in:
Hendrik van Wyk
2017-09-16 15:37:58 +02:00
parent dd8ee443e2
commit a52af88bff
10 changed files with 144 additions and 492 deletions

View File

@@ -32,23 +32,54 @@ package main
import ( import (
"flag" "flag"
"github.com/hpdvanwyk/invertergui/datasource" "github.com/hpdvanwyk/invertergui/mk2if"
"github.com/hpdvanwyk/invertergui/webgui" "github.com/hpdvanwyk/invertergui/webgui"
"github.com/mikepb/go-serial"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"io"
"log" "log"
"net"
"net/http" "net/http"
"time"
) )
func main() { func main() {
url := flag.String("url", "http://localhost:9005", "The url of the multiplus JSON interface.")
capacity := flag.Float64("capacity", 100, "The capacity of the batteries in the system.")
addr := flag.String("addr", ":8080", "TCP address to listen on.") addr := flag.String("addr", ":8080", "TCP address to listen on.")
tcp := flag.Bool("tcp", false, "Use TCP instead of TTY")
ip := flag.String("ip", "localhost:8139", "IP to connect when using tcp connection.")
dev := flag.String("dev", "/dev/ttyUSB0", "TTY device to use.")
flag.Parse() flag.Parse()
source := datasource.NewJSONSource(*url) var p io.ReadWriteCloser
poller := datasource.NewDataPoller(source, 10*time.Second) var err error
gui := webgui.NewWebGui(poller, *capacity) var tcpAddr *net.TCPAddr
if *tcp {
tcpAddr, err = net.ResolveTCPAddr("tcp", *ip)
if err != nil {
panic(err)
}
p, err = net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
panic(err)
}
} else {
options := serial.RawOptions
options.BitRate = 2400
options.Mode = serial.MODE_READ_WRITE
p, err = options.Open(*dev)
if err != nil {
panic(err)
}
}
defer p.Close()
mk2, err := mk2if.NewMk2Connection(p)
defer mk2.Close()
if err != nil {
panic(err)
}
gui := webgui.NewWebGui(mk2)
http.Handle("/", gui) http.Handle("/", gui)
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))

View File

@@ -1,99 +0,0 @@
/*
Copyright (c) 2015, Hendrik van Wyk
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of invertergui nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package datasource
import (
"sync"
"time"
)
type DataPoller interface {
C() chan *Status
Stop()
}
type Status struct {
MpStatus MultiplusStatus
Time time.Time
Err error
}
type poller struct {
source DataSource
rate time.Duration
statusChan chan *Status
stop chan struct{}
wg sync.WaitGroup
}
func NewDataPoller(source DataSource, pollRate time.Duration) DataPoller {
this := &poller{
source: source,
rate: pollRate,
statusChan: make(chan *Status),
stop: make(chan struct{}),
}
this.wg.Add(1)
go this.poll()
return this
}
func (this *poller) C() chan *Status {
return this.statusChan
}
func (this *poller) Stop() {
close(this.stop)
this.wg.Wait()
}
func (this *poller) poll() {
ticker := time.NewTicker(this.rate)
this.doPoll()
for {
select {
case <-ticker.C:
this.doPoll()
case <-this.stop:
ticker.Stop()
close(this.statusChan)
this.wg.Done()
return
}
}
}
func (this *poller) doPoll() {
tmp := new(Status)
tmp.Err = this.source.GetData(&tmp.MpStatus)
tmp.Time = time.Now()
this.statusChan <- tmp
}

View File

@@ -1,69 +0,0 @@
/*
Copyright (c) 2015, Hendrik van Wyk
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of invertergui nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package datasource
import (
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
var sampleJSON string = `{"outCurrent": 1.19,
"leds": [0, 0, 0, 0, 1, 0, 0, 1],
"batVoltage": 26.63,
"inCurrent": 1.39,
"outVoltage": 235.3,
"inVoltage": 235.3,
"inFreq": 51.3,
"batCurrent": 0.0,
"outFreq": 735.3}`
func returnJson(resp http.ResponseWriter, req *http.Request) {
resp.Write([]byte(sampleJSON))
}
func TestFetchStatus(t *testing.T) {
//setup test server
testServer := httptest.NewServer(http.HandlerFunc(returnJson))
var status MultiplusStatus
source := NewJSONSource(testServer.URL)
err := source.GetData(&status)
if err != nil {
t.Errorf("Unmarshal gave: %v", err)
}
expected := MultiplusStatus{1.19, 1.39, 235.3, 235.3, 26.63, 0, 51.3, 735.3, []int{0, 0, 0, 0, 1, 0, 0, 1}}
if !reflect.DeepEqual(status, expected) {
t.Errorf("JSON string did not decode as expected.")
}
testServer.Close()
}

View File

@@ -1,77 +0,0 @@
/*
Copyright (c) 2015, Hendrik van Wyk
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of invertergui nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package datasource
import (
"encoding/json"
"net/http"
)
type DataSource interface {
GetData(*MultiplusStatus) error
}
type MultiplusStatus struct {
OutCurrent float64 `json:"outCurrent"`
InCurrent float64 `json:"inCurrent"`
OutVoltage float64 `json:"outVoltage"`
InVoltage float64 `json:"inVoltage"`
BatVoltage float64 `json:"batVoltage"`
BatCurrent float64 `json:"batCurrent"`
InFreq float64 `json:"inFreq"`
OutFreq float64 `json:"outFreq"`
Leds []int `json:"leds"`
}
type source struct {
url string
}
func NewJSONSource(url string) DataSource {
return &source{url: url}
}
func (s *source) GetData(status *MultiplusStatus) error {
resp, err := http.Get(s.url)
if err != nil {
return err
}
dec := json.NewDecoder(resp.Body)
err = dec.Decode(status)
if err != nil {
return err
}
err = resp.Body.Close()
if err != nil {
return err
}
return nil
}

View File

@@ -1,91 +0,0 @@
/*
Copyright (c) 2015, Hendrik van Wyk
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of invertergui nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package datasource
import (
"errors"
"testing"
"time"
)
type mockDataSource struct {
currentMock int
shouldBreak bool
}
func (this *mockDataSource) GetData(data *MultiplusStatus) error {
if this.shouldBreak {
return errors.New("Do not be alarmed. This is only a test.")
}
data.BatCurrent = float64(this.currentMock)
this.currentMock++
return nil
}
func TestOnePoll(t *testing.T) {
poller := NewDataPoller(&mockDataSource{currentMock: 100}, 1*time.Millisecond)
statChan := poller.C()
status := <-statChan
if status.MpStatus.BatCurrent != 100 {
t.Errorf("Incorrect data passed from data source.")
}
if status.Time.IsZero() {
t.Errorf("Time not set.")
}
poller.Stop()
}
func TestMultiplePolls(t *testing.T) {
poller := NewDataPoller(&mockDataSource{currentMock: 100}, 1*time.Millisecond)
statChan := poller.C()
expect := 100
for i := 0; i < 100; i++ {
status := <-statChan
if status.MpStatus.BatCurrent != float64(expect) {
t.Errorf("Incorrect data passed from data source.")
}
expect++
if status.Time.IsZero() {
t.Errorf("Time not set.")
}
}
poller.Stop()
}
func TestError(t *testing.T) {
poller := NewDataPoller(&mockDataSource{shouldBreak: true}, 1*time.Millisecond)
statChan := poller.C()
status := <-statChan
if status.Err == nil {
t.Errorf("Error not correctly propagated.")
}
poller.Stop()
}

View File

@@ -40,7 +40,11 @@ var htmlTemplate string = `<html>
</head> </head>
<body> <body>
{{if .Error}} <p>Error encountered: {{.Error}} </p> {{end}} {{if .Error}} <p>Errors encountered: </p>
{{range .Error}}
<dt> {{.}}</dt>
{{end}}
{{end}}
<dl> <dl>
<dt> Date: {{.Date}}</dt> <dt> Date: {{.Date}}</dt>
</dl> </dl>
@@ -55,6 +59,7 @@ var htmlTemplate string = `<html>
<dl> <dl>
<dt> Output Current: {{.OutCurrent}} A</dt> <dt> Output Current: {{.OutCurrent}} A</dt>
<dt> Output Voltage: {{.OutVoltage}} V</dt> <dt> Output Voltage: {{.OutVoltage}} V</dt>
<dt> Output Frequency: {{.OutFreq}} Hz</dt>
<dt> Output Power: {{.OutPower}} VA</dt> <dt> Output Power: {{.OutPower}} VA</dt>
</dl> </dl>
<dl> <dl>
@@ -68,7 +73,7 @@ var htmlTemplate string = `<html>
<dt> Battery Current: {{.BatCurrent}} A</dt> <dt> Battery Current: {{.BatCurrent}} A</dt>
<dt> Battery Voltage: {{.BatVoltage}} V</dt> <dt> Battery Voltage: {{.BatVoltage}} V</dt>
<dt> Battery Power: {{.BatPower}} W</dt> <dt> Battery Power: {{.BatPower}} W</dt>
<dt> Battery Charge: {{.BatCharge}} A h</dt> <dt> Battery Charge: {{.BatCharge}} %</dt>
</dl> </dl>
</body> </body>
</html>` </html>`

View File

@@ -33,12 +33,12 @@ package webgui
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/hpdvanwyk/invertergui/mk2if"
"net/http" "net/http"
"time"
) )
type muninData struct { type muninData struct {
statusP statusProcessed status mk2if.Mk2Info
timesUpdated int timesUpdated int
} }
@@ -51,8 +51,8 @@ func (w *WebGui) ServeMuninHTTP(rw http.ResponseWriter, r *http.Request) {
} }
calcMuninAverages(&muninDat) calcMuninAverages(&muninDat)
statusP := &muninDat.statusP status := muninDat.status
tmpInput := buildTemplateInput(statusP, time.Now()) tmpInput := buildTemplateInput(&status)
outputBuf := &bytes.Buffer{} outputBuf := &bytes.Buffer{}
fmt.Fprintf(outputBuf, "multigraph in_batvolt\n") fmt.Fprintf(outputBuf, "multigraph in_batvolt\n")
fmt.Fprintf(outputBuf, "volt.value %s\n", tmpInput.BatVoltage) fmt.Fprintf(outputBuf, "volt.value %s\n", tmpInput.BatVoltage)
@@ -72,7 +72,8 @@ func (w *WebGui) ServeMuninHTTP(rw http.ResponseWriter, r *http.Request) {
fmt.Fprintf(outputBuf, "powerin.value %s\n", tmpInput.InPower) fmt.Fprintf(outputBuf, "powerin.value %s\n", tmpInput.InPower)
fmt.Fprintf(outputBuf, "powerout.value %s\n", tmpInput.OutPower) fmt.Fprintf(outputBuf, "powerout.value %s\n", tmpInput.OutPower)
fmt.Fprintf(outputBuf, "multigraph in_mainsfreq\n") fmt.Fprintf(outputBuf, "multigraph in_mainsfreq\n")
fmt.Fprintf(outputBuf, "freq.value %s\n", tmpInput.InFreq) fmt.Fprintf(outputBuf, "freqin.value %s\n", tmpInput.InFreq)
fmt.Fprintf(outputBuf, "freqout.value %s\n", tmpInput.OutFreq)
_, err := rw.Write([]byte(outputBuf.String())) _, err := rw.Write([]byte(outputBuf.String()))
if err != nil { if err != nil {
@@ -92,12 +93,12 @@ volt.label Voltage of battery (V)
multigraph in_batcharge multigraph in_batcharge
graph_title Battery Charge graph_title Battery Charge
graph_vlabel Charge (A h) graph_vlabel Charge (%)
graph_category inverter graph_category inverter
graph_info Battery charge graph_info Battery charge
charge.info Estimated charge of battery charge.info Estimated charge of battery
charge.label Battery charge (A h) charge.label Battery charge (%)
multigraph in_batcurrent multigraph in_batcurrent
graph_title Battery Current graph_title Battery Current
@@ -156,8 +157,10 @@ graph_vlabel Frequency (Hz)
graph_category inverter graph_category inverter
graph_info Mains frequency graph_info Mains frequency
freq.info Input frequency freqin.info In frequency
freq.label Input frequency (Hz) freqin.label In frequency (Hz)
freqout.info Out frequency
freqout.label Out frequency (Hz)
` `
_, err := rw.Write([]byte(output)) _, err := rw.Write([]byte(output))
@@ -167,46 +170,47 @@ freq.label Input frequency (Hz)
} }
//Munin only samples once every 5 minutes so averages have to be calculated for some values. //Munin only samples once every 5 minutes so averages have to be calculated for some values.
func calcMuninValues(muninDat *muninData, newStatus *statusProcessed) { func calcMuninValues(muninDat *muninData, newStatus *mk2if.Mk2Info) {
muninDat.timesUpdated += 1 muninDat.timesUpdated += 1
muninVal := &muninDat.statusP muninVal := &muninDat.status
muninVal.status.OutCurrent += newStatus.status.OutCurrent muninVal.OutCurrent += newStatus.OutCurrent
muninVal.status.InCurrent += newStatus.status.InCurrent muninVal.InCurrent += newStatus.InCurrent
muninVal.status.BatCurrent += newStatus.status.BatCurrent muninVal.BatCurrent += newStatus.BatCurrent
muninVal.status.OutVoltage += newStatus.status.OutVoltage muninVal.OutVoltage += newStatus.OutVoltage
muninVal.status.InVoltage += newStatus.status.InVoltage muninVal.InVoltage += newStatus.InVoltage
muninVal.status.BatVoltage += newStatus.status.BatVoltage muninVal.BatVoltage += newStatus.BatVoltage
muninVal.status.InFreq = newStatus.status.InFreq muninVal.InFrequency = newStatus.InFrequency
muninVal.OutFrequency = newStatus.OutFrequency
muninVal.chargeLevel = newStatus.chargeLevel muninVal.ChargeState = newStatus.ChargeState
muninVal.status.Leds = newStatus.status.Leds
} }
func calcMuninAverages(muninDat *muninData) { func calcMuninAverages(muninDat *muninData) {
muninVal := &muninDat.statusP muninVal := &muninDat.status
muninVal.status.OutCurrent /= float64(muninDat.timesUpdated) muninVal.OutCurrent /= float64(muninDat.timesUpdated)
muninVal.status.InCurrent /= float64(muninDat.timesUpdated) muninVal.InCurrent /= float64(muninDat.timesUpdated)
muninVal.status.BatCurrent /= float64(muninDat.timesUpdated) muninVal.BatCurrent /= float64(muninDat.timesUpdated)
muninVal.status.OutVoltage /= float64(muninDat.timesUpdated) muninVal.OutVoltage /= float64(muninDat.timesUpdated)
muninVal.status.InVoltage /= float64(muninDat.timesUpdated) muninVal.InVoltage /= float64(muninDat.timesUpdated)
muninVal.status.BatVoltage /= float64(muninDat.timesUpdated) muninVal.BatVoltage /= float64(muninDat.timesUpdated)
} }
func zeroMuninValues(muninDat *muninData) { func zeroMuninValues(muninDat *muninData) {
muninDat.timesUpdated = 0 muninDat.timesUpdated = 0
muninVal := &muninDat.statusP muninVal := &muninDat.status
muninVal.status.OutCurrent = 0 muninVal.OutCurrent = 0
muninVal.status.InCurrent = 0 muninVal.InCurrent = 0
muninVal.status.BatCurrent = 0 muninVal.BatCurrent = 0
muninVal.status.OutVoltage = 0 muninVal.OutVoltage = 0
muninVal.status.InVoltage = 0 muninVal.InVoltage = 0
muninVal.status.BatVoltage = 0 muninVal.BatVoltage = 0
muninVal.status.InFreq = 0 muninVal.InFrequency = 0
muninVal.OutFrequency = 0
muninVal.chargeLevel = 0 muninVal.ChargeState = 0
} }

View File

@@ -31,6 +31,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package webgui package webgui
import ( import (
"github.com/hpdvanwyk/invertergui/mk2if"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
@@ -116,10 +117,10 @@ func newPrometheusUpdater() *prometheusUpdater {
return tmp return tmp
} }
func (pu *prometheusUpdater) updatePrometheus(newStatus *statusProcessed) { func (pu *prometheusUpdater) updatePrometheus(newStatus *mk2if.Mk2Info) {
s := newStatus.status s := newStatus
pu.batteryVoltage.Set(s.BatVoltage) pu.batteryVoltage.Set(s.BatVoltage)
pu.batteryCharge.Set(newStatus.chargeLevel) pu.batteryCharge.Set(newStatus.ChargeState * 100)
pu.batteryCurrent.Set(s.BatCurrent) pu.batteryCurrent.Set(s.BatCurrent)
pu.batteryCurrent.Set(s.BatVoltage * s.BatCurrent) pu.batteryCurrent.Set(s.BatVoltage * s.BatCurrent)
pu.mainsCurrentIn.Set(s.InCurrent) pu.mainsCurrentIn.Set(s.InCurrent)
@@ -128,7 +129,6 @@ func (pu *prometheusUpdater) updatePrometheus(newStatus *statusProcessed) {
pu.mainsVoltageOut.Set(s.OutVoltage) pu.mainsVoltageOut.Set(s.OutVoltage)
pu.mainsPowerIn.Set(s.InVoltage * s.InCurrent) pu.mainsPowerIn.Set(s.InVoltage * s.InCurrent)
pu.mainsPowerOut.Set(s.OutVoltage * s.OutCurrent) pu.mainsPowerOut.Set(s.OutVoltage * s.OutCurrent)
pu.mainsFreqIn.Set(s.InFreq) pu.mainsFreqIn.Set(s.InFrequency)
pu.mainsFreqIn.Set(s.OutFreq) pu.mainsFreqOut.Set(s.OutFrequency)
} }

View File

@@ -32,50 +32,28 @@ package webgui
import ( import (
"fmt" "fmt"
"github.com/hpdvanwyk/invertergui/datasource" "github.com/hpdvanwyk/invertergui/mk2if"
"html/template" "html/template"
"net/http" "net/http"
"sync" "sync"
"time" "time"
) )
const (
Temperature = iota
Low_battery
Overload
Inverter
Float
Bulk
Absorption
Mains
)
var leds = map[int]string{
0: "Temperature",
1: "Low battery",
2: "Overload",
3: "Inverter",
4: "Float",
5: "Bulk",
6: "Absorption",
7: "Mains",
}
type WebGui struct { type WebGui struct {
respChan chan statusProcessed respChan chan *mk2if.Mk2Info
stopChan chan struct{} stopChan chan struct{}
template *template.Template template *template.Template
muninRespChan chan muninData muninRespChan chan muninData
poller datasource.DataPoller poller mk2if.Mk2If
wg sync.WaitGroup wg sync.WaitGroup
pu *prometheusUpdater pu *prometheusUpdater
} }
func NewWebGui(source datasource.DataPoller, batteryCapacity float64) *WebGui { func NewWebGui(source mk2if.Mk2If) *WebGui {
w := new(WebGui) w := new(WebGui)
w.respChan = make(chan statusProcessed) 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 var err error
@@ -87,12 +65,12 @@ func NewWebGui(source datasource.DataPoller, batteryCapacity float64) *WebGui {
w.pu = newPrometheusUpdater() w.pu = newPrometheusUpdater()
w.wg.Add(1) w.wg.Add(1)
go w.dataPoll(batteryCapacity) go w.dataPoll()
return w return w
} }
type templateInput struct { type templateInput struct {
Error error Error []error
Date string Date string
@@ -111,7 +89,8 @@ type templateInput struct {
BatPower string BatPower string
BatCharge string BatCharge string
InFreq string InFreq string
OutFreq string
Leds []string Leds []string
} }
@@ -119,7 +98,7 @@ type templateInput struct {
func (w *WebGui) ServeHTTP(rw http.ResponseWriter, r *http.Request) { func (w *WebGui) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
statusErr := <-w.respChan statusErr := <-w.respChan
tmpInput := buildTemplateInput(&statusErr, statusErr.timestamp) tmpInput := buildTemplateInput(statusErr)
err := w.template.Execute(rw, tmpInput) err := w.template.Execute(rw, tmpInput)
if err != nil { if err != nil {
@@ -127,20 +106,28 @@ func (w *WebGui) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
} }
} }
func buildTemplateInput(statusErr *statusProcessed, now time.Time) *templateInput { func ledName(nameInt int) string {
status := statusErr.status name, ok := mk2if.LedNames[nameInt]
if !ok {
return "Unknown led"
}
return name
}
func buildTemplateInput(status *mk2if.Mk2Info) *templateInput {
outPower := status.OutVoltage * status.OutCurrent outPower := status.OutVoltage * status.OutCurrent
inPower := status.InCurrent * status.InVoltage inPower := status.InCurrent * status.InVoltage
tmpInput := &templateInput{ tmpInput := &templateInput{
Error: statusErr.err, Error: status.Errors,
Date: now.Format(time.RFC1123Z), Date: status.Timestamp.Format(time.RFC1123Z),
OutCurrent: fmt.Sprintf("%.3f", status.OutCurrent), OutCurrent: fmt.Sprintf("%.3f", status.OutCurrent),
OutVoltage: fmt.Sprintf("%.3f", status.OutVoltage), OutVoltage: fmt.Sprintf("%.3f", status.OutVoltage),
OutPower: fmt.Sprintf("%.3f", outPower), OutPower: fmt.Sprintf("%.3f", outPower),
InCurrent: fmt.Sprintf("%.3f", status.InCurrent), InCurrent: fmt.Sprintf("%.3f", status.InCurrent),
InVoltage: fmt.Sprintf("%.3f", status.InVoltage), InVoltage: fmt.Sprintf("%.3f", status.InVoltage),
InFreq: fmt.Sprintf("%.3f", status.InFreq), InFreq: fmt.Sprintf("%.3f", status.InFrequency),
OutFreq: fmt.Sprintf("%.3f", status.OutFrequency),
InPower: fmt.Sprintf("%.3f", inPower), InPower: fmt.Sprintf("%.3f", inPower),
InMinOut: fmt.Sprintf("%.3f", inPower-outPower), InMinOut: fmt.Sprintf("%.3f", inPower-outPower),
@@ -148,56 +135,33 @@ func buildTemplateInput(statusErr *statusProcessed, now time.Time) *templateInpu
BatCurrent: fmt.Sprintf("%.3f", status.BatCurrent), BatCurrent: fmt.Sprintf("%.3f", status.BatCurrent),
BatVoltage: fmt.Sprintf("%.3f", status.BatVoltage), BatVoltage: fmt.Sprintf("%.3f", status.BatVoltage),
BatPower: fmt.Sprintf("%.3f", status.BatVoltage*status.BatCurrent), BatPower: fmt.Sprintf("%.3f", status.BatVoltage*status.BatCurrent),
BatCharge: fmt.Sprintf("%.3f", statusErr.chargeLevel), BatCharge: fmt.Sprintf("%.3f", status.ChargeState*100),
} }
if len(status.Leds) == 8 { for i := range status.LedListOn {
for i := 7; i >= 0; i-- { tmpInput.Leds = append(tmpInput.Leds, ledName(status.LedListOn[i]))
if status.Leds[i] == 1 {
tmpInput.Leds = append(tmpInput.Leds, leds[i])
}
}
} }
return tmpInput return tmpInput
} }
func (w *WebGui) Stop() { func (w *WebGui) Stop() {
w.poller.Stop()
close(w.stopChan) close(w.stopChan)
w.wg.Wait() w.wg.Wait()
} }
type statusProcessed struct {
status datasource.MultiplusStatus
chargeLevel float64
err error
timestamp time.Time
}
// dataPoll waits for data from the w.poller channel. It will send its currently stored status // dataPoll waits for data from the w.poller channel. It will send its currently stored status
// to respChan if anything reads from it. // to respChan if anything reads from it.
func (w *WebGui) dataPoll(batteryCapacity float64) { func (w *WebGui) dataPoll() {
tracker := NewChargeTracker(batteryCapacity)
pollChan := w.poller.C() pollChan := w.poller.C()
var statusP statusProcessed
var muninValues muninData var muninValues muninData
s := &mk2if.Mk2Info{}
for { for {
select { select {
case s := <-pollChan: case s = <-pollChan:
if s.Err != nil { if s.Valid {
statusP.err = s.Err calcMuninValues(&muninValues, s)
} else { w.pu.updatePrometheus(s)
statusP.status = s.MpStatus
statusP.err = nil
statusP.timestamp = s.Time
tracker.Update(s.MpStatus.BatCurrent, s.Time)
if s.MpStatus.Leds[Float] == 1 {
tracker.Reset()
}
statusP.chargeLevel = tracker.CurrentLevel()
calcMuninValues(&muninValues, &statusP)
w.pu.updatePrometheus(&statusP)
} }
case w.respChan <- statusP: case w.respChan <- s:
case w.muninRespChan <- muninValues: case w.muninRespChan <- muninValues:
zeroMuninValues(&muninValues) zeroMuninValues(&muninValues)
case <-w.stopChan: case <-w.stopChan:

View File

@@ -31,57 +31,39 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package webgui package webgui
import ( import (
"github.com/hpdvanwyk/invertergui/datasource" "fmt"
"github.com/hpdvanwyk/invertergui/mk2if"
"reflect" "reflect"
"testing" "testing"
"time" "time"
) )
type mockSource struct {
}
func NewMockSource() datasource.DataSource {
return &mockSource{}
}
func (s *mockSource) GetData(status *datasource.MultiplusStatus) error {
status.OutCurrent = 2.0
status.InCurrent = 2.3
status.OutVoltage = 230.0
status.InVoltage = 230.1
status.BatVoltage = 25
status.BatCurrent = -10
status.InFreq = 50
status.OutFreq = 50
status.Leds = []int{0, 0, 0, 0, 1, 0, 0, 1}
return nil
}
func TestWebGui(t *testing.T) { func TestWebGui(t *testing.T) {
t.Skip("Not yet implimented") t.Skip("Not yet implimented")
//TODO figure out how to test template output. //TODO figure out how to test template output.
} }
type templateTest struct { type templateTest struct {
input *statusProcessed input *mk2if.Mk2Info
output *templateInput output *templateInput
} }
var fakenow = time.Date(2017, 1, 2, 3, 4, 5, 6, time.UTC) var fakenow = time.Date(2017, 1, 2, 3, 4, 5, 6, time.UTC)
var templateInputTests = []templateTest{ var templateInputTests = []templateTest{
{ {
input: &statusProcessed{ input: &mk2if.Mk2Info{
status: datasource.MultiplusStatus{ OutCurrent: 2.0,
OutCurrent: 2.0, InCurrent: 2.3,
InCurrent: 2.3, OutVoltage: 230.0,
OutVoltage: 230.0, InVoltage: 230.1,
InVoltage: 230.1, BatVoltage: 25,
BatVoltage: 25, BatCurrent: -10,
BatCurrent: -10, InFrequency: 50,
InFreq: 50, OutFrequency: 50,
OutFreq: 50, ChargeState: 1,
Leds: []int{0, 0, 0, 0, 1, 0, 0, 1}}, LedListOn: []int{mk2if.LED_MAIN, mk2if.LED_FLOAT},
err: nil, Errors: nil,
Timestamp: fakenow,
}, },
output: &templateInput{ output: &templateInput{
Error: nil, Error: nil,
@@ -97,16 +79,18 @@ var templateInputTests = []templateTest{
BatCurrent: "-10.000", BatCurrent: "-10.000",
BatPower: "-250.000", BatPower: "-250.000",
InFreq: "50.000", InFreq: "50.000",
BatCharge: "0.000", OutFreq: "50.000",
BatCharge: "100.000",
Leds: []string{"Mains", "Float"}}, Leds: []string{"Mains", "Float"}},
}, },
} }
func TestTemplateInput(t *testing.T) { func TestTemplateInput(t *testing.T) {
for i := range templateInputTests { for i := range templateInputTests {
templateInput := buildTemplateInput(templateInputTests[i].input, fakenow) templateInput := buildTemplateInput(templateInputTests[i].input)
if !reflect.DeepEqual(templateInput, templateInputTests[i].output) { if !reflect.DeepEqual(templateInput, templateInputTests[i].output) {
t.Errorf("buildTemplateInput not producing expected results") t.Errorf("buildTemplateInput not producing expected results")
fmt.Printf("%v\n%v\n", templateInput, templateInputTests[i].output)
} }
} }
} }