diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dbd7d47 --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +#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. + +.PHONY: test install gofmt + +install: + go install ./... + +all: install gofmt test + +gofmt: + gofmt -l -s -w . + +test: + go test -v ./... + +test-race: + go test -v -race ./... + +vet: + go vet ./... diff --git a/README.md b/README.md index 1adc850..9521d63 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ # invertergui + +A primitive HTTP based monitor for a Victron Multiplus inverter. Uses https://github.com/ncthompson/inverter_monitor as +a data source. \ No newline at end of file diff --git a/cmd/invertergui/main.go b/cmd/invertergui/main.go new file mode 100644 index 0000000..b01441e --- /dev/null +++ b/cmd/invertergui/main.go @@ -0,0 +1,50 @@ +/* +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 main + +import ( + "flag" + "github.com/hpdvanwyk/invertergui/datasource" + "github.com/hpdvanwyk/invertergui/webgui" + "log" + "net/http" + "time" +) + +func main() { + url := flag.String("url", "http://localhost:9005", "The url of the multiplus JSON interface.") + flag.Parse() + + source := datasource.NewJSONSource(*url) + gui := webgui.NewWebGui(source, 10*time.Second) + http.Handle("/", gui) + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/datasource/decode_test.go b/datasource/decode_test.go new file mode 100644 index 0000000..1471e16 --- /dev/null +++ b/datasource/decode_test.go @@ -0,0 +1,69 @@ +/* +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() +} diff --git a/datasource/jsondecode.go b/datasource/jsondecode.go new file mode 100644 index 0000000..0505a7c --- /dev/null +++ b/datasource/jsondecode.go @@ -0,0 +1,77 @@ +/* +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 +} diff --git a/webgui/htmltemplate.go b/webgui/htmltemplate.go new file mode 100644 index 0000000..946b0a4 --- /dev/null +++ b/webgui/htmltemplate.go @@ -0,0 +1,70 @@ +/* +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 webgui + +var htmlTemplate string = ` + + + + + + Victron Multiplus Monitor + + + + {{if .Error}}

Error encountered: {{.Error}}

{{end}} +
+
LEDs:
+ {{range .Leds}} +
{{.}}
+ {{else}} + None + {{end}} +
+
+
Output Current: {{.OutCurrent}} A
+
Output Voltage: {{.OutVoltage}} V
+
Output Power: {{.OutPower}} VA
+
+
+
Input Current: {{.InCurrent}} A
+
Input Voltage: {{.InVoltage}} V
+
Input Frequency: {{.InFreq}} Hz
+
Input Power: {{.InPower}} VA
+
+

Input - Output Power: {{.InMinOut}} VA

