Compare commits

...

3 commits

Author SHA1 Message Date
834092a101 add ui base, api client
Some checks failed
continuous-integration/drone/push Build is failing
2022-02-03 21:40:18 +01:00
207db1c7a8 finished server 2022-02-03 16:15:43 +01:00
73208b6f51 finished grpc client 2022-02-02 00:50:25 +01:00
43 changed files with 5060 additions and 113 deletions

View file

@ -16,7 +16,7 @@ tmp_dir = "tmp"
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_error = true
stop_on_error = false
[color]
app = ""

View file

@ -7,16 +7,16 @@ repos:
- id: go-test-repo-mod
name: Backend tests
# - repo: local
# hooks:
# - id: tsc
# name: tsc
# entry: tsc
# language: node
# files: \.tsx?$
# args: ["-p", "./ui/tsconfig.json"]
# additional_dependencies: ["typescript@4.5.2"]
# pass_filenames: false
- repo: local
hooks:
- id: tsc
name: tsc
entry: tsc
language: node
files: \.tsx?$
args: ["-p", "./ui/tsconfig.json"]
additional_dependencies: ["typescript@4.5.2"]
pass_filenames: false
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.4.1

10
.vscode/launch.json vendored
View file

@ -2,9 +2,10 @@
"version": "0.2.0",
"configurations": [
{
"name": "SEBRAUC server",
"name": "TSGRain WebUI server",
"type": "go",
"request": "launch",
"cwd": "${workspaceFolder}",
"mode": "auto",
"program": "${workspaceFolder}/src"
},
@ -16,13 +17,6 @@
"runtimeExecutable": "npm",
"skipFiles": ["<node_internals>/**"],
"type": "pwa-node"
},
{
"name": "Test program",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/tmp"
}
]
}

View file

@ -35,9 +35,10 @@ protoc:
generate-apidoc:
SWAGGER_GENERATE_EXTENSION=false swagger generate spec --scan-models -o ${APIDOC_FILE}
swagger validate ${APIDOC_FILE}
generate-apiclient:
openapi-generator generate -i ${APIDOC_FILE} -g typescript-axios -o ${UI_DIR}/src/sebrauc-client -p "supportsES6=true"
openapi-generator generate -i ${APIDOC_FILE} -g typescript-axios -o ${UI_DIR}/src/tsgrain-client -p "supportsES6=true"
cd ${UI_DIR} && npm run format
clean:

View file

