Add sauna

This commit is contained in:
Thomas Klaehn 2023-01-30 15:54:49 +01:00
parent f8b2047cff
commit acb68a45ff
37 changed files with 234 additions and 123 deletions

3
.gitignore vendored
View File

@ -4,3 +4,6 @@
.DS_Store
__debug_bin
bin/
homeservice.tar.gz

69
Makefile Normal file
View File

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

0
README.md Normal file
View File

View File

@ -1,12 +0,0 @@
{
"outer_gate": {
"state": 1,
"open_time": 4,
"close_time": 2
},
"inner_gate": {
"state": 1,
"open_time": 2,
"close_time": 2
}
}

8
go.mod
View File

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

12
go.sum
View File

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

10
homeservice.service Normal file
View File

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

168
main.go
View File

@ -2,144 +2,104 @@ package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"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)
// API routes
// Serve files from static folder
http.Handle("/", http.FileServer(http.Dir("./svelte/build")))
// 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))
}

View File

@ -16,12 +16,7 @@
<ul class="menu">
<li><a href="/">Home</a></li>
<li><a href="/chicken">Chicken</a></li>
<!-- <li><a href="/Information">Information</a></li>
<li><a href="/Configuration">Configuration</a></li>
<li><a href="/Security">Security</a></li>
<li><a href="/FirmwareUpdate">Firmware update</a></li>
<li><a href="/APIDocumentation">API documentation</a></li>
<li><a href="https://docs.perinet.io" target="_blank">Online documentation</a></li> -->
<li><a href="/sauna">Sauna</a></li>
<!-- <li><a href="/chicken">Chicken</a></li> -->
</ul>
</header>

View File

@ -0,0 +1 @@
export const prerender = true;

View File

@ -0,0 +1,41 @@
<script>
import { onMount } from "../../../node_modules/svelte/internal";
import icon from "../../../static/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'>
<figure>
<img src={icon} alt="Sauna" width=150/>
</figure>
<h1>{temperature_value} {temperature_unit}</h1>
</section>

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

24
webui/static/sauna.svg Normal file
View File

@ -0,0 +1,24 @@
<?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>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB