diff --git a/cmd/invertergui/main.go b/cmd/invertergui/main.go index d1b8044..25a9f8c 100644 --- a/cmd/invertergui/main.go +++ b/cmd/invertergui/main.go @@ -44,7 +44,8 @@ func main() { flag.Parse() source := datasource.NewJSONSource(*url) - gui := webgui.NewWebGui(source, 10*time.Second, 100) + poller := datasource.NewDataPoller(source, 10*time.Second) + gui := webgui.NewWebGui(poller, 100) http.Handle("/", gui) http.Handle("/munin", http.HandlerFunc(gui.ServeMuninHTTP)) http.Handle("/muninconfig", http.HandlerFunc(gui.ServeMuninConfigHTTP)) diff --git a/datasource/datapoller.go b/datasource/datapoller.go new file mode 100644 index 0000000..c1cdf30 --- /dev/null +++ b/datasource/datapoller.go @@ -0,0 +1,99 @@ +/* +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 +} diff --git a/datasource/poller_test.go b/datasource/poller_test.go new file mode 100644 index 0000000..1cd8897 --- /dev/null +++ b/datasource/poller_test.go @@ -0,0 +1,91 @@ +/* +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() +} diff --git a/webgui/munin.go b/webgui/munin.go index 9866743..44b4311 100644 --- a/webgui/munin.go +++ b/webgui/munin.go @@ -37,7 +37,7 @@ import ( ) type muninData struct { - statusError statusError + statusP statusProcessed timesUpdated int } @@ -50,8 +50,8 @@ func (w *WebGui) ServeMuninHTTP(rw http.ResponseWriter, r *http.Request) { } calcMuninAverages(&muninDat) - statusErr := &muninDat.statusError - tmpInput := buildTemplateInput(statusErr) + statusP := &muninDat.statusP + tmpInput := buildTemplateInput(statusP) outputBuf := &bytes.Buffer{} fmt.Fprintf(outputBuf, "multigraph in_batvolt\n") fmt.Fprintf(outputBuf, "volt.value %s\n", tmpInput.BatVoltage) @@ -166,9 +166,9 @@ freq.label Input frequency (Hz) } //Munin only samples once every 5 minutes so averages have to be calculated for some values. -func calcMuninValues(muninDat *muninData, newStatus *statusError) { +func calcMuninValues(muninDat *muninData, newStatus *statusProcessed) { muninDat.timesUpdated += 1 - muninVal := &muninDat.statusError + muninVal := &muninDat.statusP muninVal.status.OutCurrent += newStatus.status.OutCurrent muninVal.status.InCurrent += newStatus.status.InCurrent muninVal.status.BatCurrent += newStatus.status.BatCurrent @@ -184,7 +184,7 @@ func calcMuninValues(muninDat *muninData, newStatus *statusError) { } func calcMuninAverages(muninDat *muninData) { - muninVal := &muninDat.statusError + muninVal := &muninDat.statusP muninVal.status.OutCurrent /= float64(muninDat.timesUpdated) muninVal.status.InCurrent /= float64(muninDat.timesUpdated) muninVal.status.BatCurrent /= float64(muninDat.timesUpdated) @@ -196,7 +196,7 @@ func calcMuninAverages(muninDat *muninData) { func zeroMuninValues(muninDat *muninData) { muninDat.timesUpdated = 0 - muninVal := &muninDat.statusError + muninVal := &muninDat.statusP muninVal.status.OutCurrent = 0 muninVal.status.InCurrent = 0 muninVal.status.BatCurrent = 0 diff --git a/webgui/webgui.go b/webgui/webgui.go index cabc60a..2deb9ab 100644 --- a/webgui/webgui.go +++ b/webgui/webgui.go @@ -35,7 +35,7 @@ import ( "github.com/hpdvanwyk/invertergui/datasource" "html/template" "net/http" - "time" + "sync" ) const ( @@ -61,29 +61,29 @@ var leds = map[int]string{ } type WebGui struct { - source datasource.DataSource - reqChan chan *statusError - respChan chan statusError + respChan chan statusProcessed stopChan chan struct{} template *template.Template muninRespChan chan muninData + poller datasource.DataPoller + wg sync.WaitGroup } -func NewWebGui(source datasource.DataSource, pollRate time.Duration, batteryCapacity float64) *WebGui { - wg := new(WebGui) - wg.source = source - wg.reqChan = make(chan *statusError) - wg.respChan = make(chan statusError) - wg.muninRespChan = make(chan muninData) - wg.stopChan = make(chan struct{}) +func NewWebGui(source datasource.DataPoller, batteryCapacity float64) *WebGui { + w := new(WebGui) + w.respChan = make(chan statusProcessed) + w.muninRespChan = make(chan muninData) + w.stopChan = make(chan struct{}) var err error - wg.template, err = template.New("thegui").Parse(htmlTemplate) + w.template, err = template.New("thegui").Parse(htmlTemplate) if err != nil { panic(err) } - go wg.dataPoll(pollRate, batteryCapacity) - return wg + w.poller = source + w.wg.Add(1) + go w.dataPoll(batteryCapacity) + return w } //TemplateInput is exported to be used as an argument to the http template package. @@ -121,7 +121,7 @@ func (w *WebGui) ServeHTTP(rw http.ResponseWriter, r *http.Request) { } } -func buildTemplateInput(statusErr *statusError) *TemplateInput { +func buildTemplateInput(statusErr *statusProcessed) *TemplateInput { status := statusErr.status outPower := status.OutVoltage * status.OutCurrent inPower := status.InCurrent * status.InVoltage @@ -143,65 +143,56 @@ func buildTemplateInput(statusErr *statusError) *TemplateInput { BatPower: fmt.Sprintf("%.3f", status.BatVoltage*status.BatCurrent), BatCharge: fmt.Sprintf("%.3f", statusErr.chargeLevel), } - for i := 7; i >= 0; i-- { - if status.Leds[i] == 1 { - tmpInput.Leds = append(tmpInput.Leds, leds[i]) + if len(status.Leds) == 8 { + for i := 7; i >= 0; i-- { + if status.Leds[i] == 1 { + tmpInput.Leds = append(tmpInput.Leds, leds[i]) + } } } return tmpInput } func (w *WebGui) Stop() { + w.poller.Stop() close(w.stopChan) + w.wg.Wait() } -type statusError struct { +type statusProcessed struct { status datasource.MultiplusStatus chargeLevel float64 err error } -// dataPoll will issue a request for a new status every pollRate. 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. -func (w *WebGui) dataPoll(pollRate time.Duration, batteryCapacity float64) { - ticker := time.NewTicker(pollRate) +func (w *WebGui) dataPoll(batteryCapacity float64) { tracker := NewChargeTracker(batteryCapacity) - var statusErr statusError + pollChan := w.poller.C() + var statusP statusProcessed var muninValues muninData - 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 + case s := <-pollChan: + if s.Err != nil { + statusP.err = s.Err } else { - statusErr.status = s.status - statusErr.err = nil - tracker.Update(s.status.BatCurrent) - if s.status.Leds[Float] == 1 { + statusP.status = s.MpStatus + statusP.err = nil + tracker.Update(s.MpStatus.BatCurrent) + if s.MpStatus.Leds[Float] == 1 { tracker.Reset() } - statusErr.chargeLevel = tracker.CurrentLevel() - calcMuninValues(&muninValues, &statusErr) + statusP.chargeLevel = tracker.CurrentLevel() + calcMuninValues(&muninValues, &statusP) } - gettingStatus = false - case w.respChan <- statusErr: + case w.respChan <- statusP: case w.muninRespChan <- muninValues: zeroMuninValues(&muninValues) case <-w.stopChan: + w.wg.Done() 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 index 40c6f6a..1ecf4b7 100644 --- a/webgui/webgui_test.go +++ b/webgui/webgui_test.go @@ -62,13 +62,13 @@ func TestWebGui(t *testing.T) { } type templateTest struct { - input *statusError + input *statusProcessed output *TemplateInput } var templateInputTests = []templateTest{ { - input: &statusError{ + input: &statusProcessed{ status: datasource.MultiplusStatus{ OutCurrent: 2.0, InCurrent: 2.3,