@ -87,6 +87,7 @@ message Task {
message TaskList {
repeated Task tasks = 1;
Timestamp now = 2;
bool auto_mode = 3;
}
message Job {

74
src/model/job.go Normal file
View file

@ -0,0 +1,74 @@
package model
// Job model
//
// Job stellt einen Bewässerungszeitplan dar.
//
//swagger:model Job
type Job struct {
// ID des Jobs
// required: true
// example: 1
Id int32 `json:"id"`
// Zritstempel des Bewässerungsjobs
// required: true
// example: 1643756107
Date int64 `json:"date"`
// Bewässerungsdauer in Sekunden
// required: true
// example: 300
Duration int32 `json:"duration"`
// Zu bewässernde Zonen
// required: true
// example: [2, 3]
Zones []int32 `json:"zones"`
// Job aktiviert?
// required: true
Enable bool `json:"enable"`
// Job täglich wiederholen
// required: true
Repeat bool `json:"repeat"`
}
// NewJob model
//
// NewJob stellt einen zu erstellenden Bewässerungszeitplan dar.
//
//swagger:model NewJob
type NewJob struct {
// Zritstempel des Bewässerungsjobs
// required: true
// example: 1643756107
Date int64 `json:"date"`
// Bewässerungsdauer in Sekunden
// required: true
// example: 300
Duration int32 `json:"duration"`
// Zu bewässernde Zonen
// required: true
// example: [2, 3]
Zones []int32 `json:"zones"`
// Job aktiviert?
// required: true
Enable bool `json:"enable"`
// Job täglich wiederholen
// required: true
Repeat bool `json:"repeat"`
}
//swagger:model JobList
type JobList []Job
//swagger:model JobID
type JobID struct {
Id int32 `json:"id" binding:"required"`
}

38
src/model/options.go Normal file
View file

@ -0,0 +1,38 @@
package model
// AutoMode model
//
// Zustand des Automatikmodus
//
//swagger:model AutoMode
type AutoMode struct {
// required: true
State bool `json:"state" binding:"required"`
}
// DefaultIrrigationTime model
//
// Manuelle Bewässerungszeit in Sekunden
//
//swagger:model DefaultIrrigationTime
type DefaultIrrigationTime struct {
// required: true
Time int32 `json:"time" binding:"required"`
}
// ConfigTime model
//
// Aktuelle Systemzeit/Zeitzone
//
//swagger:model ConfigTime
type ConfigTime struct {
// Aktuelle Systemzeit
//
// required: true
Time int64 `json:"time"`
// Aktuelle Zeitzone
//
// required: true
Timezone string `json:"timezone"`
}

65
src/model/task.go Normal file
View file

@ -0,0 +1,65 @@
package model
// Task model
//
// Task stellt eine Bewässerungsaufgabe dar.
//
//swagger:model Task
type Task struct {
// Quelle der Bewässerungsaufgabe (0: MANUAL, 1: SCHEDULE)
// required: true
// example: 0
Source int32 `json:"source"`
// Nummer der Bewässerungszone
// required: true
// example: 2
ZoneId int32 `json:"zone_id"`
// Bewässerungsdauer in Sekunden
// required: true
// example: 300
Duration int32 `json:"duration"`
// Zeitstempel, wann die Bewässerung gestartet wurde
// required: true
// nullable: true
// example: 1643756107
DatetimeStarted *int64 `json:"datetime_started"`
// Zeitstempel, wann die Bewässerung beendet sein wird
// required: true
// nullable: true
// example: 1643756407
DatetimeFinished *int64 `json:"datetime_finished"`
}
// TaskList model
//
// TaskList ist eine Liste der aktuell laufenden Bewässerungsaufgaben.
//
//swagger:model TaskList
type TaskList struct {
// Aktueller Zeitstempel
// required: true
// example: 1643756107
Now int64 `json:"now"`
// Liste der laufenden Tasks
// required: true
Tasks []Task `json:"tasks"`
// Automatikmodus aktiv
// required: true
AutoMode bool `json:"auto_mode"`
}
// TaskRequestResult model
//
// TaskRequestResult wird beim Starten eines Tasks zurückgegeben
//
//swagger:model TaskRequestResult
type TaskRequestResult struct {
Started bool
Stopped bool
}

View file

@ -24,14 +24,17 @@ import (
"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 {
@ -81,12 +84,26 @@ func (srv *WebUIServer) getRouter() *gin.Engine {
api.GET("/panic", srv.controllerPanic)
}
api.POST("/task/manual", srv.controllerStartTask)
api.GET("/jobs", srv.controllerGetJobs)
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.controllerGetConfigTime)
api.POST("config/time", srv.controllerSetConfigTime)
// UI
uiGroup := router.Group("/", middleware.Compression(
srv.config.Server.Compression.Gzip,
srv.config.Server.Compression.Brotli),
)
// ui.Register(uiGroup)
ui.Register(uiGroup, srv.nZones)
swagger.Register(uiGroup)
return router
@ -98,12 +115,496 @@ func (srv *WebUIServer) Run() error {
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()
return router.Run(fmt.Sprintf("%s:%d",
err = router.Run(fmt.Sprintf("%s:%d",
srv.config.Server.Address, srv.config.Server.Port))
_ = srv.rpc.Stop()
return err
}
// swagger:operation POST /task/manual startManualTask
//
// Starte/stoppe manuell eine neue Bewässerungsaufgabe
//
// ---
// consumes: [application/json]
// produces: [application/json]
// parameters:
// - in: body
// name: taskRequest
// schema:
// type: object
// properties:
// zone_id:
// description: Nummer der Bewässerungszone
// type: integer
// duration:
// description: "Bewässerungsdauer in Sekunden (0: Standarddauer)"
// type: integer
// queuing:
// description: |
// Aufgabe in die Warteschlange einreihen,
// wenn sie nicht sofort ausgeführt werden kann.
// type: boolean
// cancelling:
// description: |
// Aufgabe stoppen/aus der Warteschlange entfernen,
// wenn sie bereits läuft oder sich in der Warteschlange befindet.
// type: boolean
// required:
// - zone_id
// - duration
// - queuing
// - cancelling
// responses:
// 200:
// description: Bewässerungsaufgabe erfolgreich gestarted/gestoppt
// schema:
// $ref: "#/definitions/StatusMessage"
// 400:
// description: Bewässerungsaufgabe läuft schon und kann nicht gestoppt werden.
// schema:
// $ref: "#/definitions/Error"
// 500:
// description: Serverfehler
// schema:
// $ref: "#/definitions/Error"
func (srv *WebUIServer) controllerStartTask(c *gin.Context) {
var params struct {
ZoneID int32 `json:"zone_id"`
Duration int32 `json:"duration"`
Queuing bool `json:"queuing"`
Cancelling bool `json:"cancelling"`
}
if err := c.ShouldBindJSON(&params); err != nil {
log.Error().Err(err).Msg("StartTask input error")
writeStatus(c, http.StatusBadRequest, "invalid input data")
return
}
if !srv.validateZoneID(params.ZoneID) {
writeStatus(c, http.StatusBadRequest, "invalid zone_id")
return
}
res, err := srv.rpc.StartManualTask(
params.ZoneID, params.Duration, params.Queuing, params.Cancelling)
if err != nil {
log.Error().Err(err).Msg("StartManualTask")
writeStatus(c, http.StatusInternalServerError, "error starting task")
return
}
statusCode := 200
if !res.Started && !res.Stopped {
statusCode = 400
}
c.JSON(statusCode, res)
}
// 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) {
var params struct {
Id int32 `uri:"id" binding:"required"`
}
if err := c.ShouldBindUri(&params); err != nil {
log.Error().Err(err).Msg("GetJob input error")
writeStatus(c, http.StatusBadRequest, "invalid input data")
return
}
job, err := srv.rpc.GetJob(params.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: id
// in: body
// schema:
// type: integer
// 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:
// type: object
// properties:
// state:
// type: boolean
// required:
// - state
// 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 getConfigTime
//
// Rufe die aktuelle Systemzeit/Zeitzone ab
//
// ---
// produces: [application/json]
// responses:
// 200:
// description: Aktuelle Systemzeit/Zeitzone
// schema:
// $ref: "#/definitions/ConfigTime"
// 500:
// description: Serverfehler
// schema:
// $ref: "#/definitions/Error"
func (srv *WebUIServer) controllerGetConfigTime(c *gin.Context) {
configTime, err := srv.rpc.GetConfigTime()
if err != nil {
log.Error().Err(err).Msg("GetConfigTime")
writeStatus(c, http.StatusInternalServerError, "error setting configTime")
return
}
c.JSON(200, configTime)
}
// swagger:operation POST /config/time setConfigTime
//
// Automatikmodus aktivieren/deaktivieren
//
// ---
// consumes: [application/json]
// produces: [application/json]
// parameters:
// - in: body
// name: configTime
// schema:
// $ref: "#/definitions/ConfigTime"
// responses:
// 200:
// description: OK
// schema:
// $ref: "#/definitions/StatusMessage"
// 500:
// description: Serverfehler
// schema:
// $ref: "#/definitions/Error"
func (srv *WebUIServer) controllerSetConfigTime(c *gin.Context) {
var configTime model.ConfigTime
if err := c.ShouldBindJSON(&configTime); err != nil {
log.Error().Err(err).Msg("SetConfigTime input error")
writeStatus(c, http.StatusBadRequest, "invalid input data")
return
}
err := srv.rpc.SetConfigTime(configTime)
if err != nil {
log.Error().Err(err).Msg("SetConfigTime")
writeStatus(c, http.StatusInternalServerError, "error setting configTime")
return
}
writeStatus(c, http.StatusOK, "set configTime")
}
// 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, 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
@ -116,9 +617,13 @@ func (srv *WebUIServer) controllerPanic(c *gin.Context) {
panic(errors.New("panic message"))
}
func writeStatus(c *gin.Context, success bool, msg string) {
c.JSON(http.StatusOK, model.StatusMessage{
Success: success,
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,
})
}

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>SEBRAUC API documentation</title>
<title>TSGRain WebUI API documentation</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

View file

@ -1,4 +1,38 @@
definitions:
AutoMode:
description: Zustand des Automatikmodus
properties:
state:
type: boolean
required:
- state
title: AutoMode model
type: object
ConfigTime:
description: Aktuelle Systemzeit/Zeitzone
properties:
time:
description: Aktuelle Systemzeit
format: int64
type: integer
timezone:
description: Aktuelle Zeitzone
type: string
required:
- time
- timezone
title: ConfigTime model
type: object
DefaultIrrigationTime:
description: Manuelle Bewässerungszeit in Sekunden
properties:
time:
format: int32
type: integer
required:
- time
title: DefaultIrrigationTime model
type: object
Error:
description: The Error contains error relevant information.
properties:
@ -21,6 +55,94 @@ definitions:
- msg
title: Error model
type: object
Job:
description: Job stellt einen Bewässerungszeitplan dar.
properties:
date:
description: Zritstempel des Bewässerungsjobs
example: 1643756107
format: int64
type: integer
duration:
description: Bewässerungsdauer in Sekunden
example: 300
format: int32
type: integer
enable:
description: Job aktiviert?
type: boolean
id:
description: ID des Jobs
example: 1
format: int32
type: integer
repeat:
description: Job täglich wiederholen
type: boolean
zones:
description: Zu bewässernde Zonen
example:
- 2
- 3
items:
format: int32
type: integer
type: array
required:
- id
- date
- duration
- zones
- enable
- repeat
title: Job model
type: object
JobID:
properties:
id:
format: int32
type: integer
type: object
JobList:
items:
$ref: "#/definitions/Job"
type: array
NewJob:
description: NewJob stellt einen zu erstellenden Bewässerungszeitplan dar.
properties:
date:
description: Zritstempel des Bewässerungsjobs
example: 1643756107
format: int64
type: integer
duration:
description: Bewässerungsdauer in Sekunden
example: 300
format: int32
type: integer
enable:
description: Job aktiviert?
type: boolean
repeat:
description: Job täglich wiederholen
type: boolean
zones:
description: Zu bewässernde Zonen
example:
- 2
- 3
items:
format: int32
type: integer
type: array
required:
- date
- duration
- zones
- enable
- repeat
title: NewJob model
type: object
StatusMessage:
description: StatusMessage contains the status of an operation.
properties:
@ -36,13 +158,345 @@ definitions:
- msg
title: StatusMessage model
type: object
Task:
description: Task stellt eine Bewässerungsaufgabe dar.
properties:
datetime_finished:
description: Zeitstempel, wann die Bewässerung beendet sein wird
example: 1643756407
format: int64
type: integer
datetime_started:
description: Zeitstempel, wann die Bewässerung gestartet wurde
example: 1643756107
format: int64
type: integer
duration:
description: Bewässerungsdauer in Sekunden
example: 300
format: int32
type: integer
source:
description: "Quelle der Bewässerungsaufgabe (0: MANUAL, 1: SCHEDULE)"
example: 0
format: int32
type: integer
zone_id:
description: Nummer der Bewässerungszone
example: 2
format: int32
type: integer
required:
- source
- zone_id
- duration
- datetime_started
- datetime_finished
title: Task model
type: object
TaskList:
description: TaskList ist eine Liste der aktuell laufenden Bewässerungsaufgaben.
properties:
auto_mode:
description: Automatikmodus aktiv
type: boolean
now:
description: Aktueller Zeitstempel
example: 1643756107
format: int64
type: integer
tasks:
description: Liste der laufenden Tasks
items:
$ref: "#/definitions/Task"
type: array
required:
- now
- tasks
- auto_mode
title: TaskList model
type: object
TaskRequestResult:
description: TaskRequestResult wird beim Starten eines Tasks zurückgegeben
properties:
Started:
type: boolean
Stopped:
type: boolean
title: TaskRequestResult model
type: object
info:
description: REST API for the TSGRain WebUI
license:
name: MIT
title: TSGRain WebUI
version: 0.1.0
paths: {}
paths:
/config/auto:
get:
operationId: getAutoMode
produces:
- application/json
responses:
"200":
description: Status des Automatikmodus
schema:
$ref: "#/definitions/AutoMode"
"500":
description: Serverfehler
schema:
$ref: "#/definitions/Error"
summary: Rufe ab, ob der Automatikmodus aktiviert ist.
post:
consumes:
- application/json
description: Automatikmodus aktivieren/deaktivieren
operationId: setAutoMode
parameters:
- description: Zustand des Automatikmodus
in: body
name: state
schema:
properties:
state:
type: boolean
required:
- state
type: object
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: "#/definitions/StatusMessage"
"500":
description: Serverfehler
schema:
$ref: "#/definitions/Error"
/config/defaultIrrigationTime:
get:
operationId: getDefaultIrrigationTime
produces:
- application/json
responses:
"200":
description: Manuelle Bewässerungszeit in Sekunden
schema:
$ref: "#/definitions/DefaultIrrigationTime"
"500":
description: Serverfehler
schema:
$ref: "#/definitions/Error"
summary: Rufe die Standardzeit bei manueller Bewässerung ab.
post:
consumes:
- application/json
operationId: setDefaultIrrigationTime
parameters:
- in: body
name: defaultIrrigationTime
schema:
$ref: "#/definitions/DefaultIrrigationTime"
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: "#/definitions/StatusMessage"
"500":
description: Serverfehler
schema:
$ref: "#/definitions/Error"
summary: Setze die die Standardzeit bei manueller Bewässerung.
/config/time:
get:
description: Rufe die aktuelle Systemzeit/Zeitzone ab
operationId: getConfigTime
produces:
- application/json
responses:
"200":
description: Aktuelle Systemzeit/Zeitzone
schema:
$ref: "#/definitions/ConfigTime"
"500":
description: Serverfehler
schema:
$ref: "#/definitions/Error"
post:
consumes:
- application/json
description: Automatikmodus aktivieren/deaktivieren
operationId: setConfigTime
parameters:
- in: body
name: configTime
schema:
$ref: "#/definitions/ConfigTime"
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: "#/definitions/StatusMessage"
"500":
description: Serverfehler
schema:
$ref: "#/definitions/Error"
/job:
delete:
consumes:
- application/json
operationId: deleteJob
parameters:
- in: body
name: id
schema:
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: "#/definitions/StatusMessage"
"500":
description: Serverfehler
schema:
$ref: "#/definitions/Error"
summary: Lösche einen gespeicherten Zeitplan.
get:
operationId: getJob
parameters:
- description: Job-ID
in: query
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: Job
schema:
$ref: "#/definitions/Job"
"500":
description: Serverfehler
schema:
$ref: "#/definitions/Error"
summary: Rufe einen gespeicherten Zeitplan ab.
post:
consumes:
- application/json
operationId: createJob
parameters:
- description: Neuer Job
in: body
name: job
required: true
schema:
$ref: "#/definitions/NewJob"
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: "#/definitions/JobID"
"500":
description: Serverfehler
schema:
$ref: "#/definitions/Error"
summary: Erstelle einen neuen Zeitplan.
put:
consumes:
- application/json
operationId: updateJob
parameters:
- description: Aktualisierter Job
in: body
name: job
required: true
schema:
$ref: "#/definitions/Job"
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: "#/definitions/StatusMessage"
"500":
description: Serverfehler
schema:
$ref: "#/definitions/Error"
summary: Aktualisiere einen gespeicherten Zeitplan.
/jobs:
get:
operationId: getJobs
produces:
- application/json
responses:
"200":
description: Job-Liste
schema:
$ref: "#/definitions/JobList"
"500":
description: Serverfehler
schema:
$ref: "#/definitions/Error"
summary: Rufe alle gespeicherten Zeitpläne ab.
/task/manual:
post:
consumes:
- application/json
description: Starte/stoppe manuell eine neue Bewässerungsaufgabe
operationId: startManualTask
parameters:
- in: body
name: taskRequest
schema:
properties:
cancelling:
description: |
Aufgabe stoppen/aus der Warteschlange entfernen,
wenn sie bereits läuft oder sich in der Warteschlange befindet.
type: boolean
duration:
description: "Bewässerungsdauer in Sekunden (0: Standarddauer)"
type: integer
queuing:
description: |
Aufgabe in die Warteschlange einreihen,
wenn sie nicht sofort ausgeführt werden kann.
type: boolean
zone_id:
description: Nummer der Bewässerungszone
type: integer
required:
- zone_id
- duration
- queuing
- cancelling
type: object
produces:
- application/json
responses:
"200":
description: Bewässerungsaufgabe erfolgreich gestarted/gestoppt
schema:
$ref: "#/definitions/StatusMessage"
"400":
description: Bewässerungsaufgabe läuft schon und kann nicht gestoppt werden.
schema:
$ref: "#/definitions/Error"
"500":
description: Serverfehler
schema:
$ref: "#/definitions/Error"
schemes:
- http
- https

View file

@ -335,8 +335,9 @@ type TaskList struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Tasks []*Task `protobuf:"bytes,1,rep,name=tasks,proto3" json:"tasks,omitempty"`
Now *Timestamp `protobuf:"bytes,2,opt,name=now,proto3" json:"now,omitempty"`
Tasks []*Task `protobuf:"bytes,1,rep,name=tasks,proto3" json:"tasks,omitempty"`
Now *Timestamp `protobuf:"bytes,2,opt,name=now,proto3" json:"now,omitempty"`
AutoMode bool `protobuf:"varint,3,opt,name=auto_mode,json=autoMode,proto3" json:"auto_mode,omitempty"`
}
func (x *TaskList) Reset() {
@ -385,6 +386,13 @@ func (x *TaskList) GetNow() *Timestamp {
return nil
}
func (x *TaskList) GetAutoMode() bool {
if x != nil {
return x.AutoMode
}
return false
}
type Job struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -659,89 +667,91 @@ var file_proto_tsgrain_proto_rawDesc = []byte{
0x12, 0x37, 0x0a, 0x11, 0x64, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x66, 0x69, 0x6e,
0x69, 0x73, 0x68, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x54, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x64, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d,
0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x22, 0x45, 0x0a, 0x08, 0x54, 0x61, 0x73,
0x65, 0x46, 0x69, 0x6e, 0x69, 0x73, 0x68, 0x65, 0x64, 0x22, 0x62, 0x0a, 0x08, 0x54, 0x61, 0x73,
0x6b, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x05, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x01,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x05, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x05, 0x74, 0x61, 0x73,
0x6b, 0x73, 0x12, 0x1c, 0x0a, 0x03, 0x6e, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x0a, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x03, 0x6e, 0x6f, 0x77,
0x22, 0x97, 0x01, 0x0a, 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
0x6d, 0x70, 0x52, 0x04, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x04, 0x20,
0x03, 0x28, 0x05, 0x52, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e,
0x61, 0x62, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x6e, 0x61, 0x62,
0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01,
0x28, 0x08, 0x52, 0x06, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x22, 0x17, 0x0a, 0x05, 0x4a, 0x6f,
0x62, 0x49, 0x44, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52,
0x02, 0x69, 0x64, 0x22, 0x23, 0x0a, 0x07, 0x4a, 0x6f, 0x62, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x18,
0x0a, 0x04, 0x6a, 0x6f, 0x62, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x04, 0x2e, 0x4a,
0x6f, 0x62, 0x52, 0x04, 0x6a, 0x6f, 0x62, 0x73, 0x22, 0x50, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x66,
0x69, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x65, 0x74, 0x69,
0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x64, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1a,
0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x2a, 0x26, 0x0a, 0x0a, 0x54, 0x61,
0x73, 0x6b, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x41, 0x4e, 0x55,
0x41, 0x4c, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, 0x4c, 0x45,
0x10, 0x01, 0x32, 0xc7, 0x06, 0x0a, 0x07, 0x54, 0x53, 0x47, 0x52, 0x61, 0x69, 0x6e, 0x12, 0x2f,
0x0a, 0x09, 0x53, 0x74, 0x61, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x0c, 0x2e, 0x54, 0x61,
0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x54, 0x61, 0x73, 0x6b,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x12,
0x2f, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x12, 0x1b, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x6f, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x75, 0x74, 0x6f, 0x4d, 0x6f, 0x64, 0x65, 0x22, 0x97, 0x01,
0x0a, 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
0x04, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x12, 0x14, 0x0a, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x05,
0x52, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x61, 0x62, 0x6c,
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12,
0x16, 0x0a, 0x06, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52,
0x06, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x22, 0x17, 0x0a, 0x05, 0x4a, 0x6f, 0x62, 0x49, 0x44,
0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64,
0x22, 0x23, 0x0a, 0x07, 0x4a, 0x6f, 0x62, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x04, 0x6a,
0x6f, 0x62, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x52,
0x04, 0x6a, 0x6f, 0x62, 0x73, 0x22, 0x50, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54,
0x69, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x18,
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x70, 0x52, 0x08, 0x64, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74,
0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74,
0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x2a, 0x26, 0x0a, 0x0a, 0x54, 0x61, 0x73, 0x6b, 0x53,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x41, 0x4e, 0x55, 0x41, 0x4c, 0x10,
0x00, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, 0x4c, 0x45, 0x10, 0x01, 0x32,
0xc7, 0x06, 0x0a, 0x07, 0x54, 0x53, 0x47, 0x52, 0x61, 0x69, 0x6e, 0x12, 0x2f, 0x0a, 0x09, 0x53,
0x74, 0x61, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x0c, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x12, 0x2f, 0x0a, 0x08,
0x47, 0x65, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x1a, 0x09, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x12, 0x34, 0x0a,
0x0b, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x16, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x09, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x69, 0x73, 0x74, 0x22,
0x00, 0x30, 0x01, 0x12, 0x1b, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62,
0x12, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x1a, 0x06, 0x2e, 0x4a, 0x6f, 0x62, 0x49, 0x44, 0x22, 0x00,
0x12, 0x18, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x12, 0x06, 0x2e, 0x4a, 0x6f, 0x62,
0x49, 0x44, 0x1a, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x07, 0x47, 0x65,
0x74, 0x4a, 0x6f, 0x62, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x08, 0x2e,
0x4a, 0x6f, 0x62, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x09, 0x55, 0x70, 0x64,
0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x1a, 0x16, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x09, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65,
0x4a, 0x6f, 0x62, 0x12, 0x06, 0x2e, 0x4a, 0x6f, 0x62, 0x49, 0x44, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x1a, 0x09, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00,
0x12, 0x34, 0x0a, 0x0b, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12,
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x09, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x4c, 0x69,
0x73, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x1b, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x4a, 0x6f, 0x62, 0x12, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x1a, 0x06, 0x2e, 0x4a, 0x6f, 0x62, 0x49,
0x44, 0x22, 0x00, 0x12, 0x18, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x12, 0x06, 0x2e,
0x4a, 0x6f, 0x62, 0x49, 0x44, 0x1a, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x22, 0x00, 0x12, 0x2d, 0x0a,
0x07, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x1a, 0x08, 0x2e, 0x4a, 0x6f, 0x62, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x09,
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x04, 0x2e, 0x4a, 0x6f, 0x62, 0x1a,
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x09, 0x44, 0x65, 0x6c,
0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x06, 0x2e, 0x4a, 0x6f, 0x62, 0x49, 0x44, 0x1a, 0x16,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x41,
0x75, 0x74, 0x6f, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a,
0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x43, 0x0a,
0x0b, 0x53, 0x65, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x2e, 0x67,
0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x6f,
0x4d, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42,
0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x22, 0x00, 0x12, 0x36, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54,
0x69, 0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x43, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x00, 0x12, 0x36, 0x0a, 0x0d, 0x53, 0x65,
0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x0b, 0x2e, 0x43, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x22, 0x00, 0x12, 0x51, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74,
0x49, 0x72, 0x72, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x16,
0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0b, 0x53, 0x65,
0x74, 0x41, 0x75, 0x74, 0x6f, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c,
0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12,
0x36, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, 0x69, 0x6d, 0x65,
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x00, 0x12, 0x36, 0x0a, 0x0d, 0x53, 0x65, 0x74, 0x43, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x0b, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12,
0x51, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x49, 0x72, 0x72,
0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65,
0x22, 0x00, 0x12, 0x51, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74,
0x49, 0x72, 0x72, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61,
0x6c, 0x75, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61,
0x75, 0x6c, 0x74, 0x49, 0x72, 0x72, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d,
0x65, 0x12, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x4e,
0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x42, 0x30, 0x5a, 0x2e,
0x63, 0x6f, 0x64, 0x65, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x61, 0x64, 0x65, 0x76, 0x2e, 0x64, 0x65,
0x2f, 0x54, 0x53, 0x47, 0x52, 0x61, 0x69, 0x6e, 0x2f, 0x57, 0x65, 0x62, 0x55, 0x49, 0x2f, 0x73,
0x72, 0x63, 0x2f, 0x74, 0x73, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x5f, 0x72, 0x70, 0x63, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x4e, 0x5a, 0x6f, 0x6e,
0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74,
0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x42, 0x30, 0x5a, 0x2e, 0x63, 0x6f, 0x64,
0x65, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x61, 0x64, 0x65, 0x76, 0x2e, 0x64, 0x65, 0x2f, 0x54, 0x53,
0x47, 0x52, 0x61, 0x69, 0x6e, 0x2f, 0x57, 0x65, 0x62, 0x55, 0x49, 0x2f, 0x73, 0x72, 0x63, 0x2f,
0x74, 0x73, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x5f, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
}
var (

View file

@ -7,12 +7,14 @@ import (
"io"
"time"
"code.thetadev.de/TSGRain/WebUI/src/model"
tsgrain_grpc "code.thetadev.de/TSGRain/WebUI/src/tsgrain_rpc/proto"
"code.thetadev.de/TSGRain/WebUI/src/util"
"github.com/rs/zerolog/log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
type RPCClient struct {
@ -50,6 +52,78 @@ func (c *RPCClient) Stop() error {
return c.conn.Close()
}
func mapTask(pbTask *tsgrain_grpc.Task) model.Task {
task := model.Task{
Source: int32(pbTask.Source),
ZoneId: pbTask.ZoneId,
Duration: pbTask.Duration,
}
if pbTask.DatetimeStarted != nil {
task.DatetimeStarted = &pbTask.DatetimeStarted.Seconds
}
if pbTask.DatetimeFinished != nil {
task.DatetimeFinished = &pbTask.DatetimeFinished.Seconds
}
return task
}
func mapTaskList(pbTaskList *tsgrain_grpc.TaskList) model.TaskList {
taskList := model.TaskList{
Now: pbTaskList.Now.Seconds,
Tasks: []model.Task{},
AutoMode: pbTaskList.AutoMode,
}
for _, pbTask := range pbTaskList.Tasks {
if pbTask != nil {
taskList.Tasks = append(taskList.Tasks, mapTask(pbTask))
}
}
return taskList
}
func (c *RPCClient) StartManualTask(
zone_id int32, duration int32, queuing bool, cancelling bool) (
model.TaskRequestResult, error) {
res, err := c.tsgrain.StartTask(c.ctx, &tsgrain_grpc.TaskRequest{
Source: tsgrain_grpc.TaskSource_MANUAL,
ZoneId: zone_id,
Duration: duration,
Queuing: queuing,
Cancelling: cancelling,
})
if err != nil {
return model.TaskRequestResult{}, err
}
return model.TaskRequestResult{
Started: res.Started,
Stopped: res.Stopped,
}, nil
}
func (c *RPCClient) GetTasks() (model.TaskList, error) {
pbTaskList, err := c.tsgrain.GetTasks(c.ctx, &emptypb.Empty{})
if err != nil {
return model.TaskList{}, err
}
return mapTaskList(pbTaskList), nil
}
func broadcastTaskList(bc util.Broadcaster, pbTaskList *tsgrain_grpc.TaskList) error {
taskList := mapTaskList(pbTaskList)
taskListJson, err := json.Marshal(taskList)
if err != nil {
return err
}
bc.Broadcast(taskListJson)
log.Debug().RawJSON("task_list", taskListJson).Msg("TaskList received")
return nil
}
func (c *RPCClient) streamTasks(bc util.Broadcaster) error {
stream, err := c.tsgrain.StreamTasks(c.ctx, &emptypb.Empty{})
if err != nil {
@ -57,25 +131,25 @@ func (c *RPCClient) streamTasks(bc util.Broadcaster) error {
}
for {
taskList, err := stream.Recv()
pbTaskList, err := stream.Recv()
if errors.Is(err, io.EOF) {
return nil
}
if err != nil {
return err
} else {
taskListJson, err := json.Marshal(taskList)
if err != nil {
return err
}
bc.Broadcast(taskListJson)
log.Debug().RawJSON("task_list", taskListJson).Msg("TaskList received")
_ = broadcastTaskList(bc, pbTaskList)
}
}
}
func (c *RPCClient) StreamTasks(bc util.Broadcaster) {
// Get initial state
pbTaskList, err := c.tsgrain.GetTasks(c.ctx, &emptypb.Empty{})
if err == nil {
_ = broadcastTaskList(bc, pbTaskList)
}
// Keep stream running if it errors
for {
select {
@ -90,3 +164,142 @@ func (c *RPCClient) StreamTasks(bc util.Broadcaster) {
}
}
}
func (c *RPCClient) CreateJob(job model.NewJob) (int32, error) {
pbJob := &tsgrain_grpc.Job{
Date: &tsgrain_grpc.Timestamp{Seconds: job.Date},
Duration: job.Duration,
Zones: job.Zones,
Enable: job.Enable,
Repeat: job.Repeat,
}
res, err := c.tsgrain.CreateJob(c.ctx, pbJob)
if err != nil {
return 0, err
}
return res.Id, nil
}
func (c *RPCClient) GetJobs() (model.JobList, error) {
jobs := []model.Job{}
res, err := c.tsgrain.GetJobs(c.ctx, &emptypb.Empty{})
if err != nil {
return jobs, err
}
for _, pbJob := range res.Jobs {
job := model.Job{
Id: pbJob.Id,
Date: pbJob.Date.Seconds,
Duration: pbJob.Duration,
Zones: pbJob.Zones,
Enable: pbJob.Enable,
Repeat: pbJob.Repeat,
}
jobs = append(jobs, job)
}
return jobs, nil
}
func (c *RPCClient) GetJob(id int32) (model.Job, error) {
res, err := c.tsgrain.GetJob(c.ctx, &tsgrain_grpc.JobID{Id: id})
if err != nil {
return model.Job{}, err
}
job := model.Job{
Id: res.Id,
Date: res.Date.Seconds,
Duration: res.Duration,
Zones: res.Zones,
Enable: res.Enable,
Repeat: res.Repeat,
}
return job, nil
}
func (c *RPCClient) UpdateJob(job model.Job) error {
pbJob := tsgrain_grpc.Job{
Id: job.Id,
Date: &tsgrain_grpc.Timestamp{Seconds: job.Date},
Duration: job.Duration,
Zones: job.Zones,
Enable: job.Enable,
Repeat: job.Repeat,
}
_, err := c.tsgrain.UpdateJob(c.ctx, &pbJob)
return err
}
func (c *RPCClient) DeleteJob(job_id int32) error {
_, err := c.tsgrain.DeleteJob(c.ctx, &tsgrain_grpc.JobID{Id: job_id})
return err
}
func (c *RPCClient) GetAutoMode() (bool, error) {
res, err := c.tsgrain.GetAutoMode(c.ctx, &emptypb.Empty{})
if err != nil {
return false, err
}
return res.Value, nil
}
func (c *RPCClient) SetAutoMode(state bool) error {
_, err := c.tsgrain.SetAutoMode(c.ctx, wrapperspb.Bool(state))
return err
}
func (c *RPCClient) GetConfigTime() (model.ConfigTime, error) {
configTime, err := c.tsgrain.GetConfigTime(c.ctx, &emptypb.Empty{})
if err != nil {
return model.ConfigTime{}, err
}
return model.ConfigTime{
Time: configTime.Datetime.Seconds,
Timezone: configTime.Timezone,
}, nil
}
func (c *RPCClient) SetConfigTime(configTime model.ConfigTime) error {
_, err := c.tsgrain.SetConfigTime(c.ctx, &tsgrain_grpc.ConfigTime{
Datetime: &tsgrain_grpc.Timestamp{Seconds: configTime.Time},
Timezone: configTime.Timezone,
})
return err
}
func (c *RPCClient) SetSystemTimezone(timezone string) error {
_, err := c.tsgrain.SetConfigTime(c.ctx, &tsgrain_grpc.ConfigTime{
Timezone: timezone,
})
return err
}
func (c *RPCClient) GetDefaultIrrigationTime() (int32, error) {
res, err := c.tsgrain.GetDefaultIrrigationTime(c.ctx, &emptypb.Empty{})
if err != nil {
return 0, err
}
return res.Value, nil
}
func (c *RPCClient) SetDefaultIrrigationTime(defaultTime int32) error {
_, err := c.tsgrain.SetDefaultIrrigationTime(c.ctx, wrapperspb.Int32(defaultTime))
return err
}
func (c *RPCClient) GetNZones() (int32, error) {
val, err := c.tsgrain.GetNZones(c.ctx, &emptypb.Empty{})
if err != nil {
return 0, err
}
return val.Value, nil
}

1
ui/.env.development Normal file
View file

@ -0,0 +1 @@
VITE_API_HOST=127.0.0.1:8001

5
ui/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

4
ui/.prettierignore Normal file
View file

@ -0,0 +1,4 @@
node_modules
dist
tmp
.tmp

17
ui/index.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TSGRain</title>
</head>
<body>
<noscript>You have to enable JavaScript to use TSGRain.</noscript>
<div id="app"></div>
<script>
window.config = "%CONFIG%"
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

24
ui/package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "ui",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"serve": "vite preview",
"lint": "tsc",
"format": "prettier --write ../"
},
"dependencies": {
"@mdi/js": "^6.5.95",
"axios": "^0.24.0",
"preact": "^10.5.15"
},
"devDependencies": {
"@preact/preset-vite": "^2.1.5",
"prettier": "^2.4.1",
"sass": "^1.43.4",
"typescript": "^4.5.2",
"vite": "^2.6.14",
"@babel/core": ">=7.12.10 <8.0.0"
}
}

1242
ui/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

10
ui/src/components/app.tsx Normal file
View file

@ -0,0 +1,10 @@
import {Component} from "preact"
// import UpdaterView from "./Updater/UpdaterView"
// import logo from "../assets/logo.svg"
import {getConfig} from "../util/config"
export default class App extends Component {
render() {
return <div>{getConfig().version}</div>
}
}

5
ui/src/main.tsx Normal file
View file

@ -0,0 +1,5 @@
import {render} from "preact"
import App from "./components/app"
import "./style/index.scss"
render(<App />, document.getElementById("app")!)

1
ui/src/preact.d.ts vendored Normal file
View file

@ -0,0 +1 @@
import JSX = preact.JSX

0
ui/src/style/index.scss Normal file
View file

4
ui/src/tsgrain-client/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
wwwroot/*.js
node_modules
typings
dist

View file

@ -0,0 +1 @@
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm

View file

@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View file

@ -0,0 +1,9 @@
.gitignore
.npmignore
.openapi-generator-ignore
api.ts
base.ts
common.ts
configuration.ts
git_push.sh
index.ts

View file

@ -0,0 +1 @@
5.3.1

1455
ui/src/tsgrain-client/api.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,74 @@
/* tslint:disable */
/* eslint-disable */
/**
* TSGRain WebUI
* REST API for the TSGRain WebUI
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import {Configuration} from "./configuration"
// Some imports not used depending on template conditions
// @ts-ignore
import globalAxios, {AxiosPromise, AxiosInstance, AxiosRequestConfig} from "axios"
export const BASE_PATH = "http://localhost".replace(/\/+$/, "")
/**
*
* @export
*/
export const COLLECTION_FORMATS = {
csv: ",",
ssv: " ",
tsv: "\t",
pipes: "|",
}
/**
*
* @export
* @interface RequestArgs
*/
export interface RequestArgs {
url: string
options: AxiosRequestConfig
}
/**
*
* @export
* @class BaseAPI
*/
export class BaseAPI {
protected configuration: Configuration | undefined
constructor(
configuration?: Configuration,
protected basePath: string = BASE_PATH,
protected axios: AxiosInstance = globalAxios
) {
if (configuration) {
this.configuration = configuration
this.basePath = configuration.basePath || this.basePath
}
}
}
/**
*
* @export
* @class RequiredError
* @extends {Error}
*/
export class RequiredError extends Error {
name: "RequiredError" = "RequiredError"
constructor(public field: string, msg?: string) {
super(msg)
}
}

View file

@ -0,0 +1,181 @@
/* tslint:disable */
/* eslint-disable */
/**
* TSGRain WebUI
* REST API for the TSGRain WebUI
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import {Configuration} from "./configuration"
import {RequiredError, RequestArgs} from "./base"
import {AxiosInstance, AxiosResponse} from "axios"
/**
*
* @export
*/
export const DUMMY_BASE_URL = "https://example.com"
/**
*
* @throws {RequiredError}
* @export
*/
export const assertParamExists = function (
functionName: string,
paramName: string,
paramValue: unknown
) {
if (paramValue === null || paramValue === undefined) {
throw new RequiredError(
paramName,
`Required parameter ${paramName} was null or undefined when calling ${functionName}.`
)
}
}
/**
*
* @export
*/
export const setApiKeyToObject = async function (
object: any,
keyParamName: string,
configuration?: Configuration
) {
if (configuration && configuration.apiKey) {
const localVarApiKeyValue =
typeof configuration.apiKey === "function"
? await configuration.apiKey(keyParamName)
: await configuration.apiKey
object[keyParamName] = localVarApiKeyValue
}
}
/**
*
* @export
*/
export const setBasicAuthToObject = function (
object: any,
configuration?: Configuration
) {
if (configuration && (configuration.username || configuration.password)) {
object["auth"] = {
username: configuration.username,
password: configuration.password,
}
}
}
/**
*
* @export
*/
export const setBearerAuthToObject = async function (
object: any,
configuration?: Configuration
) {
if (configuration && configuration.accessToken) {
const accessToken =
typeof configuration.accessToken === "function"
? await configuration.accessToken()
: await configuration.accessToken
object["Authorization"] = "Bearer " + accessToken
}
}
/**
*
* @export
*/
export const setOAuthToObject = async function (
object: any,
name: string,
scopes: string[],
configuration?: Configuration
) {
if (configuration && configuration.accessToken) {
const localVarAccessTokenValue =
typeof configuration.accessToken === "function"
? await configuration.accessToken(name, scopes)
: await configuration.accessToken
object["Authorization"] = "Bearer " + localVarAccessTokenValue
}
}
/**
*
* @export
*/
export const setSearchParams = function (url: URL, ...objects: any[]) {
const searchParams = new URLSearchParams(url.search)
for (const object of objects) {
for (const key in object) {
if (Array.isArray(object[key])) {
searchParams.delete(key)
for (const item of object[key]) {
searchParams.append(key, item)
}
} else {
searchParams.set(key, object[key])
}
}
}
url.search = searchParams.toString()
}
/**
*
* @export
*/
export const serializeDataIfNeeded = function (
value: any,
requestOptions: any,
configuration?: Configuration
) {
const nonString = typeof value !== "string"
const needsSerialization =
nonString && configuration && configuration.isJsonMime
? configuration.isJsonMime(requestOptions.headers["Content-Type"])
: nonString
return needsSerialization
? JSON.stringify(value !== undefined ? value : {})
: value || ""
}
/**
*
* @export
*/
export const toPathString = function (url: URL) {
return url.pathname + url.search + url.hash
}
/**
*
* @export
*/
export const createRequestFunction = function (
axiosArgs: RequestArgs,
globalAxios: AxiosInstance,
BASE_PATH: string,
configuration?: Configuration
) {
return <T = unknown, R = AxiosResponse<T>>(
axios: AxiosInstance = globalAxios,
basePath: string = BASE_PATH
) => {
const axiosRequestArgs = {
...axiosArgs.options,
url: (configuration?.basePath || basePath) + axiosArgs.url,
}
return axios.request<T, R>(axiosRequestArgs)
}
}

View file

@ -0,0 +1,123 @@
/* tslint:disable */
/* eslint-disable */
/**
* TSGRain WebUI
* REST API for the TSGRain WebUI
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface ConfigurationParameters {
apiKey?:
| string
| Promise<string>
| ((name: string) => string)
| ((name: string) => Promise<string>)
username?: string
password?: string
accessToken?:
| string
| Promise<string>
| ((name?: string, scopes?: string[]) => string)
| ((name?: string, scopes?: string[]) => Promise<string>)
basePath?: string
baseOptions?: any
formDataCtor?: new () => any
}
export class Configuration {
/**
* parameter for apiKey security
* @param name security name
* @memberof Configuration
*/
apiKey?:
| string
| Promise<string>
| ((name: string) => string)
| ((name: string) => Promise<string>)
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
username?: string
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
password?: string
/**
* parameter for oauth2 security
* @param name security name
* @param scopes oauth2 scope
* @memberof Configuration
*/
accessToken?:
| string
| Promise<string>
| ((name?: string, scopes?: string[]) => string)
| ((name?: string, scopes?: string[]) => Promise<string>)
/**
* override base path
*
* @type {string}
* @memberof Configuration
*/
basePath?: string
/**
* base options for axios calls
*
* @type {any}
* @memberof Configuration
*/
baseOptions?: any
/**
* The FormData constructor that will be used to create multipart form data
* requests. You can inject this here so that execution environments that
* do not support the FormData class can still run the generated client.
*
* @type {new () => FormData}
*/
formDataCtor?: new () => any
constructor(param: ConfigurationParameters = {}) {
this.apiKey = param.apiKey
this.username = param.username
this.password = param.password
this.accessToken = param.accessToken
this.basePath = param.basePath
this.baseOptions = param.baseOptions
this.formDataCtor = param.formDataCtor
}
/**
* Check if the given MIME is a JSON MIME.
* JSON MIME examples:
* application/json
* application/json; charset=UTF8
* APPLICATION/JSON
* application/vnd.company+json
* @param mime - MIME (Multipurpose Internet Mail Extensions)
* @return True if the given MIME is JSON, false otherwise.
*/
public isJsonMime(mime: string): boolean {
const jsonMime: RegExp = new RegExp(
"^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$",
"i"
)
return (
mime !== null &&
(jsonMime.test(mime) ||
mime.toLowerCase() === "application/json-patch+json")
)
}
}

View file

@ -0,0 +1,57 @@
#!/bin/sh
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
#
# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
git_user_id=$1
git_repo_id=$2
release_note=$3
git_host=$4
if [ "$git_host" = "" ]; then
git_host="github.com"
echo "[INFO] No command line input provided. Set \$git_host to $git_host"
fi
if [ "$git_user_id" = "" ]; then
git_user_id="GIT_USER_ID"
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
fi
if [ "$git_repo_id" = "" ]; then
git_repo_id="GIT_REPO_ID"
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
fi
if [ "$release_note" = "" ]; then
release_note="Minor update"
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
fi
# Initialize the local directory as a Git repository
git init
# Adds the files in the local repository and stages them for commit.
git add .
# Commits the tracked changes and prepares them to be pushed to a remote repository.
git commit -m "$release_note"
# Sets the new remote
git_remote=$(git remote)
if [ "$git_remote" = "" ]; then # git remote not defined
if [ "$GIT_TOKEN" = "" ]; then
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
else
git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
fi
fi
git pull origin master
# Pushes (Forces) the changes in the local repository up to the remote repository
echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
git push origin master 2>&1 | grep -v 'To https'

View file

@ -0,0 +1,16 @@
/* tslint:disable */
/* eslint-disable */
/**
* TSGRain WebUI
* REST API for the TSGRain WebUI
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export * from "./api"
export * from "./configuration"

20
ui/src/util/apiUrls.ts Normal file
View file

@ -0,0 +1,20 @@
import {Configuration, DefaultApi} from "../tsgrain-client"
let apiHost = document.location.host
const httpProto = document.location.protocol
const wsProto = httpProto === "https:" ? "wss:" : "ws:"
if (import.meta.env.VITE_API_HOST !== undefined) {
apiHost = import.meta.env.VITE_API_HOST as string
}
const apiUrl = `${httpProto}//${apiHost}/api`
const wsUrl = `${wsProto}//${apiHost}/api/ws`
let apicfg = new Configuration({
basePath: apiUrl,
})
const sebraucApi = new DefaultApi(apicfg)
export {apiUrl, wsUrl, sebraucApi}

27
ui/src/util/config.ts Normal file
View file

@ -0,0 +1,27 @@
export interface Config {
version: string
n_zones: number
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
declare global {
interface Window {
config?: any
}
}
function isConfig(object: any): object is Config {
return typeof object === "object" && "version" in object
}
export function getConfig(): Config {
if (isConfig(window.config)) {
return window.config
}
console.error("App config not found")
return {
version: "dev",
n_zones: 0,
}
}

18
ui/src/util/functions.ts Normal file
View file

@ -0,0 +1,18 @@
function secondsToString(seconds: number): string {
const numyears = Math.floor(seconds / 31536000)
const numdays = Math.floor((seconds % 31536000) / 86400)
const numhours = Math.floor(((seconds % 31536000) % 86400) / 3600)
const numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60)
const numseconds = (((seconds % 31536000) % 86400) % 3600) % 60
let res = []
if (numyears > 0) res.push(numyears + "yr")
if (numdays > 0) res.push(numdays + "d")
if (numhours > 0) res.push(numhours + "h")
if (numminutes > 0) res.push(numminutes + "m")
if (seconds < 60) res.push(numseconds + "s")
return res.join(" ")
}
export {secondsToString}

92
ui/src/util/websocket.ts Normal file
View file

@ -0,0 +1,92 @@
import {wsUrl} from "./apiUrls"
class WebsocketAPI {
private static ws: WebsocketAPI | undefined
private conn: WebSocket | undefined
private wsConnected: boolean
private clients: Set<WebsocketClient>
private constructor() {
this.clients = new Set()
this.wsConnected = false
if (window.WebSocket) {
this.connect()
} else {
console.log("Your browser does not support WebSockets")
}
}
private setStatus(wsConnected: boolean) {
if (wsConnected !== this.wsConnected) {
this.wsConnected = wsConnected
this.clients.forEach((client) => {
client.statusCallback(this.wsConnected)
})
}
}
private connect() {
this.conn = new WebSocket(wsUrl)
this.conn.onopen = () => {
this.setStatus(true)
console.log("WS connected")
}
this.conn.onclose = () => {
this.setStatus(false)
console.log("WS connection closed")
window.setTimeout(() => this.connect(), 3000)
}
this.conn.onmessage = (evt) => {
this.clients.forEach((client) => {
client.msgCallback(evt)
})
}
}
static Get(): WebsocketAPI {
if (this.ws === undefined) {
this.ws = new WebsocketAPI()
}
return this.ws
}
isConnected(): boolean {
return this.wsConnected
}
addClient(client: WebsocketClient) {
console.log("added client", client)
this.clients.add(client)
}
removeClient(client: WebsocketClient) {
console.log("removed client", client)
this.clients.delete(client)
}
}
export default class WebsocketClient {
statusCallback: (wsConnected: boolean) => void
msgCallback: (evt: MessageEvent) => void
constructor(
statusCallback: (wsConnected: boolean) => void,
msgCallback: (evt: MessageEvent) => void
) {
this.statusCallback = statusCallback
this.msgCallback = msgCallback
this.api().addClient(this)
}
api(): WebsocketAPI {
return WebsocketAPI.Get()
}
destroy() {
this.api().removeClient(this)
}
}

1
ui/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

21
ui/tsconfig.json Normal file
View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}

66
ui/ui.go Normal file
View file

@ -0,0 +1,66 @@
package ui
import (
"bytes"
"embed"
"encoding/json"
"io/fs"
"net/http"
"code.thetadev.de/TSGRain/WebUI/src/server/middleware"
"code.thetadev.de/TSGRain/WebUI/src/util"
"github.com/gin-gonic/gin"
)
const distDir = "dist"
//go:embed dist/**
var assets embed.FS
type uiConfig struct {
Version string `json:"version"`
NZones int32 `json:"n_zones"`
}
func subFS(fsys fs.FS, dir string) fs.FS {
sub, err := fs.Sub(fsys, dir)
if err != nil {
panic(err)
}
return sub
}
func distFS() fs.FS {
return subFS(assets, distDir)
}
func Register(r gin.IRouter, nZones int32) {
indexHandler := getIndexHandler(nZones)
uiAssets := r.Group("/assets", middleware.Cache)
r.GET("/", indexHandler)
r.GET("/index.html", indexHandler)
uiAssets.StaticFS("/", http.FS(subFS(distFS(), "assets")))
}
func getIndexHandler(nZones int32) gin.HandlerFunc {
content, err := fs.ReadFile(distFS(), "index.html")
if err != nil {
panic(err)
}
uiConfigBytes, err := json.Marshal(uiConfig{
Version: util.Version(),
NZones: nZones,
})
if err != nil {
panic(err)
}
content = bytes.ReplaceAll(content, []byte("\"%CONFIG%\""), uiConfigBytes)
return func(c *gin.Context) {
c.Data(200, "text/html", content)
}
}

87
ui/ui_test.go Normal file
View file

@ -0,0 +1,87 @@
package ui
import (
"net/http"
"net/http/httptest"
"os"
"path"
"regexp"
"testing"
"code.thetadev.de/TSGRain/WebUI/src/fixtures"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestUI(t *testing.T) {
tests := []struct {
name string
path string
contains string
cached bool
}{
{
name: "index_html",
path: "/",
contains: "TSGRain",
cached: false,
},
{
name: "index_html2",
path: "/index.html",
contains: "TSGRain",
cached: false,
},
{
name: "index_js",
path: path.Join("/assets", getIndexJS()),
contains: "app",
cached: true,
},
}
router := gin.New()
Register(router, 3)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", tt.path, nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), tt.contains)
ccHeader := w.Header().Get("Cache-Control")
if tt.cached {
assert.Equal(t, "public, max-age=604800, immutable", ccHeader)
} else {
assert.Equal(t, "", ccHeader)
}
})
}
}
func getIndexJS() string {
baseDir := "ui/dist/assets"
indexExp := regexp.MustCompile(`index\.[0-9a-f]{8}\.js`)
fixtures.CdProjectRoot()
distDir, err := os.Open(baseDir)
if err != nil {
panic(err)
}
list, err := distDir.Readdir(-1)
if err != nil {
panic(err)
}
for _, f := range list {
if indexExp.MatchString(f.Name()) {
return f.Name()
}
}
panic("no index.js found")
}

7
ui/vite.config.ts Normal file
View file

@ -0,0 +1,7 @@
import {defineConfig} from "vite"
import preact from "@preact/preset-vite"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [preact()],
})