// TSGRain WebUI // // REST API for the TSGRain WebUI // // --- // Schemes: http, https // Version: 0.1.0 // License: MIT // // swagger:meta package server import ( "errors" "fmt" "net/http" "strconv" "time" "code.thetadev.de/TSGRain/WebUI/src/config" "code.thetadev.de/TSGRain/WebUI/src/model" "code.thetadev.de/TSGRain/WebUI/src/server/middleware" "code.thetadev.de/TSGRain/WebUI/src/server/stream" "code.thetadev.de/TSGRain/WebUI/src/server/swagger" "code.thetadev.de/TSGRain/WebUI/src/tsgrain_rpc" "code.thetadev.de/TSGRain/WebUI/src/util" "code.thetadev.de/TSGRain/WebUI/src/util/mode" "code.thetadev.de/TSGRain/WebUI/ui" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" ) type WebUIServer struct { config *config.Config streamer *stream.API rpc *tsgrain_rpc.RPCClient nZones int32 } func NewServer(config *config.Config) *WebUIServer { streamer := stream.New( time.Duration(config.Server.Websocket.Ping)*time.Second, time.Duration(config.Server.Websocket.Timeout)*time.Second, []string{}, ) rpc := tsgrain_rpc.NewClient(config.GRPC.Address) return &WebUIServer{ config: config, streamer: streamer, rpc: rpc, } } func (srv *WebUIServer) getRouter() *gin.Engine { router := gin.New() router.Use(gin.Logger()) _ = router.SetTrustedProxies(nil) if mode.IsDev() { router.Use(cors.Default()) } router.Use(middleware.ErrorHandler(false), middleware.PanicHandler(false)) router.NoRoute(func(c *gin.Context) { c.Error(util.ErrPageNotFound) }) if srv.config.Authentication.Enable { router.Use(middleware.Authentication(srv.config.Authentication.PasswdFile)) } api := router.Group("/api", middleware.ErrorHandler(true), middleware.PanicHandler(true)) // API ROUTES api.GET("/ws", srv.streamer.Handle) // Error routes for testing if mode.IsDev() { router.GET("/error", srv.controllerError) router.GET("/panic", srv.controllerPanic) api.GET("/error", srv.controllerError) api.GET("/panic", srv.controllerPanic) } api.POST("/task/start", srv.controllerStartTask) api.POST("/task/stop", srv.controllerStopTask) api.GET("/tasks", srv.controllerGetTasks) api.GET("/jobs", srv.controllerGetJobs) api.GET("/job", srv.controllerGetJob) api.POST("/job", srv.controllerCreateJob) api.PUT("/job", srv.controllerUpdateJob) api.DELETE("/job", srv.controllerDeleteJob) api.GET("config/auto", srv.controllerGetAutoMode) api.POST("config/auto", srv.controllerSetAutoMode) api.GET("config/defaultIrrigationTime", srv.controllerGetDefaultIrrigationTime) api.POST("config/defaultIrrigationTime", srv.controllerSetDefaultIrrigationTime) api.GET("config/time", srv.controllerGetSystemTime) api.POST("config/time", srv.controllerSetSystemTime) api.POST("config/timezone", srv.controllerSetSystemTimezone) // UI uiGroup := router.Group("/", middleware.Compression( srv.config.Server.Compression.Gzip, srv.config.Server.Compression.Brotli), ) ui.Register(uiGroup, srv.nZones) swagger.Register(uiGroup) return router } func (srv *WebUIServer) Run() error { err := srv.rpc.Start() if err != nil { return err } // Get TSGRain info nZones, err := srv.rpc.GetNZones() if err != nil { return err } srv.nZones = nZones go srv.rpc.StreamTasks(srv.streamer) router := srv.getRouter() err = router.Run(fmt.Sprintf("%s:%d", srv.config.Server.Address, srv.config.Server.Port)) _ = srv.rpc.Stop() return err } // swagger:operation POST /task/start startTask // // Starte eine neue manuelle Bewässerungsaufgabe // mit der eingestellten Standardzeit. // // --- // consumes: [application/json] // produces: [application/json] // parameters: // - name: zoneId // in: body // description: ZoneID // schema: // $ref: '#/definitions/ZoneID' // responses: // 200: // description: OK // schema: // $ref: "#/definitions/StatusMessage" // 500: // description: Serverfehler // schema: // $ref: "#/definitions/Error" func (srv *WebUIServer) controllerStartTask(c *gin.Context) { var zoneId model.ZoneID if err := c.ShouldBindJSON(&zoneId); err != nil { log.Error().Err(err).Msg("StartTask input error") writeStatus(c, http.StatusBadRequest, "invalid input data") return } ok, err := srv.rpc.StartManualTask(zoneId.Id, 0) if err != nil { log.Error().Err(err).Msg("StartTask") writeStatus(c, http.StatusInternalServerError, "error starting task") return } if !ok { log.Error().Msg("StartTask: alrady running") writeStatus(c, http.StatusBadRequest, "task already running") return } writeStatus(c, http.StatusOK, "task started") } // swagger:operation POST /task/stop stopTask // // Stoppe eine manuelle Bewässerungsaufgabe. // // --- // consumes: [application/json] // produces: [application/json] // parameters: // - name: zoneId // in: body // description: ZoneID // schema: // $ref: '#/definitions/ZoneID' // responses: // 200: // description: OK // schema: // $ref: "#/definitions/StatusMessage" // 500: // description: Serverfehler // schema: // $ref: "#/definitions/Error" func (srv *WebUIServer) controllerStopTask(c *gin.Context) { var zoneId model.ZoneID if err := c.ShouldBindJSON(&zoneId); err != nil { log.Error().Err(err).Msg("StopTask input error") writeStatus(c, http.StatusBadRequest, "invalid input data") return } ok, err := srv.rpc.StopManualTask(zoneId.Id) if err != nil { log.Error().Err(err).Msg("StopTask") writeStatus(c, http.StatusInternalServerError, "error stopping task") return } if !ok { log.Error().Msg("StopTask: no task to stop") writeStatus(c, http.StatusBadRequest, "no task to stop") return } writeStatus(c, http.StatusOK, "task stopped") } // swagger:operation GET /tasks getTasks // // Rufe alle momentan laufenden Bewässerungsaufgaben ab. // // --- // produces: [application/json] // responses: // 200: // description: Liste der Bewässerungsaufgaben // schema: // "$ref": "#/definitions/TaskList" // 500: // description: Serverfehler // schema: // "$ref": "#/definitions/Error" func (srv *WebUIServer) controllerGetTasks(c *gin.Context) { tasks, err := srv.rpc.GetTasks() if err != nil { log.Error().Err(err).Msg("GetTasks") writeStatus(c, http.StatusInternalServerError, "error getting tasks") return } c.JSON(200, tasks) } // swagger:operation GET /jobs getJobs // // Rufe alle gespeicherten Zeitpläne ab. // // --- // produces: [application/json] // responses: // 200: // description: Job-Liste // schema: // "$ref": "#/definitions/JobList" // 500: // description: Serverfehler // schema: // "$ref": "#/definitions/Error" func (srv *WebUIServer) controllerGetJobs(c *gin.Context) { jobs, err := srv.rpc.GetJobs() if err != nil { log.Error().Err(err).Msg("GetJobs") writeStatus(c, http.StatusInternalServerError, "error getting jobs") return } c.JSON(200, jobs) } // swagger:operation GET /job getJob // // Rufe einen gespeicherten Zeitplan ab. // // --- // produces: [application/json] // parameters: // - name: id // in: query // description: Job-ID // type: integer // required: true // responses: // 200: // description: Job // schema: // $ref: "#/definitions/Job" // 500: // description: Serverfehler // schema: // $ref: "#/definitions/Error" func (srv *WebUIServer) controllerGetJob(c *gin.Context) { idStr := c.Query("id") id, err := strconv.ParseInt(idStr, 10, 32) if err != nil { log.Error().Err(err).Msg("GetJob input error") writeStatus(c, http.StatusBadRequest, "invalid input data") return } job, err := srv.rpc.GetJob(int32(id)) if err != nil { log.Error().Err(err).Msg("GetJob") writeStatus(c, http.StatusInternalServerError, "error getting job") return } c.JSON(200, job) } // swagger:operation POST /job createJob // // Erstelle einen neuen Zeitplan. // // --- // consumes: [application/json] // produces: [application/json] // parameters: // - name: job // in: body // description: Neuer Job // schema: // $ref: '#/definitions/NewJob' // required: true // responses: // 200: // description: OK // schema: // $ref: "#/definitions/JobID" // 500: // description: Serverfehler // schema: // $ref: "#/definitions/Error" func (srv *WebUIServer) controllerCreateJob(c *gin.Context) { var newJob model.NewJob if err := c.ShouldBindJSON(&newJob); err != nil { log.Error().Err(err).Msg("CreateJob input error") writeStatus(c, http.StatusBadRequest, "invalid input data") return } jobId, err := srv.rpc.CreateJob(newJob) if err != nil { log.Error().Err(err).Msg("CreateJob") writeStatus(c, http.StatusInternalServerError, "error creating job") return } c.JSON(200, model.JobID{Id: jobId}) } // swagger:operation PUT /job updateJob // // Aktualisiere einen gespeicherten Zeitplan. // // --- // consumes: [application/json] // produces: [application/json] // parameters: // - name: job // in: body // description: Aktualisierter Job // schema: // $ref: '#/definitions/Job' // required: true // responses: // 200: // description: OK // schema: // $ref: "#/definitions/StatusMessage" // 500: // description: Serverfehler // schema: // $ref: "#/definitions/Error" func (srv *WebUIServer) controllerUpdateJob(c *gin.Context) { var job model.Job if err := c.ShouldBindJSON(&job); err != nil { log.Error().Err(err).Msg("UpdateJob input error") writeStatus(c, http.StatusBadRequest, "invalid input data") return } err := srv.rpc.UpdateJob(job) if err != nil { log.Error().Err(err).Msg("UpdateJob") writeStatus(c, http.StatusInternalServerError, "error updating job") return } writeStatus(c, http.StatusOK, "updated job") } // swagger:operation DELETE /job deleteJob // // Lösche einen gespeicherten Zeitplan. // // --- // consumes: [application/json] // produces: [application/json] // parameters: // - name: jobId // in: body // description: JobID // schema: // $ref: '#/definitions/JobID' // responses: // 200: // description: OK // schema: // $ref: "#/definitions/StatusMessage" // 500: // description: Serverfehler // schema: // $ref: "#/definitions/Error" func (srv *WebUIServer) controllerDeleteJob(c *gin.Context) { var jobId model.JobID if err := c.ShouldBindJSON(&jobId); err != nil { log.Error().Err(err).Msg("DeleteJob input error") writeStatus(c, http.StatusBadRequest, "invalid input data") return } err := srv.rpc.DeleteJob(jobId.Id) if err != nil { log.Error().Err(err).Msg("DeleteJob") writeStatus(c, http.StatusInternalServerError, "error deleting job") return } writeStatus(c, http.StatusOK, "deleted job") } // swagger:operation GET /config/auto getAutoMode // // Rufe ab, ob der Automatikmodus aktiviert ist. // // --- // produces: [application/json] // responses: // 200: // description: Status des Automatikmodus // schema: // $ref: "#/definitions/AutoMode" // 500: // description: Serverfehler // schema: // $ref: "#/definitions/Error" func (srv *WebUIServer) controllerGetAutoMode(c *gin.Context) { state, err := srv.rpc.GetAutoMode() if err != nil { log.Error().Err(err).Msg("GetAutoMode") writeStatus(c, http.StatusInternalServerError, "error getting autoMode") return } c.JSON(200, model.AutoMode{State: state}) } // swagger:operation POST /config/auto setAutoMode // // Automatikmodus aktivieren/deaktivieren // // --- // consumes: [application/json] // produces: [application/json] // parameters: // - in: body // name: state // description: Zustand des Automatikmodus // schema: // $ref: '#/definitions/AutoMode' // responses: // 200: // description: OK // schema: // $ref: "#/definitions/StatusMessage" // 500: // description: Serverfehler // schema: // $ref: "#/definitions/Error" func (srv *WebUIServer) controllerSetAutoMode(c *gin.Context) { var autoMode model.AutoMode if err := c.ShouldBindJSON(&autoMode); err != nil { log.Error().Err(err).Msg("SetAutoMode input error") writeStatus(c, http.StatusBadRequest, "invalid input data") return } err := srv.rpc.SetAutoMode(autoMode.State) if err != nil { log.Error().Err(err).Msg("SetAutoMode") writeStatus(c, http.StatusInternalServerError, "error setting autoMode") return } writeStatus(c, http.StatusOK, "set autoMode") } // swagger:operation GET /config/time getSystemTime // // Rufe die aktuelle Systemzeit/Zeitzone ab // // --- // produces: [application/json] // responses: // 200: // description: Aktuelle Systemzeit/Zeitzone // schema: // $ref: "#/definitions/SystemTime" // 500: // description: Serverfehler // schema: // $ref: "#/definitions/Error" func (srv *WebUIServer) controllerGetSystemTime(c *gin.Context) { systemTime, err := srv.rpc.GetSystemTime() if err != nil { log.Error().Err(err).Msg("GetSystemTime") writeStatus(c, http.StatusInternalServerError, "error getting system time") return } c.JSON(200, systemTime) } // swagger:operation POST /config/time setSystemTime // // Systemzeit einstellen // // --- // consumes: [application/json] // produces: [application/json] // parameters: // - in: body // name: timestamp // schema: // $ref: "#/definitions/Timestamp" // responses: // 200: // description: OK // schema: // $ref: "#/definitions/StatusMessage" // 500: // description: Serverfehler // schema: // $ref: "#/definitions/Error" func (srv *WebUIServer) controllerSetSystemTime(c *gin.Context) { var systemTime model.Timestamp if err := c.ShouldBindJSON(&systemTime); err != nil { log.Error().Err(err).Msg("SetSystemTime input error") writeStatus(c, http.StatusBadRequest, "invalid input data") return } err := srv.rpc.SetSystemTime(systemTime.Time) if err != nil { log.Error().Err(err).Msg("SetSystemTime") writeStatus(c, http.StatusInternalServerError, "error setting systemTime") return } writeStatus(c, http.StatusOK, "set systemTime") } // swagger:operation POST /config/timezone setSystemTimezone // // Systemzeitzone einstellen // // --- // consumes: [application/json] // produces: [application/json] // parameters: // - in: body // name: timezone // schema: // $ref: "#/definitions/Timezone" // responses: // 200: // description: OK // schema: // $ref: "#/definitions/StatusMessage" // 500: // description: Serverfehler // schema: // $ref: "#/definitions/Error" func (srv *WebUIServer) controllerSetSystemTimezone(c *gin.Context) { var timezone model.Timezone if err := c.ShouldBindJSON(&timezone); err != nil { log.Error().Err(err).Msg("SetSystemTimezone input error") writeStatus(c, http.StatusBadRequest, "invalid input data") return } err := srv.rpc.SetSystemTimezone(timezone.Timezone) if err != nil { log.Error().Err(err).Msg("SetSystemTimezone") writeStatus(c, http.StatusInternalServerError, "error setting systemTimezone") return } writeStatus(c, http.StatusOK, "set systemTimezone") } // swagger:operation GET /config/defaultIrrigationTime getDefaultIrrigationTime // // Rufe die Standardzeit bei manueller Bewässerung ab. // // --- // produces: [application/json] // responses: // 200: // description: Manuelle Bewässerungszeit in Sekunden // schema: // $ref: "#/definitions/DefaultIrrigationTime" // 500: // description: Serverfehler // schema: // $ref: "#/definitions/Error" func (srv *WebUIServer) controllerGetDefaultIrrigationTime(c *gin.Context) { defaultIrrigationTime, err := srv.rpc.GetDefaultIrrigationTime() if err != nil { log.Error().Err(err).Msg("GetDefaultIrrigationTime") writeStatus(c, http.StatusInternalServerError, "error getting defaultIrrigationTime") return } c.JSON(200, model.DefaultIrrigationTime{Time: defaultIrrigationTime}) } // swagger:operation POST /config/defaultIrrigationTime setDefaultIrrigationTime // // Setze die die Standardzeit bei manueller Bewässerung. // // --- // consumes: [application/json] // produces: [application/json] // parameters: // - in: body // name: defaultIrrigationTime // schema: // $ref: "#/definitions/DefaultIrrigationTime" // responses: // 200: // description: OK // schema: // $ref: "#/definitions/StatusMessage" // 500: // description: Serverfehler // schema: // $ref: "#/definitions/Error" func (srv *WebUIServer) controllerSetDefaultIrrigationTime(c *gin.Context) { var defaultIrrigationTime model.DefaultIrrigationTime if err := c.ShouldBindJSON(&defaultIrrigationTime); err != nil { log.Error().Err(err).Msg("SetDefaultIrrigationTime input error") writeStatus(c, http.StatusBadRequest, "invalid input data") return } err := srv.rpc.SetDefaultIrrigationTime(defaultIrrigationTime.Time) if err != nil { log.Error().Err(err).Msg("SetDefaultIrrigationTime") writeStatus(c, http.StatusInternalServerError, "error setting defaultIrrigationTime") return } writeStatus(c, http.StatusOK, "set defaultIrrigationTime") } // controllerError throws an error for testing func (srv *WebUIServer) controllerError(c *gin.Context) { c.Error(util.HttpErrNew("error test", http.StatusBadRequest)) } // controllerPanic panics for testing func (srv *WebUIServer) controllerPanic(c *gin.Context) { panic(errors.New("panic message")) } func (srv *WebUIServer) validateZoneID(zoneId int32) bool { return 0 < zoneId && zoneId <= srv.nZones } func writeStatus(c *gin.Context, code int, msg string) { c.JSON(code, model.StatusMessage{ Success: code == http.StatusOK, Msg: msg, }) }