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() }