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]
}
|