summaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'main.go')
-rw-r--r--main.go155
1 files changed, 155 insertions, 0 deletions
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..d6b59b9
--- /dev/null
+++ b/main.go
@@ -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]
+
+}