Add sauna
3
.gitignore
vendored
@ -4,3 +4,6 @@
|
||||
.DS_Store
|
||||
|
||||
__debug_bin
|
||||
|
||||
bin/
|
||||
homeservice.tar.gz
|
||||
|
2
.vscode/launch.json
vendored
@ -10,7 +10,7 @@
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"preLaunchTask": "npm: build - svelte"
|
||||
"preLaunchTask": "npm: build - webui"
|
||||
}
|
||||
]
|
||||
}
|
4
.vscode/tasks.json
vendored
@ -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"
|
||||
}
|
||||
]
|
||||
|
69
Makefile
Normal 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/*
|
@ -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
@ -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
@ -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
@ -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
|
173
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))
|
||||
|
||||
}
|
||||
|
0
svelte/.gitignore → webui/.gitignore
vendored
@ -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>
|
1
webui/src/routes/sauna/+page.js
Normal file
@ -0,0 +1 @@
|
||||
export const prerender = true;
|
41
webui/src/routes/sauna/+page.svelte
Normal 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>
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
24
webui/static/sauna.svg
Normal 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 |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |