Compare commits

...

19 Commits

Author SHA1 Message Date
8f0fd525d9 Manual mode
Signed-off-by: Thomas Klaehn <tkl@blackfinn.de>
2025-08-14 08:55:02 +02:00
Thomas Klaehn
6a9a42adc6 Water web service
Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
2025-04-17 12:44:35 +02:00
Thomas Klaehn
f71c15b4d2 Update module dependencies
Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
2024-07-09 17:13:17 +02:00
Thomas Klaehn
d3b5b7d9ba Deactivate plot in sauna
Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
2024-07-04 07:04:52 +02:00
Thomas Klaehn
d26f92a0f1 Fix: exchange test data with measurement in plot
Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
2024-07-03 11:32:47 +02:00
Thomas Klaehn
5822328d9d Add data plot to sauna route
Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
2024-07-03 11:25:54 +02:00
Thomas Klaehn
59cb6d1380 Update go module dependencies 2024-04-03 07:51:20 +02:00
Thomas Klaehn
149a6ddefe Change menu 2024-03-25 07:46:16 +01:00
tkl
0a2b75f4b5 Update webui/src/routes/bicycle/+page.svelte 2024-02-20 08:26:56 +00:00
tkl
ca7ff8e154 Update webui/src/routes/bicycle/+page.svelte 2024-02-20 08:26:12 +00:00
tkl
7e36c36ec0 Fix year format 2024-02-13 12:52:57 +00:00
tkl
2a1925dba9 Change display yeat 2023 -> 2024 2024-02-13 08:18:23 +00:00
tkl
bd7af78cba Merge pull request 'add_bicycle' (#3) from add_bicycle into main
Reviewed-on: #3
2023-03-05 08:15:57 +00:00
Thomas Klaehn
6c6e405c92 Update module dependencies 2023-03-05 09:14:34 +01:00
Thomas Klaehn
99d0cb478a Add bicycle page 2023-03-05 09:10:17 +01:00
Thomas Klaehn
52dbdb0b65 Webserver: Add bicycle api 2023-03-01 08:21:56 +01:00
Thomas Klaehn
137b342629 Merge branch 'add_time_to_sauna_api' 2023-03-01 08:20:16 +01:00
Thomas Klaehn
4eae02cd0c Separate sauna api into single module 2023-03-01 08:17:32 +01:00
Thomas Klaehn
84d84b4313 Sauna: Add update timestamp to sauna api 2023-02-21 08:06:05 +01:00
59 changed files with 1334 additions and 6343 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
/node_modules/ /node_modules/
/build/ /build/
/vendor/
.DS_Store .DS_Store

3
.vscode/launch.json vendored
View File

@@ -9,8 +9,7 @@
"type": "go", "type": "go",
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"program": "${workspaceFolder}/main.go", "program": "${workspaceFolder}/src/main.go",
"preLaunchTask": "npm: build - webui"
} }
] ]
} }

View File

