diff options
Diffstat (limited to 'main.go')
-rw-r--r-- | main.go | 155 |
1 files changed, 155 insertions, 0 deletions
@@ -0,0 +1,155 @@ +// vim: shiftwidth=4 tabstop=4 noexpandtab + +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/netip" + "text/template" +) + +type RuntimeConfig struct { + Devices DevicesConfig + Web WebConfig +} + +type DevicesConfig struct { + Hs100 []Hs100Conf +} + +type WebConfig struct { + Listen netip.AddrPort +} + +// main() contains the control flow of this program. +func main() { + configPath := parseFlags() + config := parseConfig(configPath) + http.HandleFunc("/", index(config.Devices)) + http.HandleFunc("/api", api()) + http.HandleFunc("/webiot.css", css()) + fmt.Printf("Serving at http://%s\n", config.Web.Listen) + log.Fatal(http.ListenAndServe(config.Web.Listen.String(), nil)) +} + +// parseFlags() handles command line interface (CLI) flags. +func parseFlags() string { + + var r string // return value + + flag.StringVar(&r, "c", "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 { + + // read config file and ensure proper JSON formatting + data := mustRead(path) + if !json.Valid(data) { + log.Fatalf("%s contains invalid JSON!", path) + } + + // read to RuntimeConfig struct and handle errors + 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(config DevicesConfig) func(http.ResponseWriter, *http.Request) { + + // prepare HTML + html := mustRender("index.html.tmpl", config) + + return func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, html) + } +} + +func css() func(http.ResponseWriter, *http.Request) { + + // read CSS file + css := string(mustRead("./libweb/libweb.css")) + + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/css; charset=utf-8") + fmt.Fprint(w, css) + } +} + +// mustRead() reads a file and panics if this is not possible. +func mustRead(path string) []byte { + data, err := ioutil.ReadFile(path) + if err != nil { + log.Fatalf("Could not read '%s'!", path) + } + return data +} + +// mustRender() renders a template file with the given data and panics if this +// is not possible. +func mustRender(filepath string, data interface{}) string { + + file := mustRead(filepath) + tmpl, err := template.New(filepath).Parse(string(file)) + var buffer bytes.Buffer + err = tmpl.Execute(&buffer, data) + if err != nil { + fmt.Println(err) + log.Fatalf("Could not execute template for %s!", filepath) + } + + return buffer.String() +} + +// 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 + + // read parameters and handle errors + 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 + } + + // set WiFi plug + 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] + +} |