shrooms-server/shroom_server.go

260 lines
6.3 KiB
Go

package main
import (
_ "github.com/mattn/go-sqlite3"
)
import (
s "shroom_server/shroom_internals"
)
import (
"database/sql"
"embed"
"encoding/json"
"fmt"
"io/fs"
"log"
"net/http"
"strconv"
"strings"
"time"
)
//go:embed auth_secret
var auth_secret string
//go:embed static/*
var content embed.FS
type statusJson struct {
Connected bool `json:"connected"`
Humidifier bool `json:"humidifier"`
Humidifier2 bool `json:"humidifier2"`
ManualMode bool `json:"manual_mode"`
}
type adminMsg struct {
Auth string `json:"auth"`
Msg map[string]interface{} `json:"data"`
}
// returns a function that multiplies the number at the very last segment of the url
// and returns the data that was collected in the last n*multiplier milliseconds
func dumpData(db *sql.DB, dc *s.DataCache, multiplier int64) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
now := time.Now().Unix()
path := strings.Split(req.URL.Path, "/")
last := path[len(path)-1]
count, err := strconv.Atoi(last)
if err != nil {
w.WriteHeader(400)
w.Write([]byte("could not read integer in path: " + err.Error()))
return
}
offset := int64(count) * multiplier
t := now*1000 - offset
//log.Println("req start ", t)
var msg []byte
cachedPoints := dc.ReadSince(uint64(t))
if cachedPoints != nil {
msg, err = s.DatapointsToJson(cachedPoints)
if err != nil {
msg = nil
}
}
// fallback to actually reading from the sql
if msg == nil {
log.Println("unable to read from cache for ", t, " using sql")
msg, err = s.GetRows(db, t)
}
if err != nil {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
} else {
w.Write(msg)
}
}
}
func main() {
db, err := sql.Open("sqlite3", "shrooms.db")
if err != nil {
log.Fatal("unable to open db ", err)
}
if err = db.Ping(); err != nil {
log.Fatal("unable to ping db ", err)
}
err = s.CreateTable(db)
if err != nil {
log.Fatal("unable to create table ", err)
}
state := s.NewShroomState()
s.InitTcpServer(db, &state)
contentSub, err := fs.Sub(content, "static")
if err != nil {
log.Fatal("unable to use subdirectory of embedded fs: ", err)
}
dumpWeek := dumpData(db, &state.Cache, 7*24*60*60*1000)
dumpDay := dumpData(db, &state.Cache, 24*60*60*1000)
dumpHour := dumpData(db, &state.Cache, 60*60*1000)
dumpMinute := dumpData(db, &state.Cache, 60*1000)
dumpSecond := dumpData(db, &state.Cache, 1000)
lastPoint := func(w http.ResponseWriter, _req *http.Request) {
var err error
time := state.Cache.LatestTime()
if time > 0 {
msg, err := json.Marshal(time)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
} else {
w.Write(msg)
}
return
}
msg, err := s.LastTime(db)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
} else {
w.Write(msg)
}
}
getStatus := func(w http.ResponseWriter, _req *http.Request) {
state.RLock()
num_connections := state.NumConnections
humidifier := state.HumidifierOn
humidifier2 := state.Humidifier2On
manual_mode := state.ManualMode
state.RUnlock()
s := statusJson{
Connected: num_connections > 0,
Humidifier: humidifier,
Humidifier2: humidifier2,
ManualMode: manual_mode,
}
msg, err := json.Marshal(s)
if err != nil {
err = fmt.Errorf("unable to marshal json: %w", err)
w.WriteHeader(400)
w.Write([]byte(err.Error()))
} else {
w.Write(msg)
}
}
adminHandler := func(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
w.WriteHeader(405)
w.Write([]byte("method must be POST"))
return
}
msg := make([]byte, 256)
l, err := req.Body.Read(msg)
if err != nil && l == 0 {
err = fmt.Errorf("unable to read body: %w", err)
w.WriteHeader(400)
w.Write([]byte(err.Error()))
return
}
adminReq := adminMsg{}
err = json.Unmarshal(msg[:l], &adminReq)
if err != nil {
err = fmt.Errorf("unable to unmarshal body as json: %w", err)
w.WriteHeader(400)
w.Write([]byte(err.Error()))
return
}
// switch to embedded secret
if adminReq.Auth != auth_secret {
w.WriteHeader(401)
w.Write([]byte("invalid secret"))
return
}
inner_msg, err := json.Marshal(adminReq.Msg)
if err != nil {
err = fmt.Errorf("unable to marshal inner message: %w", err)
w.WriteHeader(400)
w.Write([]byte(err.Error()))
return
}
select {
case state.Commands <- inner_msg:
w.Write([]byte("ok"))
default:
w.WriteHeader(503)
w.Write([]byte("unable to forward request; controller may not be connected"))
}
}
paramsHandler := func(w http.ResponseWriter, req *http.Request) {
msg, err := s.QueryParams(&state)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(err.Error()))
return
}
w.Write(msg)
}
updateHandler := func(w http.ResponseWriter, req *http.Request) {
s.WaitForClose(state.Wait)
w.Write([]byte("ok"))
}
statusUpdateHandler := func(w http.ResponseWriter, req *http.Request) {
s.WaitForClose(state.StatusWait)
w.Write([]byte("ok"))
}
http.Handle("/d/", http.StripPrefix("/d/", http.FileServer(http.Dir("./dev"))))
http.Handle("/", http.FileServer(http.FS(contentSub)))
http.HandleFunc("/api/last/weeks/", dumpWeek)
http.HandleFunc("/api/last/days/", dumpDay)
http.HandleFunc("/api/last/hours/", dumpHour)
http.HandleFunc("/api/last/minutes/", dumpMinute)
http.HandleFunc("/api/last/seconds/", dumpSecond)
http.HandleFunc("/api/latest", lastPoint)
http.HandleFunc("/api/status", getStatus)
http.HandleFunc("/api/admin", adminHandler)
http.HandleFunc("/api/params", paramsHandler)
http.HandleFunc("/api/update", updateHandler)
http.HandleFunc("/api/status_update", statusUpdateHandler)
// periodically clear old entries from the database
go func() {
// TODO maybe make this exit gracefully
for {
t, err := s.OldestTime(db)
if err != nil {
log.Println("unable to get oldest time: ", err)
}
now := time.Now().Unix()
diff := now*1000 - t
log.Println("oldest time", t, " current time", now, "diff", diff)
if diff > 2*7*24*60*60*1000 {
err = s.ClearOldRows(db, 1000*now-8*24*60*60*1000)
if err != nil {
log.Println("unable to delete rows: ", err)
}
}
time.Sleep(24 * time.Hour)
}
}()
err = http.ListenAndServe(":8085", nil)
if err != nil {
log.Fatal("unable to start server: ", err)
}
defer db.Close()
}