diff --git a/.vscode/launch.json b/.vscode/launch.json
index b994667..fb0855d 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -9,7 +9,7 @@
"type": "go",
"request": "launch",
"mode": "auto",
- "program": "${workspaceFolder}/main.go",
+ "program": "${workspaceFolder}/src/main.go",
}
]
}
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 95fb7c2..121b2f7 100644
--- a/Makefile
+++ b/Makefile
@@ -1,10 +1,13 @@
-PROJECT_NAME := homeservice
+PROJECT_NAME := waterservice
PREFIX ?= /usr/bin
CONFIG_DIR := /etc/$(PROJECT_NAME)
+CONFIG_FILE := config.json
+DEVICES_DIR := $(CONFIG_DIR)
+DEVICES_FILE := devices.json
SYSTEM_DIR := /usr/lib/systemd/system
-WEB_DIR := /var/lib/$(PROJECT_NAME)
+WEB_DIR := /var/lib/$(PROJECT_NAME)/webui
BIN_FILE := build/bin/$(PROJECT_NAME)
UNIT_FILE := $(PROJECT_NAME).service
@@ -16,15 +19,12 @@ all: service
.PHONY: service
service:
mkdir -p bin
- go clean
- go mod tidy
- go build -o $(BIN_FILE)
+ cd src && go build -o ../$(BIN_FILE)
.PHONY: clean
clean:
- go clean
- rm -rf bin
- -rm $(PROJECT_NAME).tar.gz
+ cd src && go clean
+ rm -rf build
.PHONY: install
install: all
@@ -40,13 +40,29 @@ install: all
install -d $(WEB_DIR)
cp -r webui/* $(WEB_DIR)
+ # Config file
+ @if [ -f $(CONFIG_DIR)/$(notdir $(CONFIG_FILE)) ]; then \
+ echo "$(CONFIG_DIR)/$(notdir $(CONFIG_FILE)) already exists - skipping..."; \
+ else \
+ install -d $(CONFIG_DIR); \
+ install -m 0644 config/$(CONFIG_FILE) $(CONFIG_DIR); \
+ echo "install -d $(CONFIG_DIR)"; \
+ echo "install -m 0644 $(CONFIG_FILE) $(CONFIG_DIR)"; \
+ fi
+
+ # devices file
+ @if [ -f $(DEVICES_DIR)/$(notdir $(DEVICES_FILE)) ]; then \
+ echo "$(DEVICES_DIR)/$(notdir $(DEVICES_FILE)) already exists - skipping..."; \
+ else \
+ install -d $(DEVICES_DIR); \
+ install -m 0644 config/$(DEVICES_FILE) $(DEVICES_DIR); \
+ echo "install -d $(DEVICES_DIR)"; \
+ echo "install -m 0644 $(DEVICES_FILE) $(DEVICES_DIR)"; \
+ fi
+
.PHONY: uninstall
uninstall:
rm -rf $(CONFIG_DIR)
rm -rf $(SYSTEM_DIR)/$(UNIT_FILE)
rm -rf $(PREFIX)/$(PROJECT_NAME)
rm -rf $(WEB_DIR)
-
-.PHONY: package
-package: all
- tar cvzf $(PROJECT_NAME).tar.gz $(CONFIG_FILE) $(BIN_FILE) $(UNIT_FILE) $(README_FILE) webui/build/*
diff --git a/config/config.json b/config/config.json
new file mode 100644
index 0000000..6b63557
--- /dev/null
+++ b/config/config.json
@@ -0,0 +1,9 @@
+{
+ "influx": {
+ "host": "p5.local",
+ "port": 8086,
+ "token": "",
+ "org": "tkl",
+ "bucket": "home"
+ }
+}
diff --git a/config/devices.json b/config/devices.json
new file mode 100644
index 0000000..4567580
--- /dev/null
+++ b/config/devices.json
@@ -0,0 +1,84 @@
+{
+ "devices": [
+ {
+ "name": "Hühnerauslauf",
+ "url": "http://192.168.178.56:8080/katara/chicken",
+ "cmd_on": "{\"value\":\"on\"}",
+ "cmd_off": "{\"value\":\"off\"}",
+ "runtime": {
+ "value": 60,
+ "unit": "min"
+ }
+ },
+ {
+ "name": "Am Hochbeet",
+ "url": "http://192.168.178.56:8080/katara/bed",
+ "cmd_on": "{\"value\":\"on\"}",
+ "cmd_off": "{\"value\":\"off\"}",
+ "runtime": {
+ "value": 45,
+ "unit": "min"
+ }
+ },
+ {
+ "name": "An der Sauna",
+ "url": "http://192.168.178.56:8080/katara/sauna",
+ "cmd_on": "{\"value\":\"on\"}",
+ "cmd_off": "{\"value\":\"off\"}",
+ "runtime": {
+ "value": 15,
+ "unit": "min"
+ }
+ },
+ {
+ "name": "An der Haselnuss",
+ "url": "https://perinode-ms26f.local/sample/gpio2",
+ "cmd_on": "{\"data\": true}",
+ "cmd_off": "{\"data\": false}",
+ "runtime": {
+ "value": 60,
+ "unit": "min"
+ }
+ },
+ {
+ "name": "Am Holzlager",
+ "url": "https://perinode-gzzx6.local/sample/gpio1",
+ "cmd_on": "{\"data\": true}",
+ "cmd_off": "{\"data\": false}",
+ "runtime": {
+ "value": 15,
+ "unit": "min"
+ }
+ },
+ {
+ "name": "Tomatentuppen",
+ "url": "https://perinode-jim7u.local/sample/gpio1",
+ "cmd_on": "{\"data\": true}",
+ "cmd_off": "{\"data\": false}",
+ "runtime": {
+ "value": 5,
+ "unit": "min"
+ }
+ },
+ {
+ "name": "Zwischen Scheune und Haus",
+ "url": "https://perinode-jim7u.local/sample/gpio2",
+ "cmd_on": "{\"data\": true}",
+ "cmd_off": "{\"data\": false}",
+ "runtime": {
+ "value": 60,
+ "unit": "min"
+ }
+ },
+ {
+ "name": "Frei",
+ "url": "https://perinode-ms26f.local/sample/gpio1",
+ "cmd_on": "{\"data\": true}",
+ "cmd_off": "{\"data\": false}",
+ "runtime": {
+ "value": 60,
+ "unit": "min"
+ }
+ }
+ ]
+}
diff --git a/go.mod b/go.mod
deleted file mode 100644
index f1e3a25..0000000
--- a/go.mod
+++ /dev/null
@@ -1,20 +0,0 @@
-module webserver
-
-go 1.20
-
-require (
- git.blackfinn.de/apiservice/bicycle v0.0.2
- git.blackfinn.de/apiservice/sauna v1.1.0
-)
-
-require (
- github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
- github.com/eclipse/paho.mqtt.golang v1.4.3 // indirect
- github.com/google/uuid v1.6.0 // indirect
- github.com/gorilla/websocket v1.5.1 // indirect
- github.com/influxdata/influxdb-client-go/v2 v2.13.0 // indirect
- github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
- github.com/oapi-codegen/runtime v1.0.0 // indirect
- golang.org/x/net v0.22.0 // indirect
- golang.org/x/sync v0.6.0 // indirect
-)
diff --git a/go.sum b/go.sum
deleted file mode 100644
index d4df380..0000000
--- a/go.sum
+++ /dev/null
@@ -1,34 +0,0 @@
-git.blackfinn.de/apiservice/bicycle v0.0.2 h1:q+DMnWzxa4ZWxwMf03OPhQyDGRii7l7YmrEKYLgwn2A=
-git.blackfinn.de/apiservice/bicycle v0.0.2/go.mod h1:ygL9Ax8JreS+taIsqNq0NNlmhGnrHE+v1NE8Solihls=
-git.blackfinn.de/apiservice/sauna v1.1.0 h1:F2qysKjOld5lwTT7VJCY1YIEz6rXtE8CLpEfkGmeM3o=
-git.blackfinn.de/apiservice/sauna v1.1.0/go.mod h1:yM5lSlCApQrrZ7a+Y8ZxzBkLV2Frx6k1cw219XeSEG4=
-github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
-github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
-github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
-github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
-github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
-github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
-github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
-github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
-github.com/influxdata/influxdb-client-go/v2 v2.13.0 h1:ioBbLmR5NMbAjP4UVA5r9b5xGjpABD7j65pI8kFphDM=
-github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0sr5D8LolXHqAAOfPw9v/RIRHl4=
-github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU=
-github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
-github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
-github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
-github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
-golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/homeservice.service b/homeservice.service
deleted file mode 100644
index 19af6a7..0000000
--- a/homeservice.service
+++ /dev/null
@@ -1,10 +0,0 @@
-[Unit]
-Description=homeservice service
-After=multi-user.target
-
-[Service]
-Type=idle
-ExecStart=/usr/bin/homeservice -w /var/lib/homeservice/
-
-[Install]
-WantedBy=multi-user.target
diff --git a/main.go b/main.go
deleted file mode 100644
index 571faf8..0000000
--- a/main.go
+++ /dev/null
@@ -1,145 +0,0 @@
-package main
-
-import (
- "bytes"
- "crypto/tls"
- "encoding/json"
- "flag"
- "io"
- "log"
- "net/http"
-)
-
-type switcher struct {
- Name string `json:"name"`
- State bool `json:"state"`
-}
-
-var (
- logger log.Logger = *log.Default()
-)
-
-func init() {
- logger.SetFlags(log.Llongfile | log.Ltime)
-}
-
-func patch_request(url string, cmd string) error {
- req, err := http.NewRequest(http.MethodPatch, url, bytes.NewReader([]byte(cmd)))
- if err != nil {
- return err
- }
- res, err := http.DefaultClient.Do(req)
- if err != nil {
- return err
- }
- logger.Printf("got response!\n")
- logger.Printf("status code: %d\n", res.StatusCode)
- return nil
-}
-
-func do_the_switch(sw switcher) {
- var url string
- var cmd string
- switch sw.Name {
- case "chicken":
- url = "http://barsch:8080/katara/chicken"
- if sw.State {
- cmd = "{\"value\":\"on\"}"
- } else {
- cmd = "{\"value\":\"off\"}"
- }
- case "bed":
- url = "http://barsch:8080/katara/bed"
- if sw.State {
- cmd = "{\"value\":\"on\"}"
- } else {
- cmd = "{\"value\":\"off\"}"
- }
- case "sauna":
- url = "http://barsch:8080/katara/sauna"
- if sw.State {
- cmd = "{\"value\":\"on\"}"
- } else {
- cmd = "{\"value\":\"off\"}"
- }
- case "nut":
- url = "https://perinode-ms26f.local/sample/gpio2"
- if sw.State {
- cmd = "{\"data\": true}"
- } else {
- cmd = "{\"data\": false}"
- }
- case "wood":
- url = "https://perinode-gzzx6.local/sample/gpio1"
- if sw.State {
- cmd = "{\"data\": true}"
- } else {
- cmd = "{\"data\": false}"
- }
- case "tomato":
- url = "https://perinode-jim7u.local/sample/gpio1"
- if sw.State {
- cmd = "{\"data\": true}"
- } else {
- cmd = "{\"data\": false}"
- }
- case "barn":
- url = "https://perinode-jim7u.local/sample/gpio1"
- if sw.State {
- cmd = "{\"data\": true}"
- } else {
- cmd = "{\"data\": false}"
- }
- default:
- logger.Printf("Unknown switch: %s", sw.Name)
- return
- }
- err := patch_request(url, cmd)
- if err != nil {
- logger.Print(err)
- }
-}
-
-func http_endpoint_water_switch(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-type", "application/json; charset=utf-8;")
- if r.Method == http.MethodPatch {
- tmp, err := io.ReadAll(r.Body)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- w.Write(json.RawMessage(`{"error": "cannot unmarshal json to object"}`))
- } else {
- var sw switcher
- err = json.Unmarshal(tmp, &sw)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- w.Write(json.RawMessage(`{"error": "cannot unmarshal json to object"}`))
- } else {
- do_the_switch(sw)
- w.WriteHeader(http.StatusAccepted)
- }
- }
- } else {
- w.WriteHeader(http.StatusMethodNotAllowed)
- }
-}
-
-func main() {
- logger.Println("starting")
-
- var webui_path string
- flag.StringVar(&webui_path, "w", "./webui", "Specify path to serve the web ui. Default is ./webui")
- flag.Parse()
-
- http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
- // Serve files from static folder
- http.Handle("/", http.FileServer(http.Dir(webui_path)))
-
- http.HandleFunc("/water/switch", http_endpoint_water_switch)
-
- port := ":5005"
- logger.Println("Server is running on port" + port)
-
- // Start server on port specified above
- logger.Fatal(http.ListenAndServe(port, nil))
-
-}
diff --git a/src/go.mod b/src/go.mod
new file mode 100644
index 0000000..1ff2546
--- /dev/null
+++ b/src/go.mod
@@ -0,0 +1,13 @@
+module waterservice
+
+go 1.24
+
+require github.com/influxdata/influxdb-client-go/v2 v2.14.0
+
+require (
+ github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
+ github.com/google/uuid v1.3.1 // indirect
+ github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
+ github.com/oapi-codegen/runtime v1.0.0 // indirect
+ golang.org/x/net v0.23.0 // indirect
+)
diff --git a/src/go.sum b/src/go.sum
new file mode 100644
index 0000000..ca5e61e
--- /dev/null
+++ b/src/go.sum
@@ -0,0 +1,27 @@
+github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
+github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
+github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
+github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
+github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4=
+github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
+github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
+github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
+github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
+github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
+github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
+golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/src/internal/apiservice/devices/devices.go b/src/internal/apiservice/devices/devices.go
new file mode 100644
index 0000000..adc9e2a
--- /dev/null
+++ b/src/internal/apiservice/devices/devices.go
@@ -0,0 +1,75 @@
+package apiservice_devices
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+ "os"
+ "waterservice/internal/app/types"
+)
+
+type Device struct {
+ Name string `json:"name"`
+ Url string `json:"url"`
+ CmdOn string `json:"cmd_on"`
+ CmdOff string `json:"cmd_off"`
+ Runtime types.Telemetry `json:"runtime"`
+}
+
+type devices struct {
+ Devices []Device `json:"devices"`
+}
+
+var (
+ logger log.Logger = *log.Default()
+ devices_path string
+ devices_cache devices
+)
+
+func init() {
+ logger.SetFlags(log.Llongfile | log.Ltime)
+}
+
+func read_devices() {
+ data, err := os.ReadFile(devices_path)
+ if err != nil {
+ logger.Printf("Unable to read %s", devices_path)
+ return
+ }
+ err = json.Unmarshal(data, &devices_cache)
+ if err != nil {
+ logger.Print("Unable to evaluate config data")
+ return
+ }
+}
+
+func SetDevicesFilePath(path string) {
+ devices_path = path
+}
+
+func AddHandler() {
+ http.HandleFunc("/devices", handle_endpoint_devices)
+}
+
+func GetDevices() devices {
+ read_devices()
+ return devices_cache
+}
+
+func handle_endpoint_devices(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-type", "application/json; charset=utf-8;")
+ switch r.Method {
+ case http.MethodGet:
+ read_devices()
+ data, err := json.Marshal(devices_cache)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write(json.RawMessage(`{"error":"cannot mashal devices cache"}`))
+ } else {
+ w.WriteHeader(http.StatusOK)
+ w.Write(data)
+ }
+ default:
+ w.WriteHeader(http.StatusNotImplemented)
+ }
+}
diff --git a/src/internal/apiservice/mode/mode.go b/src/internal/apiservice/mode/mode.go
new file mode 100644
index 0000000..fc60dd0
--- /dev/null
+++ b/src/internal/apiservice/mode/mode.go
@@ -0,0 +1,61 @@
+package apiservice_mode
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ "waterservice/internal/app/scheduler"
+)
+
+type mode_container struct {
+ Mode string `json:"mode"`
+}
+
+func AddHandler() {
+ http.HandleFunc("/mode", handle_endpoint_mode)
+}
+
+func handle_endpoint_mode(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-type", "application/json; charset=utf-8;")
+ switch r.Method {
+ case http.MethodGet:
+ handle_get(w)
+ case http.MethodPatch:
+ handle_patch(w, r)
+ default:
+ w.WriteHeader(http.StatusNotImplemented)
+ }
+}
+
+func handle_get(w http.ResponseWriter) {
+ mode := scheduler.GetMode()
+ container := mode_container{
+ Mode: string(mode),
+ }
+ data, err := json.Marshal(container)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write(json.RawMessage(`{"error":"cannot mashal devices cache"}`))
+ } else {
+ w.WriteHeader(http.StatusOK)
+ w.Write(data)
+ }
+}
+
+func handle_patch(w http.ResponseWriter, r *http.Request) {
+ tmp, err := io.ReadAll(r.Body)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write(json.RawMessage(`{"error": "cannot read reqest body"}`))
+ return
+ }
+ var container mode_container
+ err = json.Unmarshal(tmp, &container)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write(json.RawMessage(`{"error": "cannot unmarshal json to object"}`))
+ return
+ }
+ scheduler.SetMode(scheduler.AppMode(container.Mode))
+ w.WriteHeader(http.StatusAccepted)
+}
diff --git a/src/internal/apiservice/pv/pv.go b/src/internal/apiservice/pv/pv.go
new file mode 100644
index 0000000..f278e19
--- /dev/null
+++ b/src/internal/apiservice/pv/pv.go
@@ -0,0 +1,83 @@
+package apiservice_pv
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "strconv"
+ "waterservice/internal/app/config"
+ "waterservice/internal/app/types"
+
+ influxdb2 "github.com/influxdata/influxdb-client-go/v2"
+ "github.com/influxdata/influxdb-client-go/v2/api"
+)
+
+var (
+ logger log.Logger = *log.Default()
+ client influxdb2.Client
+ query_api api.QueryAPI
+ query string
+)
+
+func init() {
+ logger.SetFlags(log.Llongfile | log.Ltime)
+}
+
+func Start() {
+ cfg := config.GetConfig()
+ // setup influx connection
+ influxdb_url := "http://" + cfg.Influx.Host + ":" + strconv.Itoa(cfg.Influx.Port)
+ client = influxdb2.NewClient(influxdb_url, cfg.Influx.Token)
+
+ query = fmt.Sprintf(`from(bucket: "%s")
+ |> range(start: -10m)
+ |> filter(fn: (r) => r["_measurement"] == "AlphaEss")
+ |> filter(fn: (r) => r["_field"] == "BatteryStateOfCharge")
+ |> last()`, cfg.Influx.Bucket)
+
+ query_api = client.QueryAPI(cfg.Influx.Org)
+}
+
+func AddHandler() {
+ http.HandleFunc("/battery/stateofcharge", handle_endpoint_stateofcharge)
+}
+
+func handle_endpoint_stateofcharge(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-type", "application/json; charset=utf-8;")
+ switch r.Method {
+ case http.MethodGet:
+ res, err := battery_state_of_charge()
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write(json.RawMessage(fmt.Sprintf(`{"error":"%s"}`, err.Error())))
+ }
+ data, err := json.Marshal(res)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write(json.RawMessage(`{"error":"cannot mashal battery state of charge"}`))
+ } else {
+ w.WriteHeader(http.StatusOK)
+ w.Write(data)
+ }
+ default:
+ w.WriteHeader(http.StatusNotImplemented)
+ }
+}
+
+func battery_state_of_charge() (types.Telemetry, error) {
+ var ret types.Telemetry
+ results, err := query_api.Query(context.Background(), query)
+ if err != nil {
+ return ret, err
+ }
+ for results.Next() {
+ ret.Value = results.Record().Value().(float64)
+ }
+ if err := results.Err(); err != nil {
+ return ret, err
+ }
+ ret.Unit = "%"
+ return ret, nil
+}
diff --git a/src/internal/apiservice/soil/moisture.go b/src/internal/apiservice/soil/moisture.go
new file mode 100644
index 0000000..b555ce6
--- /dev/null
+++ b/src/internal/apiservice/soil/moisture.go
@@ -0,0 +1,88 @@
+package apiservice_soil
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "strconv"
+ "waterservice/internal/app/config"
+ "waterservice/internal/app/types"
+
+ influxdb2 "github.com/influxdata/influxdb-client-go/v2"
+ "github.com/influxdata/influxdb-client-go/v2/api"
+)
+
+var (
+ logger log.Logger = *log.Default()
+ client influxdb2.Client
+ query_api api.QueryAPI
+ query string
+)
+
+func init() {
+ logger.SetFlags(log.Llongfile | log.Ltime)
+}
+
+func Start() {
+ cfg := config.GetConfig()
+ // setup influx connection
+ influxdb_url := "http://" + cfg.Influx.Host + ":" + strconv.Itoa(cfg.Influx.Port)
+ client = influxdb2.NewClient(influxdb_url, cfg.Influx.Token)
+
+ query = fmt.Sprintf(`from(bucket: "%s")
+ |> range(start: -10m)
+ |> filter(fn: (r) => r["_measurement"] == "garden")
+ |> filter(fn: (r) => r["_field"] == "value")
+ |> filter(fn: (r) => r["soil"] == "moisture")
+ |> last()`, cfg.Influx.Bucket)
+
+ query_api = client.QueryAPI(cfg.Influx.Org)
+}
+
+func AddHandler() {
+ http.HandleFunc("/soil/moisture", handle_endpoint_moisture)
+}
+
+func handle_endpoint_moisture(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-type", "application/json; charset=utf-8;")
+ switch r.Method {
+ case http.MethodGet:
+ res, err := moisture()
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write(json.RawMessage(fmt.Sprintf(`{"error":"%s"}`, err.Error())))
+ }
+ data, err := json.Marshal(res)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write(json.RawMessage(`{"error":"cannot mashal moisture"}`))
+ } else {
+ w.WriteHeader(http.StatusOK)
+ w.Write(data)
+ }
+ default:
+ w.WriteHeader(http.StatusNotImplemented)
+ }
+}
+
+func moisture() (types.Telemetry, error) {
+ var ret types.Telemetry
+ results, err := query_api.Query(context.Background(), query)
+ if err != nil {
+ return ret, err
+ }
+ for results.Next() {
+ ret.Value = results.Record().Value().(float64)
+ }
+ if err := results.Err(); err != nil {
+ return ret, err
+ }
+ // re-calculate to %
+ // 100 % := 3.0 V
+ // 0 % := 0.0 V
+ ret.Value = -0.03*ret.Value + 3
+ ret.Unit = "%"
+ return ret, nil
+}
diff --git a/src/internal/apiservice/state/requests.go b/src/internal/apiservice/state/requests.go
new file mode 100644
index 0000000..b196750
--- /dev/null
+++ b/src/internal/apiservice/state/requests.go
@@ -0,0 +1,54 @@
+package apiservice_state
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+)
+
+func patch(url string, cmd string) error {
+ req, err := http.NewRequest(http.MethodPatch, url, bytes.NewReader([]byte(cmd)))
+ if err != nil {
+ return err
+ }
+ _, err = http.DefaultClient.Do(req)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func get(url string) (state, error) {
+ resp, err := http.Get(url)
+ if err != nil {
+ return StateOff, err
+ }
+ if resp.StatusCode != http.StatusOK {
+ return StateOff, fmt.Errorf("error: received status code: %d", resp.StatusCode)
+ }
+ res, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return StateOff, err
+ }
+ var dev map[string]any
+ err = json.Unmarshal(res, &dev)
+ if err != nil {
+ return StateOff, err
+ }
+ tmp := dev["Value"]
+ if tmp != nil {
+ return state(tmp.(string)), nil
+ }
+ tmp = dev["data"]
+ if tmp != nil {
+ if tmp.(bool) {
+ return StateOn, nil
+ } else {
+ return StateOff, nil
+ }
+ }
+ return StateOff, errors.New("unknown result")
+}
diff --git a/src/internal/apiservice/state/state.go b/src/internal/apiservice/state/state.go
new file mode 100644
index 0000000..9a44c4c
--- /dev/null
+++ b/src/internal/apiservice/state/state.go
@@ -0,0 +1,220 @@
+package apiservice_state
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "sync"
+ "time"
+ apiservice_devices "waterservice/internal/apiservice/devices"
+)
+
+type state string
+
+type registerOffSwitch func(apiservice_devices.Device)
+
+type State struct {
+ Name string `json:"name"`
+ State state `json:"state"`
+ Runtime time.Duration `json:"runtime"`
+}
+
+const (
+ StateOn state = "on"
+ StateOff state = "off"
+)
+
+var (
+ logger log.Logger = *log.Default()
+
+ state_cache map[string]State
+ state_mutex sync.Mutex
+ cb_register_off registerOffSwitch
+ cb_unregister_off registerOffSwitch
+)
+
+func init() {
+ logger.SetFlags(log.Llongfile | log.Ltime)
+ state_cache = map[string]State{}
+}
+
+func AddHandler() {
+ http.HandleFunc("/state", handle_endpoint_switch)
+}
+
+func Start() {
+ state_mutex.Lock()
+ for _, device := range apiservice_devices.GetDevices().Devices {
+ tmp := State{
+ Name: device.Name,
+ State: StateOff,
+ Runtime: 0,
+ }
+ state_cache[device.Name] = tmp
+ }
+ state_mutex.Unlock()
+
+ go poll_states()
+}
+
+func SetOffSwitchCallbacks(cb_register registerOffSwitch, cb_unregister registerOffSwitch) {
+ cb_register_off = cb_register
+ cb_unregister_off = cb_unregister
+}
+
+func SetState(st State) error {
+ devices := apiservice_devices.GetDevices()
+ for _, dev := range devices.Devices {
+ if dev.Name == st.Name {
+ url := dev.Url
+ var cmd string
+ switch st.State {
+ case StateOn:
+ cmd = dev.CmdOn
+ case StateOff:
+ cmd = dev.CmdOff
+ }
+ err := patch(url, cmd)
+ if err != nil {
+ return err
+ }
+ state_mutex.Lock()
+ tmp := state_cache[dev.Name]
+ tmp.State = st.State
+ state_cache[dev.Name] = tmp
+ state_mutex.Unlock()
+ if cb_register_off != nil && cb_unregister_off != nil {
+ if st.State == StateOn {
+ cb_register_off(dev)
+ } else {
+ cb_unregister_off(dev)
+ }
+ }
+ return nil
+ }
+ }
+ // FIXME: device not found...
+ return nil
+}
+
+func SetRuntime(name string, runtime time.Duration) {
+ state_mutex.Lock()
+ tmp := state_cache[name]
+ tmp.Runtime = runtime
+ state_cache[name] = tmp
+ state_mutex.Unlock()
+}
+
+func handle_endpoint_switch(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-type", "application/json; charset=utf-8;")
+ switch r.Method {
+ case http.MethodPatch:
+ err := handle_patch(r)
+ if err != nil {
+ logger.Print(err)
+ w.WriteHeader(http.StatusInternalServerError)
+ err_str := fmt.Sprintf("{\"error\": \"%s\"}", err.Error())
+ w.Write(json.RawMessage([]byte(err_str)))
+ return
+ }
+ w.WriteHeader(http.StatusAccepted)
+ case http.MethodGet:
+ data, err := handle_get()
+ if err != nil {
+ logger.Print(err)
+ w.WriteHeader(http.StatusInternalServerError)
+ err_str := fmt.Sprintf("{\"error\": \"%s\"}", err.Error())
+ w.Write(json.RawMessage([]byte(err_str)))
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Write(data)
+ default:
+ w.WriteHeader(http.StatusNotImplemented)
+ }
+}
+
+func handle_patch(r *http.Request) error {
+ tmp, err := io.ReadAll(r.Body)
+ if err != nil {
+ return err
+ }
+ var st State
+ err = json.Unmarshal(tmp, &st)
+ if err != nil {
+ return err
+ }
+
+ devices := apiservice_devices.GetDevices()
+ for _, dev := range devices.Devices {
+ if dev.Name == st.Name {
+ url := dev.Url
+ var cmd string
+ switch st.State {
+ case StateOn:
+ cmd = dev.CmdOn
+ case StateOff:
+ cmd = dev.CmdOff
+ }
+ err = patch(url, cmd)
+ if err != nil {
+ return err
+ }
+ state_mutex.Lock()
+ tmp := state_cache[dev.Name]
+ tmp.State = st.State
+ state_cache[dev.Name] = tmp
+ state_mutex.Unlock()
+ if cb_register_off != nil && cb_unregister_off != nil {
+ if st.State == StateOn {
+ cb_register_off(dev)
+ } else {
+ cb_unregister_off(dev)
+ }
+ }
+ return nil
+ }
+ }
+ // FIXME: device not found...
+ return nil
+}
+
+func handle_get() ([]byte, error) {
+ var states []State
+ state_mutex.Lock()
+ for _, v := range state_cache {
+ tmp := State{
+ Name: v.Name,
+ State: v.State,
+ Runtime: v.Runtime,
+ }
+ states = append(states, tmp)
+ }
+ state_mutex.Unlock()
+ res, err := json.Marshal(states)
+ if err != nil {
+ return res, err
+ }
+ return res, nil
+}
+
+func poll_states() {
+ for {
+ devices := apiservice_devices.GetDevices()
+ for _, dev := range devices.Devices {
+ status, err := get(dev.Url)
+ if err != nil {
+ logger.Print(err)
+ continue
+ }
+ state_mutex.Lock()
+ tmp := state_cache[dev.Name]
+ tmp.State = status
+ state_cache[dev.Name] = tmp
+ state_mutex.Unlock()
+ }
+ time.Sleep(time.Second * 10)
+ }
+}
diff --git a/src/internal/app/config/config.go b/src/internal/app/config/config.go
new file mode 100644
index 0000000..6180d7b
--- /dev/null
+++ b/src/internal/app/config/config.go
@@ -0,0 +1,44 @@
+package config
+
+import (
+ "encoding/json"
+ "log"
+ "waterservice/internal/app/storage"
+)
+
+type config struct {
+ Influx struct {
+ Host string `json:"host"`
+ Port int `json:"port"`
+ Token string `json:"token"`
+ Org string `json:"org"`
+ Bucket string `json:"bucket"`
+ } `json:"influx"`
+}
+
+var (
+ logger log.Logger = *log.Default()
+ store storage.Storage
+ config_cache config
+)
+
+func init() {
+ logger.SetFlags(log.Llongfile | log.Ltime)
+}
+
+func SetConfigFilePath(path string) {
+ store.Path = path
+ res, err := store.Read()
+ if err != nil {
+ logger.Print("unable to read config")
+ return
+ }
+ err = json.Unmarshal(res, &config_cache)
+ if err != nil {
+ logger.Print("unable to unmarshal json to object")
+ }
+}
+
+func GetConfig() config {
+ return config_cache
+}
diff --git a/src/internal/app/scheduler/scheduler.go b/src/internal/app/scheduler/scheduler.go
new file mode 100644
index 0000000..4490266
--- /dev/null
+++ b/src/internal/app/scheduler/scheduler.go
@@ -0,0 +1,149 @@
+package scheduler
+
+import (
+ "encoding/json"
+ "log"
+ "sync"
+ "time"
+ "waterservice/internal/app/storage"
+
+ apiservice_devices "waterservice/internal/apiservice/devices"
+ apiservice_state "waterservice/internal/apiservice/state"
+)
+
+const (
+ app_state_storage_path = "/var/lib/waterservice/scheduler/state.json"
+ ModeAuto AppMode = "auto"
+ ModeManual AppMode = "manual"
+)
+
+var (
+ logger log.Logger = *log.Default()
+ app_state_cache app_state
+ app_state_mutex sync.Mutex
+ store storage.Storage
+)
+
+func init() {
+ logger.SetFlags(log.Llongfile | log.Ltime)
+
+ // set defaults
+ app_state_cache.Mode = ModeManual
+
+ // override defaults with stored values
+ store.Path = app_state_storage_path
+ res, err := store.Read()
+ if err != nil {
+ logger.Print("unable to read app state cache")
+ return
+ }
+ app_state_mutex.Lock()
+ err = json.Unmarshal(res, &app_state_cache)
+ app_state_mutex.Unlock()
+ if err != nil {
+ logger.Print("unable to evaluate config data")
+ }
+}
+
+func SetMode(mode AppMode) {
+ app_state_mutex.Lock()
+ app_state_cache.Mode = mode
+ res, err := json.Marshal(app_state_cache)
+ if err != nil {
+ logger.Print("unable to store app state cache")
+ return
+ }
+ store.Write(res)
+ app_state_mutex.Unlock()
+}
+
+func GetMode() AppMode {
+ app_state_mutex.Lock()
+ ret := app_state_cache.Mode
+ app_state_mutex.Unlock()
+ return ret
+}
+
+func Start() {
+ apiservice_state.SetOffSwitchCallbacks(register_off_device, unregister_off_device)
+
+ go app_state_saver()
+ go poll_auto_off()
+}
+
+func app_state_saver() {
+ for {
+ app_state_mutex.Lock()
+ res, err := json.Marshal(app_state_cache)
+ app_state_mutex.Unlock()
+ if err != nil {
+ logger.Print("unable to marshal object to json")
+ time.Sleep(time.Minute)
+ continue
+ }
+ store.Write(res)
+ time.Sleep(time.Minute)
+ }
+}
+
+func poll_auto_off() {
+ for {
+ app_state_mutex.Lock()
+ for i, od := range app_state_cache.off_devices {
+ var duration time.Duration
+ switch od.device.Runtime.Unit {
+ case "min":
+ duration = time.Duration(od.device.Runtime.Value) * 60000000000 // minute -> nanosecont
+ default:
+ logger.Printf("Unit %s handling not implemented", od.device.Runtime.Unit)
+ }
+ off_time := od.start_time.Add(duration)
+ run_time := time.Since(od.start_time)
+ apiservice_state.SetRuntime(od.device.Name, run_time)
+ if time.Now().After(off_time) {
+ logger.Printf("switching off %s", od.device.Name)
+ var st = apiservice_state.State{
+ Name: od.device.Name,
+ State: apiservice_state.StateOff,
+ }
+ apiservice_state.SetState(st)
+ if len(app_state_cache.off_devices) > 1 {
+ app_state_cache.off_devices = append(app_state_cache.off_devices[:i], app_state_cache.off_devices[i+1:]...)
+ } else {
+ app_state_cache.off_devices = nil
+ }
+ }
+ }
+ app_state_mutex.Unlock()
+ time.Sleep(time.Second)
+ }
+}
+
+func register_off_device(dev apiservice_devices.Device) {
+ for _, od := range app_state_cache.off_devices {
+ if od.device.Name == dev.Name {
+ // device already in off list
+ // FIXME: update off time
+ return
+ }
+ }
+ var tmp off_device
+ tmp.device = dev
+ tmp.start_time = time.Now()
+ app_state_mutex.Lock()
+ app_state_cache.off_devices = append(app_state_cache.off_devices, tmp)
+ app_state_mutex.Unlock()
+}
+
+func unregister_off_device(dev apiservice_devices.Device) {
+ for i, od := range app_state_cache.off_devices {
+ if od.device.Name == dev.Name {
+ if len(app_state_cache.off_devices) > 1 {
+ app_state_cache.off_devices = append(app_state_cache.off_devices[:i], app_state_cache.off_devices[i+1:]...)
+ } else {
+ app_state_cache.off_devices = nil
+ }
+ return
+ }
+ }
+}
diff --git a/src/internal/app/scheduler/types.go b/src/internal/app/scheduler/types.go
new file mode 100644
index 0000000..8013c99
--- /dev/null
+++ b/src/internal/app/scheduler/types.go
@@ -0,0 +1,18 @@
+package scheduler
+
+import (
+ "time"
+ apiservice_devices "waterservice/internal/apiservice/devices"
+)
+
+type AppMode string
+
+type app_state struct {
+ Mode AppMode
+ off_devices []off_device
+}
+
+type off_device struct {
+ device apiservice_devices.Device
+ start_time time.Time
+}
diff --git a/src/internal/app/storage/storage.go b/src/internal/app/storage/storage.go
new file mode 100644
index 0000000..bb16e61
--- /dev/null
+++ b/src/internal/app/storage/storage.go
@@ -0,0 +1,50 @@
+package storage
+
+import (
+ "log"
+ "os"
+ "path/filepath"
+)
+
+type Storage struct {
+ Path string
+}
+
+var (
+ logger log.Logger = *log.Default()
+)
+
+func init() {
+ logger.SetFlags(log.Llongfile | log.Ltime)
+}
+
+func (storage Storage) Read() ([]byte, error) {
+ data, err := os.ReadFile(storage.Path)
+ if err != nil {
+ logger.Printf("unable to read %s (%s)", storage.Path, err.Error())
+ return nil, err
+ }
+ return data, nil
+}
+
+func (storage Storage) Write(data []byte) error {
+ dir := filepath.Dir(storage.Path)
+ _, err := os.Stat(dir)
+ if err != nil {
+ if os.IsNotExist(err) {
+ err = os.MkdirAll(dir, 0644)
+ if err != nil {
+ logger.Printf("unable to create %s (%s)", dir, err.Error())
+ return err
+ }
+ } else {
+ logger.Print(err)
+ return err
+ }
+ }
+ err = os.WriteFile(storage.Path, data, 0644)
+ if err != nil {
+ logger.Printf("unable to store %s (%s)", storage.Path, err.Error())
+ }
+ return nil
+}
diff --git a/src/internal/app/types/types.go b/src/internal/app/types/types.go
new file mode 100644
index 0000000..3173cd8
--- /dev/null
+++ b/src/internal/app/types/types.go
@@ -0,0 +1,6 @@
+package types
+
+type Telemetry struct {
+ Value float64 `json:"value"`
+ Unit string `json:"unit"`
+}
diff --git a/src/main.go b/src/main.go
new file mode 100644
index 0000000..145f80c
--- /dev/null
+++ b/src/main.go
@@ -0,0 +1,60 @@
+package main
+
+import (
+ "crypto/tls"
+ "flag"
+ "log"
+ "net/http"
+
+ apiservice_devices "waterservice/internal/apiservice/devices"
+ apiservice_mode "waterservice/internal/apiservice/mode"
+ apiservice_pv "waterservice/internal/apiservice/pv"
+ apiservice_soil "waterservice/internal/apiservice/soil"
+ apiservice_state "waterservice/internal/apiservice/state"
+ "waterservice/internal/app/config"
+ "waterservice/internal/app/scheduler"
+)
+
+var (
+ logger log.Logger = *log.Default()
+)
+
+func init() {
+ logger.SetFlags(log.Llongfile | log.Ltime)
+}
+
+func main() {
+ logger.Println("starting")
+
+ var webui_path string
+ var devices_path string
+ var config_path string
+ flag.StringVar(&webui_path, "w", "../webui", "Specify path to serve the web ui. Default is ./webui")
+ flag.StringVar(&devices_path, "d", "../config/devices.json", "Specify path of devices defifintion file. Default is ../config/devices.json")
+ flag.StringVar(&config_path, "c", "../config/config.json", "Specify path of config file. Default is ../config/config.json")
+ flag.Parse()
+
+ http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+ http.Handle("/", http.FileServer(http.Dir(webui_path)))
+
+ config.SetConfigFilePath(config_path)
+
+ apiservice_devices.SetDevicesFilePath(devices_path)
+
+ apiservice_devices.AddHandler()
+ apiservice_mode.AddHandler()
+ apiservice_state.AddHandler()
+ apiservice_pv.AddHandler()
+ apiservice_soil.AddHandler()
+
+ apiservice_state.Start()
+ apiservice_pv.Start()
+ apiservice_soil.Start()
+ scheduler.Start()
+
+ port := ":5005"
+ logger.Println("Server is running on port" + port)
+
+ logger.Fatal(http.ListenAndServe(port, nil))
+}
diff --git a/waterservice.service b/waterservice.service
new file mode 100644
index 0000000..9f01f35
--- /dev/null
+++ b/waterservice.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=waterservice service
+After=multi-user.target
+
+[Service]
+Type=idle
+ExecStart=/usr/bin/waterservice -w /var/lib/waterservice/webui -d /etc/waterservice/devices.json -c /etc/waterservice/config.json
+
+[Install]
+WantedBy=multi-user.target
diff --git a/webui/css/slider.css b/webui/css/slider.css
new file mode 100644
index 0000000..a4cc803
--- /dev/null
+++ b/webui/css/slider.css
@@ -0,0 +1,61 @@
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 60px;
+ height: 34px;
+}
+
+/* Hide default HTML checkbox */
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+/* The slider */
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #ccc;
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+.slider:before {
+ position: absolute;
+ content: "";
+ height: 26px;
+ width: 26px;
+ left: 4px;
+ bottom: 4px;
+ background-color: white;
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+input:checked + .slider {
+ background-color: #5b5b5b;
+}
+
+input:focus + .slider {
+ box-shadow: 0 0 1px #2196F3;
+}
+
+input:checked + .slider:before {
+ -webkit-transform: translateX(26px);
+ -ms-transform: translateX(26px);
+ transform: translateX(26px);
+}
+
+/* Rounded sliders */
+.slider.round {
+ border-radius: 34px;
+}
+
+.slider.round:before {
+ border-radius: 50%;
+}
diff --git a/webui/css/style.css b/webui/css/style.css
new file mode 100644
index 0000000..0123e51
--- /dev/null
+++ b/webui/css/style.css
@@ -0,0 +1,14 @@
+html, body {
+ height: 100%;
+}
+
+html {
+ display: table;
+ margin: auto;
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+body {
+ display: table-cell;
+ vertical-align: middle;
+}
diff --git a/webui/index.html b/webui/index.html
index 0de4ee0..3db18a8 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -1,125 +1,142 @@
-
-
+
+
Wasser
@@ -127,84 +144,33 @@