summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--go.mod6
-rw-r--r--go.sum69
-rw-r--r--server/building.go6
-rw-r--r--server/chart.go55
-rw-r--r--server/dashboard.go8
-rw-r--r--server/dashboard.html4
-rw-r--r--server/record.go18
-rw-r--r--server/server.go1
8 files changed, 152 insertions, 15 deletions
diff --git a/go.mod b/go.mod
index 413036e..268c89c 100644
--- a/go.mod
+++ b/go.mod
@@ -3,3 +3,9 @@ module git.samanthony.xyz/hvacserver
go 1.23.1
require github.com/sam-rba/share v0.2.1
+
+require (
+ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
+ github.com/wcharczuk/go-chart/v2 v2.1.2 // indirect
+ golang.org/x/image v0.22.0 // indirect
+)
diff --git a/go.sum b/go.sum
index 897bc8b..ca7bfa8 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,71 @@
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/sam-rba/share v0.2.1 h1:cqC+Li72A27hM3E/XdArv2cgbOX//MXutRDg1vzPuZA=
github.com/sam-rba/share v0.2.1/go.mod h1:rDx/wFG1Vc6JO1F/F0eBnkcy2wHwV0T9100ig/hu12A=
+github.com/wcharczuk/go-chart v2.0.1+incompatible h1:0pz39ZAycJFF7ju/1mepnk26RLVLBCWz1STcD3doU0A=
+github.com/wcharczuk/go-chart v2.0.1+incompatible/go.mod h1:PF5tmL4EIx/7Wf+hEkpCqYi5He4u90sw+0+6FhrryuE=
+github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
+github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
+golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
+golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/server/building.go b/server/building.go
index 8772374..e275c65 100644
--- a/server/building.go
+++ b/server/building.go
@@ -23,10 +23,10 @@ func (b Building) average() (Humidity, bool) {
var sum Humidity = 0
nRooms := 0
for room, record := range b {
- c := make(chan Humidity)
+ c := make(chan Entry[Humidity])
record.getRecent <- c
- if humidity, ok := <-c; ok {
- sum += humidity
+ if e, ok := <-c; ok {
+ sum += e.v
nRooms++
} else {
log.Printf("Warning: no humidity for room '%s'\n", room)
diff --git a/server/chart.go b/server/chart.go
new file mode 100644
index 0000000..1f20d3c
--- /dev/null
+++ b/server/chart.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+ "log"
+ "net/http"
+ "fmt"
+ "time"
+ "github.com/wcharczuk/go-chart/v2"
+)
+
+type ChartHandler struct {
+ building Building
+}
+
+func (h ChartHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ log.Println(r.Method, r.URL)
+
+ if r.Method != http.MethodGet {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ fmt.Fprintf(w, "invalid method: '%s'", r.Method)
+ return
+ }
+
+ var series []chart.Series
+ for room, record := range h.building {
+ var x []time.Time
+ var y []float64
+ c := make(chan Entry[Humidity])
+ record.get <- c
+ for e := range c {
+ x = append(x, e.t)
+ y = append(y, float64(e.v))
+ }
+ series = append(series, chart.TimeSeries{
+ Name: string(room),
+ XValues: x,
+ YValues: y,
+ })
+ }
+
+ graph := chart.Chart{
+ Background: chart.Style{
+ Padding: chart.Box{Top: 20, Left: 20},
+ },
+ Series: series,
+ }
+ graph.Elements = []chart.Renderable{
+ chart.Legend(&graph),
+ }
+
+ w.Header().Set("Content-Type", "image/png")
+ if err := graph.Render(chart.PNG, w); err != nil {
+ log.Println(err)
+ }
+}
diff --git a/server/dashboard.go b/server/dashboard.go
index 48a9423..1dca024 100644
--- a/server/dashboard.go
+++ b/server/dashboard.go
@@ -65,10 +65,12 @@ func (h DashboardHandler) buildDashboard() Dashboard {
rooms := make(map[RoomID]Humidity)
for id, record := range h.building {
- c := make(chan Humidity)
+ c := make(chan Entry[Humidity])
record.getRecent <- c
- humidity, ok := <-c
- if !ok {
+ var humidity Humidity
+ if e, ok := <-c; ok {
+ humidity = e.v
+ } else {
humidity = -1
}
rooms[id] = humidity
diff --git a/server/dashboard.html b/server/dashboard.html
index 23e1d97..50ad682 100644
--- a/server/dashboard.html
+++ b/server/dashboard.html
@@ -21,6 +21,7 @@
unknown
{{- end -}}
</p>
+
<table>
<tr><th>Room</th><th>Humidity</th></tr>
{{ range $id, $humidity := .Rooms }}
@@ -37,5 +38,8 @@
</tr>
{{ end }}
</table>
+
+ <h4 style="text-align: center;">Humidity per Room vs. Time</h4>
+ <img src="/chart.png" alt="chart of humidity vs time"/>
</body>
</html>
diff --git a/server/record.go b/server/record.go
index c2f38eb..407dcd7 100644
--- a/server/record.go
+++ b/server/record.go
@@ -6,11 +6,11 @@ import (
type Record[T any] struct {
put chan<- T
- get chan<- chan T
- getRecent chan<- chan T
+ get chan<- chan Entry[T]
+ getRecent chan<- chan Entry[T]
}
-type entry[T any] struct {
+type Entry[T any] struct {
t time.Time
v T
}
@@ -19,11 +19,11 @@ type entry[T any] struct {
// If the capacity is exceeded, old entires will be discarded and new ones kept.
func newRecord[T any](capacity int) Record[T] {
put := make(chan T)
- get := make(chan chan T)
- getRecent := make(chan chan T)
+ get := make(chan chan Entry[T])
+ getRecent := make(chan chan Entry[T])
go func() {
- entries := make([]entry[T], 0, capacity)
+ entries := make([]Entry[T], 0, capacity)
for {
select {
@@ -31,7 +31,7 @@ func newRecord[T any](capacity int) Record[T] {
if !ok {
return
}
- entries = append(entries, entry[T]{time.Now(), v})
+ entries = append(entries, Entry[T]{time.Now(), v})
if len(entries) > capacity {
entries = entries[1:]
}
@@ -40,7 +40,7 @@ func newRecord[T any](capacity int) Record[T] {
return
}
for _, e := range entries {
- c <- e.v
+ c <- e
}
close(c)
case c, ok := <-getRecent:
@@ -48,7 +48,7 @@ func newRecord[T any](capacity int) Record[T] {
return
}
if len(entries) > 0 {
- c <- entries[len(entries)-1].v
+ c <- entries[len(entries)-1]
}
close(c)
}
diff --git a/server/server.go b/server/server.go
index 4ef5151..8cfb1b0 100644
--- a/server/server.go
+++ b/server/server.go
@@ -33,6 +33,7 @@ func main() {
http.Handle("/humidity", HumidityHandler{building})
http.Handle("/target_humidity", TargetHumidityHandler{target})
http.Handle("/duty_cycle", DutyCycleHandler{dutyCycle})
+ http.Handle("/chart.png", ChartHandler{building})
fmt.Printf("Listening on %s...\n", addr)
log.Fatal(http.ListenAndServe(addr, nil))