+
+
Battery Current: {{.BatCurrent}} A
+
Battery Voltage: {{.BatVoltage}} V
+
Battery Power: {{.BatPower}} W
+
+ +` diff --git a/webgui/webgui.go b/webgui/webgui.go new file mode 100644 index 0000000..78a88ec --- /dev/null +++ b/webgui/webgui.go @@ -0,0 +1,180 @@ +/* +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 webgui + +import ( + "fmt" + "github.com/hpdvanwyk/invertergui/datasource" + "html/template" + "net/http" + "time" +) + +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 { + source datasource.DataSource + reqChan chan *statusError + respChan chan statusError + stopChan chan struct{} + template *template.Template +} + +func NewWebGui(source datasource.DataSource, pollRate time.Duration) *WebGui { + wg := new(WebGui) + wg.source = source + wg.reqChan = make(chan *statusError) + wg.respChan = make(chan statusError) + wg.stopChan = make(chan struct{}) + var err error + wg.template, err = template.New("thegui").Parse(htmlTemplate) + if err != nil { + panic(err) + } + go wg.dataPoll(pollRate) + return wg +} + +//TemplateInput is exported to be used as an argument to the http template package. +type TemplateInput struct { + Error error + + OutCurrent string + OutVoltage string + OutPower string + + InCurrent string + InVoltage string + InPower string + + InMinOut string + + BatVoltage string + BatCurrent string + BatPower string + + InFreq string + + Leds []string +} + +func (w *WebGui) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + statusErr := <-w.respChan + + tmpInput := buildTemplateInput(&statusErr) + + err := w.template.Execute(rw, tmpInput) + if err != nil { + panic(err) + } +} + +func buildTemplateInput(statusErr *statusError) *TemplateInput { + status := statusErr.status + outPower := status.OutVoltage * status.OutCurrent + inPower := status.InCurrent * status.InVoltage + + tmpInput := &TemplateInput{ + Error: statusErr.err, + OutCurrent: fmt.Sprintf("%.3f", status.OutCurrent), + OutVoltage: fmt.Sprintf("%.3f", status.OutVoltage), + OutPower: fmt.Sprintf("%.3f", outPower), + InCurrent: fmt.Sprintf("%.3f", status.InCurrent), + InVoltage: fmt.Sprintf("%.3f", status.InVoltage), + InFreq: fmt.Sprintf("%.3f", status.InFreq), + InPower: fmt.Sprintf("%.3f", inPower), + + InMinOut: fmt.Sprintf("%.3f", inPower-outPower), + + BatCurrent: fmt.Sprintf("%.3f", status.BatCurrent), + BatVoltage: fmt.Sprintf("%.3f", status.BatVoltage), + BatPower: fmt.Sprintf("%.3f", status.BatVoltage*status.BatCurrent), + } + for i := 7; i >= 0; i-- { + if status.Leds[i] == 1 { + tmpInput.Leds = append(tmpInput.Leds, leds[i]) + } + } + return tmpInput +} + +func (w *WebGui) Stop() { + close(w.stopChan) +} + +type statusError struct { + status datasource.MultiplusStatus + err error +} + +// dataPoll will issue a request for a new status every pollRate. It will send its currently stored status +// to respChan if anything reads from it. +func (w *WebGui) dataPoll(pollRate time.Duration) { + ticker := time.NewTicker(pollRate) + var statusErr statusError + go w.getStatus() + gettingStatus := true + for { + select { + case <-ticker.C: + if gettingStatus == false { + go w.getStatus() + gettingStatus = true + } + case s := <-w.reqChan: + if s.err != nil { + statusErr.err = s.err + } else { + statusErr.status = s.status + statusErr.err = nil + } + gettingStatus = false + case w.respChan <- statusErr: + case <-w.stopChan: + return + } + } +} + +func (w *WebGui) getStatus() { + statusErr := new(statusError) + statusErr.err = w.source.GetData(&statusErr.status) + w.reqChan <- statusErr +} diff --git a/webgui/webgui_test.go b/webgui/webgui_test.go new file mode 100644 index 0000000..d6f62a3 --- /dev/null +++ b/webgui/webgui_test.go @@ -0,0 +1,108 @@ +/* +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 webgui + +import ( + "github.com/hpdvanwyk/invertergui/datasource" + "reflect" + "testing" +) + +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) { + t.Skip("Not yet implimented") + //TODO figure out how to test template output. +} + +type templateTest struct { + input *statusError + output *TemplateInput +} + +var templateInputTests = []templateTest{ + { + input: &statusError{ + status: datasource.MultiplusStatus{ + OutCurrent: 2.0, + InCurrent: 2.3, + OutVoltage: 230.0, + InVoltage: 230.1, + BatVoltage: 25, + BatCurrent: -10, + InFreq: 50, + OutFreq: 50, + Leds: []int{0, 0, 0, 0, 1, 0, 0, 1}}, + err: nil, + }, + output: &TemplateInput{ + Error: nil, + OutCurrent: "2.000", + OutVoltage: "230.000", + OutPower: "460.000", + InCurrent: "2.300", + InVoltage: "230.100", + InPower: "529.230", + InMinOut: "69.230", + BatVoltage: "25.000", + BatCurrent: "-10.000", + BatPower: "-250.000", + InFreq: "50.000", + Leds: []string{"Mains", "Float"}}, + }, +} + +func TestTemplateInput(t *testing.T) { + for i := range templateInputTests { + templateInput := buildTemplateInput(templateInputTests[i].input) + if !reflect.DeepEqual(templateInput, templateInputTests[i].output) { + t.Errorf("buildTemplateInput not producing expected results") + } + } +}