diff --git a/.gitignore b/.gitignore index 2b1ac1a..986f939 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ .DS_Store __debug_bin + +bin/ +homeservice.tar.gz diff --git a/.vscode/launch.json b/.vscode/launch.json index 34dafd5..41b23e7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "mode": "auto", "program": "${workspaceFolder}/main.go", - "preLaunchTask": "npm: build - svelte" + "preLaunchTask": "npm: build - webui" } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7492294..f093e81 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,10 +4,10 @@ { "type": "npm", "script": "build", - "path": "svelte", + "path": "webui", "group": "build", "problemMatcher": [], - "label": "npm: build - svelte", + "label": "npm: build - webui", "detail": "vite build" } ] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..05eb4de --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +PROJECT_NAME := homeservice + +PREFIX ?= /usr/bin + +CONFIG_DIR := /etc/$(PROJECT_NAME) +SYSTEM_DIR := /usr/lib/systemd/system +WEB_DIR := /var/lib/$(PROJECT_NAME) + +CONFIG_FILE := config/config.json +BIN_FILE := bin/$(PROJECT_NAME) +UNIT_FILE := $(PROJECT_NAME).service +README_FILE := README.md + +.PHONY: all +all: webui service + +.PHONY: service +service: + mkdir -p bin + go clean + go mod tidy + go build -o $(BIN_FILE) + +.PHONY: webui +webui: + npm install --prefix webui + npm run build --prefix webui + +.PHONY: clean +clean: + go clean + rm -rf bin + rm -rf webui/build webui/node_modules webui/.svelte-kit + -rm $(PROJECT_NAME).tar.gz + +.PHONY: install +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 + install -d $(PREFIX) + install -m 0755 $(BIN_FILE) $(PREFIX) + + # System unit + install -d $(SYSTEM_DIR) + install -m 0644 $(UNIT_FILE) $(SYSTEM_DIR) + + # Web ui + install -d $(WEB_DIR) + cp -r webui/build/* $(WEB_DIR) + +.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/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/config/config.json b/config/config.json index d33e287..e69de29 100644 --- a/config/config.json +++ b/config/config.json @@ -1,12 +0,0 @@ -{ - "outer_gate": { - "state": 1, - "open_time": 4, - "close_time": 2 - }, - "inner_gate": { - "state": 1, - "open_time": 2, - "close_time": 2 - } -} \ No newline at end of file diff --git a/go.mod b/go.mod index 43d5b3d..4ced99d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ 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.0.0-20200425230154-ff2c4b7c35a0 // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect +) diff --git a/go.sum b/go.sum index e69de29..ddd8ce6 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,12 @@ +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 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +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= diff --git a/homeservice.service b/homeservice.service new file mode 100644 index 0000000..247549f --- /dev/null +++ b/homeservice.service @@ -0,0 +1,10 @@ +[Unit] +Description=homeservice service +After=multi-user.target + +[Service] +Type=idle +ExecStart=/usr/bin/homeservice -c /etc/homeservice/config.json + +[Install] +WantedBy=multi-user.target diff --git a/main.go b/main.go index f29735d..822e8c0 100644 --- a/main.go +++ b/main.go @@ -2,144 +2,107 @@ package main import ( "encoding/json" - "flag" - "fmt" - "io" "log" + "mime" "net/http" - "os" + "sync" + + mqtt "github.com/eclipse/paho.mqtt.golang" ) -const ( - OPENED uint = 1 - CLOSED uint = 2 - CLOSING uint = 3 - OPENING uint = 4 -) - -type gate struct { - State uint `json:"state"` - OpenTime uint `json:"open_time"` - CloseTime uint `json:"close_time"` -} - -type config struct { - InnerGate gate `json:"inner_gate"` - OuterGate gate `json:"outer_gate"` +type temperature struct { + Value float64 `json:"value"` + Unit string `json:"unit"` } var ( - logger log.Logger = *log.Default() - config_path string + logger log.Logger = *log.Default() - cache_inner_gate = gate{ - State: 1, - OpenTime: 2, - CloseTime: 2, - } - - cache_outer_gate = gate{ - State: 1, - OpenTime: 4, - CloseTime: 2, - } - - config_cache = config{ - InnerGate: cache_inner_gate, - OuterGate: cache_outer_gate, + sauna_mutex sync.Mutex + sauna_temperature = temperature{ + Value: 0.0, + Unit: "°C", } ) -func read_config() { - data, err := os.ReadFile(config_path) +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.Printf("Unable to read %s", config_path) - return - } - err = json.Unmarshal(data, &config_cache) - if err != nil { - logger.Print("Unable to evaluate config data") + logger.Print(err) return } } -func api_handler_gates(w http.ResponseWriter, r *http.Request) { +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 == "GET" { - res, err := json.Marshal(config_cache) + if r.Method == http.MethodGet { + sauna_mutex.Lock() + data, err := json.Marshal(sauna_temperature) + sauna_mutex.Unlock() if err != nil { - res = json.RawMessage(`{"error": "` + err.Error() + `"}`) w.WriteHeader(http.StatusInternalServerError) - w.Write(res) - return + w.Write(json.RawMessage(`{"error": "cannot marshal object to json"}`)) + } else { + w.WriteHeader(http.StatusOK) + w.Write(data) } - w.WriteHeader(http.StatusOK) - w.Write(res) - } else if r.Method == "PATCH" { - var tmp config - payload, _ := io.ReadAll(r.Body) - err := json.Unmarshal(payload, &tmp) - if err != nil { - logger.Print(err) - } - logger.Print(r.Body) - - } else { - w.WriteHeader(http.StatusMethodNotAllowed) - } -} - -func api_handler_gates_outer(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - res, err := json.Marshal(map[string]uint{"state": config_cache.OuterGate.State}) - if err != nil { - res = json.RawMessage(`{"error": "` + err.Error() + `"}`) - w.WriteHeader(http.StatusInternalServerError) - w.Write(res) - return - } - w.WriteHeader(http.StatusOK) - w.Write(res) - } else if r.Method == "PATCH" { - var tmp config - payload, _ := io.ReadAll(r.Body) - err := json.Unmarshal(payload, &tmp) - if err != nil { - res := json.RawMessage(`{"error": "` + err.Error() + `"}`) - w.WriteHeader(http.StatusInternalServerError) - w.Write(res) - return - } - if tmp.OuterGate.State == 1 { - config_cache.OuterGate.State = 3 // Schliessen => Schliesst - } else if tmp.OuterGate.State == 2 { - config_cache.OuterGate.State = 4 // Oeffnen => Oeffnet - } - // FIXME: add real gate handling - - w.WriteHeader(http.StatusOK) } else { w.WriteHeader(http.StatusMethodNotAllowed) } } func main() { - flag.StringVar(&config_path, "c", "./config/config.json", "Specify path to find the config file. Default is ./config/config.json") - flag.Parse() - read_config() + logger.Println("starting") + // 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) + + mime.AddExtensionType(".js", "text/javascript; charset=utf-8") + mime.AddExtensionType(".css", "text/css; charset=utf-8") // API routes // Serve files from static folder - http.Handle("/", http.FileServer(http.Dir("./svelte/build"))) + http.Handle("/", http.FileServer(http.Dir("/var/lib/home/"))) - // Serve api - http.HandleFunc("/gates", api_handler_gates) - http.HandleFunc("/gates/outer", api_handler_gates_outer) + http.HandleFunc("/sauna/sample", http_endpoint_sauna) port := ":5000" - fmt.Println("Server is running on port" + port) + logger.Println("Server is running on port" + port) // Start server on port specified above - log.Fatal(http.ListenAndServe(port, nil)) + logger.Fatal(http.ListenAndServe(port, nil)) } diff --git a/svelte/.gitignore b/webui/.gitignore similarity index 100% rename from svelte/.gitignore rename to webui/.gitignore diff --git a/svelte/.npmrc b/webui/.npmrc similarity index 100% rename from svelte/.npmrc rename to webui/.npmrc diff --git a/svelte/README.md b/webui/README.md similarity index 100% rename from svelte/README.md rename to webui/README.md diff --git a/svelte/package-lock.json b/webui/package-lock.json similarity index 100% rename from svelte/package-lock.json rename to webui/package-lock.json diff --git a/svelte/package.json b/webui/package.json similarity index 100% rename from svelte/package.json rename to webui/package.json diff --git a/svelte/src/app.html b/webui/src/app.html similarity index 100% rename from svelte/src/app.html rename to webui/src/app.html diff --git a/svelte/src/css/style.css b/webui/src/css/style.css similarity index 100% rename from svelte/src/css/style.css rename to webui/src/css/style.css diff --git a/svelte/src/hooks.server.js b/webui/src/hooks.server.js similarity index 100% rename from svelte/src/hooks.server.js rename to webui/src/hooks.server.js diff --git a/svelte/src/lib/Header.svelte b/webui/src/lib/Header.svelte similarity index 57% rename from svelte/src/lib/Header.svelte rename to webui/src/lib/Header.svelte index 8a2616e..f0d13ca 100644 --- a/svelte/src/lib/Header.svelte +++ b/webui/src/lib/Header.svelte @@ -16,12 +16,7 @@
diff --git a/svelte/src/routes/+layout.svelte b/webui/src/routes/+layout.svelte similarity index 100% rename from svelte/src/routes/+layout.svelte rename to webui/src/routes/+layout.svelte diff --git a/svelte/src/routes/+page.js b/webui/src/routes/+page.js similarity index 100% rename from svelte/src/routes/+page.js rename to webui/src/routes/+page.js diff --git a/svelte/src/routes/+page.svelte b/webui/src/routes/+page.svelte similarity index 100% rename from svelte/src/routes/+page.svelte rename to webui/src/routes/+page.svelte diff --git a/svelte/src/routes/chicken/+page.js b/webui/src/routes/chicken/+page.js similarity index 100% rename from svelte/src/routes/chicken/+page.js rename to webui/src/routes/chicken/+page.js diff --git a/svelte/src/routes/chicken/+page.svelte b/webui/src/routes/chicken/+page.svelte similarity index 100% rename from svelte/src/routes/chicken/+page.svelte rename to webui/src/routes/chicken/+page.svelte diff --git a/webui/src/routes/sauna/+page.js b/webui/src/routes/sauna/+page.js new file mode 100644 index 0000000..189f71e --- /dev/null +++ b/webui/src/routes/sauna/+page.js @@ -0,0 +1 @@ +export const prerender = true; diff --git a/webui/src/routes/sauna/+page.svelte b/webui/src/routes/sauna/+page.svelte new file mode 100644 index 0000000..8496591 --- /dev/null +++ b/webui/src/routes/sauna/+page.svelte @@ -0,0 +1,41 @@ + + +