@@ -1,50 +1,33 @@
PROJECT_NAME := homeservice PROJECT_NAME := waterservice
PREFIX ?= /usr/bin PREFIX ?= /usr/bin
CONFIG_DIR := /etc/$(PROJECT_NAME) CONFIG_DIR := /etc/$(PROJECT_NAME)
CONFIG_FILE := config.json
DEVICES_DIR := $(CONFIG_DIR)
DEVICES_FILE := devices.json
SYSTEM_DIR := /usr/lib/systemd/system SYSTEM_DIR := /usr/lib/systemd/system
WEB_DIR := /var/lib/$(PROJECT_NAME) WEB_DIR := /var/lib/$(PROJECT_NAME)/webui
CONFIG_FILE := config/config.json
BIN_FILE := build/bin/$(PROJECT_NAME) BIN_FILE := build/bin/$(PROJECT_NAME)
UNIT_FILE := $(PROJECT_NAME).service UNIT_FILE := $(PROJECT_NAME).service
README_FILE := README.md README_FILE := README.md
.PHONY: all .PHONY: all
all: webui service all: service
.PHONY: service .PHONY: service
service: service:
mkdir -p bin mkdir -p bin
go clean cd src && go build -o ../$(BIN_FILE)
go mod tidy
go build -o $(BIN_FILE)
.PHONY: webui
webui:
npm install --prefix webui
npm run build --prefix webui
.PHONY: clean .PHONY: clean
clean: clean:
go clean cd src && go clean
rm -rf bin rm -rf build
rm -rf webui/build webui/node_modules webui/.svelte-kit
-rm $(PROJECT_NAME).tar.gz
.PHONY: install .PHONY: install
install: all install: all
# 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_FILE) $(CONFIG_DIR); \
echo "install -d $(CONFIG_DIR)"; \
echo "install -m 0644 $(CONFIG_FILE) $(CONFIG_DIR)"; \
fi
# Binary # Binary
install -d $(PREFIX) install -d $(PREFIX)
install -m 0755 $(BIN_FILE) $(PREFIX) install -m 0755 $(BIN_FILE) $(PREFIX)
@@ -55,7 +38,27 @@ install: all
# Web ui # Web ui
install -d $(WEB_DIR) install -d $(WEB_DIR)
cp -r webui/build/* $(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 .PHONY: uninstall
uninstall: uninstall:
@@ -63,7 +66,3 @@ uninstall:
rm -rf $(SYSTEM_DIR)/$(UNIT_FILE) rm -rf $(SYSTEM_DIR)/$(UNIT_FILE)
rm -rf $(PREFIX)/$(PROJECT_NAME) rm -rf $(PREFIX)/$(PROJECT_NAME)
rm -rf $(WEB_DIR) rm -rf $(WEB_DIR)
.PHONY: package
package: all
tar cvzf $(PROJECT_NAME).tar.gz $(CONFIG_FILE) $(BIN_FILE) $(UNIT_FILE) $(README_FILE) webui/build/*

View File

@@ -0,0 +1,9 @@
{
"influx": {
"host": "p5.local",
"port": 8086,
"token": "",
"org": "tkl",
"bucket": "home"
}
}

84
config/devices.json Normal file
View File

@@ -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"
}
}
]
}

11
go.mod
View File

@@ -1,11 +0,0 @@
module webserver
go 1.19
require github.com/eclipse/paho.mqtt.golang v1.4.2
require (
github.com/gorilla/websocket v1.4.2 // indirect
golang.org/x/net v0.4.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
)

13
go.sum
View File

@@ -1,13 +0,0 @@
github.com/eclipse/paho.mqtt.golang v1.4.2 h1:66wOzfUHSSI1zamx7jR6yMEI5EuHnT1G6rNA5PM12m4=
github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -1,10 +0,0 @@
[Unit]
Description=homeservice service
After=multi-user.target
[Service]
Type=idle
ExecStart=/usr/bin/homeservice -d /var/lib/home/
[Install]
WantedBy=multi-user.target

110
main.go
View File

@@ -1,110 +0,0 @@
package main
import (
"encoding/json"
"flag"
"log"
"net/http"
"sync"
mqtt "github.com/eclipse/paho.mqtt.golang"
)
type temperature struct {
Value float64 `json:"value"`
Unit string `json:"unit"`
}
var (
logger log.Logger = *log.Default()
sauna_mutex sync.Mutex
sauna_temperature = temperature{
Value: 0.0,
Unit: "°C",
}
)
var messagePubHandler mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) {
logger.Printf("Received message: %s from topic: %s\n", msg.Payload(), msg.Topic())
}
var saunaHandler mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) {
log.Printf("Received message: %s from topic: %s\n", msg.Payload(), msg.Topic())
sauna_mutex.Lock()
err := json.Unmarshal(msg.Payload(), &sauna_temperature)
sauna_mutex.Unlock()
if err != nil {
logger.Print(err)
return
}
}
var connectHandler mqtt.OnConnectHandler = func(client mqtt.Client) {
logger.Println("Connected")
}
var connectLostHandler mqtt.ConnectionLostHandler = func(client mqtt.Client, err error) {
logger.Printf("Connect lost: %v", err)
}
func init() {
logger.SetPrefix("Homeservice: ")
}
func http_endpoint_sauna(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "application/json; charset=utf-8;")
if r.Method == http.MethodGet {
sauna_mutex.Lock()
data, err := json.Marshal(sauna_temperature)
sauna_mutex.Unlock()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write(json.RawMessage(`{"error": "cannot marshal object to json"}`))
} else {
w.WriteHeader(http.StatusOK)
w.Write(data)
}
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func main() {
logger.Println("starting")
var webui_path string
flag.StringVar(&webui_path, "d", "./build/webui", "Specify path to serve the web ui. Default is ./static")
flag.Parse()
// MQTT connection
opts := mqtt.NewClientOptions()
opts.AddBroker("tcp://nuc:1883")
opts.SetClientID("homeservice")
opts.SetDefaultPublishHandler(messagePubHandler)
opts.OnConnect = connectHandler
opts.OnConnectionLost = connectLostHandler
client := mqtt.NewClient(opts)
if token := client.Connect(); token.Wait() && token.Error() != nil {
panic(token.Error())
}
// MQTT subscribtion
topic := "sauna/temperature"
token := client.Subscribe(topic, 1, saunaHandler)
token.Wait()
logger.Printf("Subscribed to topic %s", topic)
// API routes
// Serve files from static folder
http.Handle("/", http.FileServer(http.Dir(webui_path)))
http.HandleFunc("/sauna/sample", http_endpoint_sauna)
port := ":5000"
logger.Println("Server is running on port" + port)
// Start server on port specified above
logger.Fatal(http.ListenAndServe(port, nil))
}

13
src/go.mod Normal file
View File

@@ -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
)

27
src/go.sum Normal file
View File

@@ -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=

View File

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

View File

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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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")
}

View File

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

View File

@@ -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
}

View File

@@ -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
}
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1,6 @@
package types
type Telemetry struct {
Value float64 `json:"value"`
Unit string `json:"unit"`
}

60
src/main.go Normal file
View File

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

10
waterservice.service Normal file
View File

@@ -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

View File

@@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@@ -1,15 +0,0 @@
module.exports = {
root: true,
extends: ['eslint:recommended', 'prettier'],
plugins: ['svelte3'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
}
};

12
webui/.gitignore vendored
View File

@@ -1,12 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
.vercel
.output
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View File

@@ -1 +0,0 @@
engine-strict=true

View File

@@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@@ -1,9 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@@ -1,11 +0,0 @@
FROM node:latest AS build
WORKDIR /app
COPY package.json ./
COPY package-lock.json ./
RUN npm install
COPY . ./
RUN npm run build
FROM nginx:1.19-alpine
COPY --from=build /app/build /usr/share/nginx/html

View File

@@ -1,38 +0,0 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

61
webui/css/slider.css Normal file
View File

@@ -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%;
}

14
webui/css/style.css Normal file
View File

@@ -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;
}

176
webui/index.html Normal file
View File

@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/slider.css">
<title id="title">Wasser</title>
<script type="text/javaScript">
function init() {
get_devices();
get_states();
get_battery_level();
get_moisture_level();
setInterval(poll, 1000);
get_moisture_level();
}
function poll() {
get_states();
get_battery_level();
get_moisture_level();
}
function get_moisture_level() {
const xhr = new XMLHttpRequest();
xhr.open("GET", "/soil/moisture");
xhr.send();
xhr.responseType = "json";
xhr.onload = () => {
if (xhr.readyState == 4 && xhr.status == 200) {
let result = xhr.response
let str = result.value.toFixed(1) + " " + result.unit
let cell = document.getElementById("soil_moisture");
cell.innerText = str;
} else {
console.log(`Error: ${xhr.status}`);
}
};
}
function get_battery_level() {
const xhr = new XMLHttpRequest();
xhr.open("GET", "/battery/stateofcharge");
xhr.send();
xhr.responseType = "json";
xhr.onload = () => {
if (xhr.readyState == 4 && xhr.status == 200) {
let result = xhr.response
let str = result.value.toFixed(1) + " " + result.unit
let cell = document.getElementById("battery_level");
cell.innerText = str;
} else {
console.log(`Error: ${xhr.status}`);
}
};
}
function get_states() {
const xhr = new XMLHttpRequest();
xhr.open("GET", "/state");
xhr.send();
xhr.responseType = "json";
xhr.onload = () => {
if (xhr.readyState == 4 && xhr.status == 200) {
let states = xhr.response
for(let i in states) {
let btn = document.getElementById(states[i].name);
let state = states[i].state;
let chk = false;
if(state === "on") {
chk = true;
}
btn.checked = chk;
let runtime_cell = document.getElementById("runtime" + states[i].name);
const millis = states[i].runtime / 1000000;
const res = new Date(millis).toISOString().slice(11, 19);
runtime_cell.innerHTML = res;
}
} else {
console.log(`Error: ${xhr.status}`);
}
};
}
function get_devices() {
const xhr = new XMLHttpRequest();
xhr.open("GET", "/devices");
xhr.send();
xhr.responseType = "json";
xhr.onload = () => {
if (xhr.readyState == 4 && xhr.status == 200) {
let devices = xhr.response['devices'];
let table = document.getElementById("devices_table");
for(let i = 0; i < devices.length; i++) {
// insert per device row into devices table
let new_row = table.insertRow();
// first row is name
let name_cell = new_row.insertCell();
name_cell.innerHTML = devices[i].name;
// create slider switch
let input = document.createElement("input");
input.setAttribute("type", "checkbox");
input.setAttribute("id", devices[i].name);
input.onchange = function(){check(this, devices[i].name)};
let span = document.createElement("span");
span.className = "slider round";
let btn = document.createElement("label");
btn.className = "switch";
btn.appendChild(input);
btn.appendChild(span);
let button_cell = new_row.insertCell();
button_cell.appendChild(btn);
// add runtime cell
let runtime_cell = new_row.insertCell();
runtime_cell.setAttribute("id", "runtime" + devices[i].name);
}
} else {
console.log(`Error: ${xhr.status}`);
}
};
}
function check(checkbox, name) {
var obj;
console.log("name: ", name);
if(checkbox.checked) {
obj = '{"name":"' + name + '","state":"on"}'
} else {
obj = '{"name":"' + name + '","state":"off"}'
}
console.log(obj);
const xhr = new XMLHttpRequest();
xhr.open("PATCH", "/state");
xhr.setRequestHeader("Content-type", "application/json; charset=utf-8");
// xhr.onload = () => {
// var data = JSON.parse(xhr.responseText);
// if (xhr.readyState == 4 && xhr.status == "202") {
// console.log(data);
// } else {
// console.log(`Error: ${xhr.status}`);
// }
// };
xhr.send(obj);
}
</script>
</head>
<body onload=init()>
<div class="headercontainer">
<!-- <div class="meta">
<div class="left" style="line-height: 24px;font-weight: bold;" id="headline">Wasser</div>
<div class="middle"></div>
<div class="right"><span></span></div>
</div> -->
<!-- <div class="header" id="header_cnt">
<div class="headerlogo"><img src="images/logo_perinet.png" width="258" height="94" alt="" /></div>
</div> -->
</div>
<div id="content">
<h1>Photovoltaik</h1>
<table>
<tr>
<td>Batterielevel:</td>
<td id="battery_level"></td>
</tr>
</table>
<h1>Bodenfeuchtigkeit</h1>
<table>
<tr>
<td>Bodenfeuchtigkeit:</td>
<td id="soil_moisture"></td>
</tr>
</table>
<h1>Rasensprenger</h1>
<table id="devices_table"></table>
</div>
</body>
</html>

View File

@@ -1,17 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

4979
webui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +0,0 @@
{
"name": "home",
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"test": "playwright test",
"test:unit": "vitest",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@fontsource/fira-mono": "^4.5.10",
"@neoconfetti/svelte": "^1.0.0",
"@playwright/test": "^1.28.1",
"@sveltejs/adapter-auto": "^1.0.0",
"@sveltejs/adapter-node": "^1.1.4",
"@sveltejs/adapter-static": "^1.0.5",
"@sveltejs/kit": "^1.0.0",
"@types/cookie": "^0.5.1",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"svelte": "^3.54.0",
"svelte-check": "^3.0.1",
"typescript": "^4.9.3",
"vite": "^4.0.4",
"vitest": "^0.25.3"
},
"type": "module"
}

View File

@@ -1,10 +0,0 @@
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'tests'
};
export default config;

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- <link rel="icon" href="%sveltekit.assets%/haus.svg"/> -->
<meta name="viewport" content="width=device-width"/>
%sveltekit.head%
</head>
<body>
<div>%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,566 +0,0 @@
html,
body {
margin: 0;
padding: 0;
font-size: 18px !important;
font-family: 'Muli', arial, verdana, helvetica, sans-serif;
font-weight: 300;
font-style: normal;
height: 100%;
color: #b6b6b6;
background: #282929;
}
body {
display: flex;
flex-direction: column;
}
.modalbg {
background: rgba(0, 0, 0, 0.6);
height: 100%;
width: 100%;
position: fixed;
top: 0px;
z-index: 999;
overflow-x: hidden;
overflow-y: auto;
color: white;
}
a {
outline: none;
color: #b6b6b6;
text-decoration-line: none;
}
li {
list-style: none;
}
.content {
padding: 10rem 0;
margin: 2%;
}
section {
margin: auto 0;
align-items: center;
text-align: center;
}
section h1 {
margin: 20px auto;
font-size: 2rem;
}
section h2 {
font-size: 1.4rem;
}
table {
margin: auto;
}
td p {
margin: 0;
}
td.left {
text-align: right;
font-weight: 700;
width: 33%;
padding: 5px;
vertical-align: text-top;
}
td.right {
text-align: left;
}
td.input {
text-align: left;
width: 65%;
outline: none;
vertical-align: text-top;
}
form {
font-size: 1.2rem;
}
td select {
width: 51%;
outline: none;
width: 62%;
outline: none;
margin-left: 2%;
}
td input {
width: 60%;
outline: none;
}
td textarea {
text-align: left;
}
td input span {
font-size: 1.0rem;
outline: none;
border: none;
background: none;
}
select,
input {
border: 1px #999999 solid;
background-color: #f6f6f6;
padding: 2px 4px 2px 4px;
margin: 0 0 0.5rem 0;
color: #b6b6b6 !important;
font-size: 1.0rem;
border-radius: 8px;
outline: none;
}
td div input {
color: #b6b6b6 !important;
border: none;
background: none;
}
textarea {
border-radius: 10px;
width: 98%;
height: 10rem;
position: relative;
resize: none;
outline: none;
margin-left: 2%;
}
header {
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
background-color: #282929;
}
/*logo placement*/
header figure {
float: left;
color: #b6b6b6;
}
header figure.logo {
position: absolute;
right: 0px;
top: -15px;
}
/*Navigation*/
.header {
background-color: #282929;
/*position: fixed;*/
width: 100%;
z-index: 510;
}
.header ul {
margin: 0;
padding: 0;
list-style: none;
overflow: hidden;
/* background-color: #282929; */
background-color: transparent;
}
.header li a {
display: block;
padding: 30px 5px 10px 5px;
font-size: 1.5rem;
border-right: 1px solid #282929;
text-decoration: none;
/* background: rgba(255, 255, 255, 0.9) */
background-color: #282929;
}
.header li a:hover,
.header .menu-btn:hover {
background-color: #282929;
}
.header .logo {
display: block;
float: left;
font-size: 2em;
padding: 5px;
text-decoration: none;
}
/* menu */
.header .menu {
clear: both;
max-height: 0;
transition: max-height .2s ease-out;
}
/* menu icon */
.header .menu-icon {
cursor: pointer;
float: right;
margin: 2%;
padding: 10px 20px;
position: relative;
user-select: none;
}
.header .menu-icon .nav-icon {
background: #000;
display: block;
height: 5px;
position: relative;
transition: background .2s ease-out;
width: 30px;
}
.header .menu-icon .nav-icon:before,
.header .menu-icon .nav-icon:after {
background: #000;
content: '';
display: block;
height: 100%;
position: absolute;
transition: all .2s ease-out;
width: 100%;
}
.header .menu-icon .nav-icon:before {
top: 20px;
}
.header .menu-icon .nav-icon:after {
top: 10px;
}
/* menu btn */
.header .menu-btn {
display: none;
}
.header .menu-btn:checked~.menu {
max-height: 1200px;
}
.header .menu-btn:checked~.menu-icon .nav-icon {
background: transparent;
}
.header .menu-btn:checked~.menu-icon .nav-icon:before {
transform: rotate(-45deg);
top: 0;
}
.header .menu-btn:checked~.menu-icon .nav-icon:after {
transform: rotate(45deg);
top: 0;
}
/* menu content - larger screen*/
@media (min-width: 1132px) {
.header li {
float: left;
}
.header li a {
padding: 20px 15px 5px 0px;
font-size: 1rem;
border: none;
}
.header .menu {
clear: none;
float: right;
max-height: none;
}
.header .menu-icon {
display: none;
}
}
/* smaller screen*/
@media (max-width: 924px) {
.header li {
padding: 30px 5px 10px 5px;
}
.header li a {
font-size: 1.2rem;
}
}
#overlay {
position: absolute;
display: none;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999;
background-color: rgba(0, 0, 0, 0.5);
/*dim the background*/
}
.update-container {
display: flex;
align-items: center;
justify-content: center;
}
#update-plain {
position: relative;
}
#update-overlay {
position: absolute;
z-index: 502;
transform-origin: 50% 50%;
transition: transform 1.00s;
}
address {
text-align: center;
margin-top: 5px;
}
address table {
margin: 0 auto;
width: 100%;
font-style: normal;
}
address td {
width: 50%;
}
input[type="submit" i] {
margin: 0px 0 6px 0;
vertical-align: middle;
position: relative;
font-size: 16px;
font-weight: bold;
width: auto;
height: 45px;
z-index: 200;
width: 65%;
outline: none;
}
input[type="submit"] {
border-radius: 30px;
background: #282929;
border: 1px solid #d2d2d2;
-webkit-transition: all 1s;
transition: all 1s;
margin-top: 2%;
margin-left: 2%;
outline: none;
z-index: auto;
}
input[type="submit" i]:hover {
background-color: #f6f6f6;
color: #282929;
outline: none;
}
input[type="file"] {
border-radius: 10px;
margin: 1%;
width: 390px;
margin-left: 45px;
margin-right: 28px;
outline: none;
}
input {
border-radius: 10px;
background: #282929;
margin-left: 2%;
outline: none;
}
.confi table td.input {
padding: 1px 0px 10px 0px;
outline: none;
}
.confi table td.left {
padding: 0px 25px 0px 0px;
}
.content #uploadbutton {
width: 400px;
outline: none;
}
footer {
background-color: #f6f6f6;
padding: 1px 50px 1px 0px;
text-align: center;
text-decoration: none;
font-size: 0.8rem;
bottom: 0;
width: 100%;
position: fixed;
}
footer nav :hover {
color: #b6b6b6;
font-weight: 600;
}
footer td p {
text-align: left;
padding-left: 40%;
}
/* upload loader animation*/
.loader {
display: flex;
justify-content: center;
align-items: center;
width: 100px;
height: 100px;
background: transparent;
margin: 30px auto 0 auto;
border: solid 10px #b6b6b6;
border-top: solid 10px #c2d100;
border-radius: 50%;
opacity: 0;
}
.loader.active {
animation: loading 4s ease-in-out;
animation-fill-mode: forwards;
}
@keyframes loading {
5% {
opacity: 1;
border-color: #b6b6b6;
}
15% {
opacity: 1;
border-color: #c2d100;
}
30% {
opacity: 1;
transform: rotate(180deg);
border-color: #b6b6b6;
}
45% {
opacity: 1;
border-color: #c2d100;
}
60% {
opacity: 1;
transform: rotate(180deg);
border-color: #b6b6b6;
}
70% {
opacity: 1;
border-color: #c2d100;
}
85% {
opacity: 1;
transform: rotate(180deg);
border-color: #c2d100;
}
90% {
opacity: 1;
transform: rotate(1080deg);
border-color: #c2d100;
}
99% {
opacity: 1;
transform: rotate(1080deg);
border-color: #c2d100;
}
100% {
opacity: 1;
transform: rotate(1080deg);
border-color: #282929;
}
}
/*Chrome runable slider*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
input[type='range'] {
overflow: hidden;
/* Standard */
width: calc(99.9% - 1.5px);
-webkit-appearance: none;
background-color: transparent;
margin: 0 0 0;
}
input[type='range']::-webkit-slider-runnable-track {
height: fit-content;
/* -webkit-appearance: none; */
color: #C2D100;
/* margin-top: -5px; */
}
input[type='range']::-webkit-slider-thumb {
width: 20px;
-webkit-appearance: none;
height: 20px;
cursor: ew-resize;
border-radius: 5px;
background: #292659;
box-shadow: -1080px 10px 10px 1080px #C1D10A;
}
input:focus {
outline: none;
border-color: #C1D10A;
}
}
/** FF*/
input[type="range"]::-moz-range-progress {
background-color: #C1D10A;
}
input[type="range"]::-moz-range-track {
background-color: transparent;
}
/* IE*/
input[type="range"]::-ms-fill-lower {
background-color: #C1D10A;
}
input[type="range"]::-ms-fill-upper {
background-color: #C1D10A;
}

