Add sauna
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -4,3 +4,6 @@
 | 
			
		||||
.DS_Store
 | 
			
		||||
 | 
			
		||||
__debug_bin
 | 
			
		||||
 | 
			
		||||
bin/
 | 
			
		||||
homeservice.tar.gz
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
							
								
								
									
										168
									
								
								main.go
									
									
									
									
									
								
							
							
						
						@@ -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))
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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  |