summaryrefslogtreecommitdiff
path: root/main.go
blob: 43311232f51b47e0db23eb77cc4620f62e2da819 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package main

import (
	"encoding/json"
	"embed"
	"flag"
	"fmt"
	"log"
	"net/http"
	"net/netip"
	"os"
	"text/template"
)

//go:embed simple.css/simple.css templates/index.html
var static embed.FS

type RuntimeConfig struct {
	Devices DevicesConfig
	Web     WebConfig
}

type DevicesConfig struct {
	Hs100 []Hs100Conf
}

type WebConfig struct {
	Listen netip.AddrPort
}

func main() {
	configPath := parseFlags()
	c := parseConfig(configPath)
	http.HandleFunc("/", index(c.Devices))
	http.HandleFunc("/api", api())
	http.Handle("/static/", http.StripPrefix("/static/",
		http.FileServer(http.FS(static))))
	fmt.Printf("Serving at http://%s\n", c.Web.Listen)
	log.Fatal(http.ListenAndServe(c.Web.Listen.String(), nil))
}

// parseFlags() handles command line interface (CLI) flags.
func parseFlags() string {

	var r string

	flag.StringVar(&r, "c", "/etc/webiot/config.json",
		"path to configuration file")
	flag.Parse()

	return r
}

// parseConfig() parses and validates the runtime configuration file and
// returns it as Go datastructure.
func parseConfig(path string) RuntimeConfig {
	data, err := os.ReadFile(path)
	if err != nil {
		log.Fatalf("Could not read '%s'!", path)
	}

	if !json.Valid(data) {
		log.Fatalf("%s contains invalid JSON!", path)
	}

	config := RuntimeConfig{}
	err = json.Unmarshal(data, &config)
	if err != nil {
		log.Fatalf("Could not parse configuration file:\n%s\n", err)
	}

	return config
}

// index() returns a HTTP handler for the index page.
func index(devices DevicesConfig) func(http.ResponseWriter, *http.Request) {
	tmpl, err := template.ParseFS(static, "templates/index.html")
	if err != nil {
		log.Fatal(err)
	}

	return func(w http.ResponseWriter, r *http.Request) {
		err = tmpl.Execute(w, devices)
		if err != nil {
			http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
			return
		}
	}
}

// api() returns the HTTP handler for the API endpoint.
func api() func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {

		// TODO assert correct HTTP method

		errHost, host := assertSingleParam(r, "host")
		errState, state := assertSingleParam(r, "state")
		if (errHost != nil) || (errState != nil) {
			http.Error(w,
				"Provide exactly one host and one state parameter!", 400)
			return
		}

		err := set(host, state)
		if err != nil {
			http.Error(w, "Could not set WiFi plug.", 500)
		} else {
			fmt.Fprint(w, "ok")
		}
	}
}

// assertSingleParam() returns the value of given URL key and panics if there
// is not exactly one match for this key.
func assertSingleParam(r *http.Request, key string) (error, string) {

	values := r.URL.Query()[key]
	if len(values) != 1 {
		return fmt.Errorf("Provide exactly one '%s' parameter!", key), ""
	}
	return nil, values[0]

}