View File

@@ -1,14 +0,0 @@
export const handle = async ({ event, resolve }) => {
let userid = event.cookies.get('userid');
if (!userid) {
// if this is the first time the user has visited this app,
// set a cookie so that we recognise them when they return
userid = crypto.randomUUID();
event.cookies.set('userid', userid, { path: '/' });
}
event.locals.userid = userid;
return resolve(event);
};

View File

@@ -1,21 +0,0 @@
<script>
import favicon from '$lib/images/haus.svg'
</script>
<header class="header">
<!-- <a href="https://www.perinet.io" target="_blank" class="logo">
<img src={perinetLogo} alt="Perinet Logo" width="180">
</a> -->
<link rel="shortcut icon" type="image/svg" href={favicon} />
<input class="menu-btn" type="checkbox" id="menu-btn" />
<label class="menu-icon" for="menu-btn"><span class="nav-icon"></span></label>
<ul class="menu">
<li><a href="/">Home</a></li>
<li><a href="/sauna">Sauna</a></li>
<!-- <li><a href="/chicken">Chicken</a></li> -->
</ul>
</header>

View File

@@ -1,79 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1280.000000pt" height="1210.000000pt" viewBox="0 0 1280.000000 1210.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.15, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,1210.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M11345 12075 c-49 -33 -58 -44 -146 -173 -39 -57 -77 -106 -85 -109
-27 -10 -63 7 -141 69 -121 96 -179 128 -233 128 -98 0 -285 -84 -384 -172
-79 -70 -152 -176 -166 -241 -23 -110 22 -257 116 -373 l33 -41 -82 -7 c-331
-28 -656 -105 -851 -203 -314 -157 -535 -430 -726 -893 -73 -179 -139 -389
-235 -750 -105 -391 -159 -559 -229 -705 -102 -213 -292 -470 -496 -675 -155
-155 -294 -265 -550 -435 -640 -426 -839 -521 -1245 -596 -211 -40 -362 -31
-623 37 -333 86 -415 126 -722 350 -415 303 -681 451 -1005 559 -264 88 -490
121 -770 112 -536 -19 -1152 -196 -1560 -450 -532 -330 -957 -995 -1129 -1762
-37 -167 -54 -286 -83 -591 -36 -383 -38 -796 -5 -1012 35 -222 114 -439 255
-702 53 -100 75 -129 167 -221 171 -171 395 -307 579 -349 91 -21 457 -64 495
-58 16 3 13 10 -21 53 -22 28 -102 115 -178 195 -304 320 -424 526 -519 891
-33 125 -46 221 -72 519 l-6 75 72 -140 c149 -290 218 -384 305 -421 104 -43
165 9 219 188 26 85 31 116 60 365 20 165 35 190 36 58 2 -343 21 -830 35
-915 20 -123 77 -301 118 -370 95 -158 210 -120 281 92 13 40 52 233 85 428
189 1104 245 1410 261 1439 24 46 33 23 26 -66 -8 -90 -18 -520 -16 -653 1
-41 9 -97 18 -125 19 -57 39 -65 104 -46 49 15 142 101 161 150 9 21 62 212
117 423 118 446 184 676 210 733 l18 40 1 -45 c1 -57 -22 -155 -99 -410 -112
-375 -145 -530 -156 -740 -17 -335 71 -805 187 -993 111 -180 323 -392 481
-481 114 -63 258 -84 258 -36 0 27 -60 214 -134 420 -106 295 -129 389 -151
630 -27 283 -17 718 21 945 34 205 186 705 213 705 9 0 -3 -116 -23 -210 -46
-229 -67 -495 -67 -870 0 -562 54 -984 161 -1257 29 -74 56 -122 63 -112 2 2
26 96 54 209 49 202 94 329 136 391 25 36 74 76 82 67 4 -3 13 -70 21 -149 19
-185 39 -290 71 -362 50 -112 234 -253 424 -324 91 -34 242 -63 256 -50 4 4
-14 94 -40 200 -60 248 -72 354 -51 455 5 23 6 22 34 -17 16 -22 49 -51 72
-63 38 -20 58 -23 163 -23 l120 0 58 -111 c63 -121 78 -134 151 -134 34 0 60
11 127 51 125 76 117 76 191 17 99 -80 135 -93 253 -92 105 1 161 15 216 55
17 12 31 20 33 18 1 -2 15 -33 29 -68 66 -153 185 -323 326 -461 97 -94 199
-167 223 -157 7 2 49 91 92 196 l79 192 22 -27 c12 -15 53 -83 90 -151 80
-144 129 -197 288 -310 59 -42 107 -80 107 -83 0 -4 -16 -116 -35 -250 -19
-134 -35 -244 -35 -245 0 -1 -36 8 -80 21 -103 30 -177 42 -320 53 -87 6 -133
15 -190 37 -65 24 -89 28 -185 28 -100 1 -115 -1 -165 -26 -30 -15 -74 -47
-98 -71 l-43 -44 75 -5 c58 -4 90 -12 141 -37 72 -36 105 -72 157 -175 62
-122 72 -129 226 -146 72 -8 124 -22 222 -60 71 -28 134 -50 140 -50 47 0 -99
-104 -190 -135 -105 -36 -238 -107 -397 -214 -159 -106 -239 -181 -291 -273
-82 -144 -96 -325 -35 -449 18 -35 45 -77 62 -93 l30 -28 45 89 c71 142 108
176 226 203 111 26 228 81 357 167 45 31 87 52 93 48 6 -4 32 -39 57 -79 74
-117 114 -163 193 -229 136 -114 233 -167 306 -167 14 0 44 -15 67 -34 77 -62
90 -65 248 -63 126 2 151 -1 202 -20 73 -28 175 -95 260 -174 37 -34 70 -59
74 -56 3 4 6 73 7 154 1 138 -1 153 -27 228 -19 53 -25 81 -17 83 13 5 195
125 252 167 34 25 39 26 74 15 l37 -13 60 67 c176 197 266 160 575 -233 62
-79 130 -156 150 -171 91 -66 140 -152 163 -287 6 -37 13 -69 14 -71 5 -6 148
29 173 42 42 22 108 98 123 142 21 62 16 143 -15 228 -16 42 -32 79 -37 82
-21 13 -7 23 19 13 15 -6 83 -24 151 -40 209 -50 213 -55 246 -282 9 -62 18
-114 20 -116 2 -2 52 23 111 55 71 39 113 69 125 89 46 75 64 293 34 408 -40
152 -137 262 -311 350 -29 15 -132 54 -229 87 -202 69 -238 86 -361 168 -134
90 -174 135 -203 223 -13 41 -39 108 -58 149 -26 58 -42 119 -69 265 -40 220
-85 430 -114 529 l-20 69 98 93 c257 242 366 531 395 1044 l2 44 190 164 c479
414 733 657 1026 982 418 464 613 726 765 1025 115 228 163 396 211 735 25
178 24 730 0 950 -46 400 -108 725 -222 1154 -25 92 -45 175 -45 183 0 9 14
31 30 50 46 51 101 169 171 363 l63 175 2 240 c0 187 4 256 18 315 9 41 19 84
21 95 4 21 5 21 167 -7 90 -15 190 -27 223 -28 72 0 167 23 205 50 l27 19 102
-69 101 -68 17 31 c11 21 17 55 17 97 0 58 -5 76 -51 170 -61 125 -106 187
-215 297 l-81 83 38 82 c38 81 39 87 43 212 5 159 -9 247 -54 342 -36 76 -109
166 -155 189 -38 20 -125 19 -244 -4 -57 -11 -104 -16 -109 -11 -5 5 7 45 28
94 59 137 50 223 -26 262 -62 32 -143 4 -293 -100 -44 -31 -85 -56 -92 -56 -9
0 -12 37 -10 153 2 140 0 155 -20 194 -28 52 -69 82 -125 89 -36 5 -50 1 -83
-21z m-2875 -9705 c30 -183 56 -273 113 -390 28 -58 52 -107 54 -110 1 -3 -29
-20 -68 -38 -206 -97 -249 -254 -143 -519 l29 -72 50 56 c54 62 122 99 201
110 45 6 48 5 35 -10 -7 -9 -64 -55 -126 -102 -129 -97 -174 -143 -211 -216
l-26 -52 -31 57 c-49 92 -198 221 -336 291 -73 37 -137 55 -193 55 -27 0 -48
2 -48 5 0 2 12 40 26 82 31 92 29 174 -5 241 l-21 42 21 62 c11 35 30 135 41
223 11 88 21 160 22 161 0 0 58 19 129 42 141 47 310 120 402 175 33 19 61 34
62 33 1 -1 11 -58 23 -126z m-454 -1530 c38 -17 106 -38 151 -45 75 -12 143
-43 143 -63 0 -7 -16 -115 -26 -170 l-5 -34 -31 46 c-16 25 -57 71 -91 101
-53 48 -66 55 -102 55 -53 0 -180 63 -209 103 -18 24 -19 29 -6 38 24 16 103
2 176 -31z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -1,29 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1280.000000pt" height="1210.000000pt" viewBox="0 0 1280.000000 1210.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.15, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,1210.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M5699 11502 c-371 -328 -1786 -1580 -3144 -2782 -1359 -1201 -2489
-2203 -2512 -2225 l-41 -40 329 -370 c181 -203 333 -371 338 -373 6 -2 949
828 2098 1844 1148 1017 2434 2154 2857 2529 423 374 773 680 777 680 5 0
1291 -1135 2859 -2523 1568 -1388 2857 -2525 2864 -2528 7 -3 122 119 267 283
140 158 289 327 332 375 l78 87 -1038 918 c-571 505 -1882 1665 -2913 2578
-1031 912 -1998 1768 -2149 1902 -214 190 -277 240 -288 232 -9 -8 -17 -8 -26
-1 -10 8 -171 -129 -688 -586z"/>
<path d="M1910 10089 l0 -1201 108 98 c59 53 418 378 798 721 l691 623 6 347
c4 190 7 406 7 480 l0 133 -805 0 -805 0 0 -1201z"/>
<path d="M4875 8574 c-836 -740 -1845 -1632 -2242 -1982 l-722 -637 0 -2875
c-1 -2870 -1 -2875 20 -2920 29 -64 84 -117 146 -140 53 -20 80 -20 1478 -20
l1425 0 2 1277 c3 1272 3 1278 24 1323 26 58 76 108 134 134 45 21 49 21 1260
21 1211 0 1215 0 1260 -21 58 -26 108 -76 134 -134 21 -45 21 -51 24 -1322 l2
-1278 1425 0 c1398 0 1425 0 1478 20 62 23 117 76 146 140 21 45 21 51 20
2920 l0 2875 -831 735 c-457 404 -1360 1202 -2007 1775 -1658 1467 -1645 1455
-1651 1455 -3 0 -689 -606 -1525 -1346z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 485.252 485.252" xml:space="preserve">
<g id="XMLID_288_">
<path id="XMLID_289_" d="M315.478,319.287c-7.563-11.352-20.3-18.164-33.928-18.164h-81.497l-1.975-20.936l65.72,9.052
c1.497,0.205,2.993,0.301,4.49,0.301c16.032,0,30.012-11.829,32.255-28.154c2.451-17.84-10.014-34.295-27.859-36.753l-67.87-9.347
l-14.95-22.24l-2.626-27.853c-2.118-22.402-22.131-39.03-44.403-36.745c-22.417,2.109-38.863,21.994-36.762,44.402l16.303,172.857
c1.975,20.936,19.553,36.93,40.582,36.93h96.766l56.248,84.441c7.865,11.79,20.792,18.174,33.958,18.174
c7.771,0,15.62-2.221,22.56-6.846c18.738-12.475,23.803-37.779,11.336-56.51L315.478,319.287z"/>
<path id="XMLID_290_" d="M163.643,108.688c30.01,0,54.337-24.333,54.337-54.346C217.98,24.333,193.653,0,163.643,0
c-30.026,0-54.354,24.333-54.354,54.343C109.29,84.355,133.617,108.688,163.643,108.688z"/>
<path id="XMLID_291_" d="M222.741,406.453H108.063L86.762,236.356c-1.115-8.924-9.17-15.292-18.197-14.153
c-8.932,1.115-15.269,9.266-14.154,18.197l23.085,184.377c1.019,8.159,7.961,14.281,16.176,14.281h129.069
c9.01,0,16.303-7.301,16.303-16.303C239.044,413.752,231.751,406.453,222.741,406.453z"/>
<path id="XMLID_292_" d="M406.618,319.467c0-21.502,24.35-30.94,24.35-61.759c0-20.446-15.583-30.286-26.073-34.594
c-2.808-1.152-5.521,1.896-4.014,4.531c3.606,6.301,7.722,16.468,7.722,30.063c0,24.186-22.58,27.358-22.58,61.759
c0,22.987,15.241,34.074,25.869,39.012c2.855,1.327,5.712-1.857,4.087-4.555C411.674,346.781,406.618,335.071,406.618,319.467z"/>
<path id="XMLID_293_" d="M369.124,249.696c2.855,1.326,5.712-1.857,4.086-4.555c-4.305-7.142-9.361-18.853-9.361-34.457
c0-21.502,24.352-30.941,24.352-61.759c0-20.445-15.583-30.286-26.073-34.593c-2.808-1.153-5.522,1.896-4.015,4.53
c3.607,6.301,7.723,16.468,7.723,30.063c0,24.187-22.58,27.357-22.58,61.759C343.254,233.672,358.496,244.76,369.124,249.696z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,13 +0,0 @@
<script>
import Header from '$lib/Header.svelte';
// import Footer from '$lib/Footer.svelte';
import '../css/style.css'
</script>
<Header />
<main>
<slot />
</main>
<!-- <Footer/> -->

View File

@@ -1,2 +0,0 @@
export const prerender = true;
export const trailingSlash = 'always';

View File

@@ -1,15 +0,0 @@
<script>
import icon from "$lib/images/haus.svg"
</script>
<svelte:head>
<title>Home</title>
<meta name="description" content="Home"/>
</svelte:head>
<section id='content_id' class='content'>
<h1>Home</h1>
<figure>
<img src={icon} alt="Home" width=150/>
</figure>
</section>

View File

@@ -1,2 +0,0 @@
export const prerender = true;
export const trailingSlash = 'always';

View File

@@ -1,152 +0,0 @@
<script>
import { bind, onMount } from "svelte/internal";
import icon from "$lib/images/hahn.svg"
// base api url
let backend_url = "http://localhost:5000/gates";
let value_state_outer = "";
let value_open_times_outer = "";
let value_close_times_outer = "";
let options_state = [
{id: 1, text: "Schließen" }, // means "is open"
{id: 2, text: "Öffnen" },
{id: 3, text: "Schließt..." },
{id: 4, text: "Öffnet..." },
];
let options_open = [
{id: 1, text: "Sonnenaufgang"},
{id: 2, text: "08:00"},
{id: 3, text: "09:00"},
{id: 4, text: "10:00"},
{id: 5, text: "11:00"},
{id: 6, text: "12:00"}
];
let options_close = [
{id: 1, text: "Sonnenuntergang"},
{id: 2, text: "Sonnenuntergang + 30min"},
{id: 3, text: "16:00"},
{id: 4, text: "16:30"},
{id: 5, text: "17:00"},
{id: 6, text: "17:30"},
{id: 7, text: "18:00"},
{id: 8, text: "18:30"},
{id: 9, text: "19:00"},
{id: 10, text: "19:30"},
{id: 11, text: "20:00"},
{id: 12, text: "20:30"},
{id: 13, text: "21:00"},
{id: 14, text: "21:30"},
{id: 15, text: "22:00"},
];
function get_next_state(state) {
if(state == 1) {
return 2;
}
if(state == 2) {
return 1;
}
}
function handle_button_outer(event) {
console.log(value_state_outer)
for( var i = 0; i < options_state.length; i++) {
if(options_state[i].text == value_state_outer) {
fetch(backend_url + "/outer", {
method: "PATCH",
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({
outer_gate: {state: get_next_state(options_state[i].id)}
})
})
.then(response => response.json())
// .then(result => console.log(result))
break;
}
}
}
function handle_open_times_outer() {
alert(value_open_times_outer);
}
function handle_close_times_outer() {
alert(value_close_times_outer);
}
function set_state_outer(index) {
for( var i = 0; i < options_state.length; i++) {
if(options_state[i].id == index) {
value_state_outer = options_state[i].text;
break;
}
}
}
function get_gates() {
fetch(backend_url)
.then(response => response.json())
.then(data => {
set_state_outer(data.outer_gate.state)
}).catch(error => {
console.log(error);
return [];
});
}
setInterval(() => {
get_gates();
}, 1000)
onMount(async () => {
get_gates();
});
</script>
<svelte:head>
<title>Chicken</title>
<meta name="description" content="Chicken"/>
</svelte:head>
<section id='content_id' class='content'>
<h1>Chicken</h1>
<figure>
<img src={icon} alt="Chicken" width=150/>
</figure>
<hr>
<table>
<tr>
<td class="left">
<h2>Außenklappe</h2>
</td>
<td>
<input type="submit" enabled id="button_outer" value={value_state_outer} style="margin-left:55px;width:200px" on:click={(event) => handle_button_outer(event)}/>
</td>
</tr>
<tr>
<td class="left">Öffnen:</td>
<td class="input">
<select style="margin-left:55px;width:250px;" bind:value={value_open_times_outer} on:change={handle_open_times_outer}>
{#each options_open as option}
<option value={option}>{option.text}</option>
{/each}
</select>
</td>
</tr>
<tr>
<td class="left">Schließen:</td>
<td class="input">
<select style="margin-left:55px;width:250px;" bind:value={value_close_times_outer} on:change={handle_close_times_outer}>
{#each options_close as option}
<option value={option}>{option.text}</option>
{/each}
</select>
</td>
</tr>
</table>
<hr>
</section>

View File

@@ -1,2 +0,0 @@
export const prerender = true;
export const trailingSlash = 'always';

View File

@@ -1,42 +0,0 @@
<script>
import { onMount } from "../../../node_modules/svelte/internal";
import icon from "$lib/images/sauna.svg"
let backend_url = "https://home.blackfinn.de/sauna/sample";
let temperature_value = 0.0
let temperature_unit = "°C"
function get_temperature() {
fetch(backend_url)
.then(response => response.json())
.then(data => {
temperature_unit = data.unit
temperature_value = data.value
}).catch(error => {
console.log(error);
return [];
});
}
setInterval(() => {
get_temperature();
}, 1000)
onMount(async () => {
get_temperature();
});
</script>
<svelte:head>
<title>Sauna</title>
<meta name="description" content="Sauna"/>
</svelte:head>
<section id='content_id' class='content'>
<h1>Sauna</h1>
<figure>
<img src={icon} alt="Sauna" width=150/>
</figure>
<h1>{temperature_value} {temperature_unit}</h1>
</section>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -1,19 +0,0 @@
//import adapter from '@sveltejs/adapter-auto';
// import adapter from '@sveltejs/adapter-node';
import adapter from '@sveltejs/adapter-static';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
// default options are shown. On some platforms
// these options are set automatically — see below
pages: '../build/webui',
assets: '../build/webui',
fallback: null,
precompress: false,
strict: true
})
}
};
export default config;

View File

@@ -1,6 +0,0 @@
import { expect, test } from '@playwright/test';
test('about page has expected h1', async ({ page }) => {
await page.goto('/about');
await expect(page.locator('h1')).toHaveText('About this app');
});

View File

@@ -1,11 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
/** @type {import('vite').UserConfig} */
const config = {
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
}
};
export default config;