package main import ( "bytes" "encoding/json" "encoding/xml" "fmt" "io/ioutil" "log" "net/http" "net/url" "os" "strings" "strconv" "time" "git.lerch.org/lobo/wemo/logger" "git.lerch.org/lobo/wemo/wemodiscovery" ) type basementPost struct { MovieMode bool } type controlDataDevice struct { Name, Url string } type controlData struct { Devices []controlDataDevice BasicEvent string } type deviceAction struct { Device, Action, Content string StartAction, StartContent, StartField string Steps, Seconds, Start, End int EndOff bool } type soapActions struct { On, Off []deviceAction } type soapResponse struct { XMLName xml.Name `xml:"Envelope"` Body getBinaryStateBody `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` } type getBinaryStateBody struct { XMLName xml.Name GetBinaryStateResponse getBinaryStateBodyResponse `xml:"urn:Belkin:service:basicevent:1 GetBinaryStateResponse"` } type getBinaryStateBodyResponse struct { XMLName xml.Name State bool `xml:"BinaryState"` Brightness int `xml:"brightness"` Fader string `xml:"fader"` } var command string var client http.Client func main() { getState := "\"urn:Belkin:service:basicevent:1#GetBinaryState\"" getStateContent := "" command = os.Getenv("CMD") port := os.Getenv("PORT") if port == "" { port = ":8081" } else { port = ":" + port } logger.Infof("listening on port %s", port) if len(os.Args) > 1 { if os.Args[1] == "on" { movieMode(true) time.Sleep(10 * time.Second) // Allow the thread time to do magic }else if os.Args[1] == "off" { movieMode(false) time.Sleep(10 * time.Second) // Allow the thread time to do magic }else if os.Args[1] == "scan" { refreshControl() println("control file updated") }else if os.Args[1] == "getState" { logger.Infof("getState") addresses := readAddresses() for _, address := range addresses.Devices { if address.Name == os.Args[2] { dim := getCurrentDimValue(address.Url+addresses.BasicEvent, address.Name, getState, getStateContent) println("dim value is ", dim) } } }else{ println("wemo [on|off|scan|getState] [device name]") } // movieMode(false) os.Exit(0) } // POST /basement { movieMode: true } OR { movieMode: false } http.HandleFunc("/basement", func(w http.ResponseWriter, r *http.Request) { var postBody basementPost switch r.Method { case "GET": query := r.URL.Query().Get("moviemode") switch query { case "on", "true", "1": postBody.MovieMode = true case "off", "false", "0": postBody.MovieMode = false default: http.Error(w, "Missing query string", 400) return } case "POST": postBodyBytes, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, "Could not read body", 400) return } json.Unmarshal(postBodyBytes, &postBody) default: http.Error(w, "Not found", 404) return } fmt.Fprintf(w, "MovieMode: %t", postBody.MovieMode) movieMode(postBody.MovieMode) }) http.HandleFunc("*", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "Not found", 404) }) log.Fatal(http.ListenAndServe(port, nil)) } func movieMode(desiredState bool) { fmt.Fprintf(os.Stdout, "setting movieMode: %t\n", desiredState) addresses := readAddresses() actions := readMovieMode() var action []deviceAction if desiredState { action = actions.On } else { action = actions.Off } logger.Tracef("Modifying state on %d devices", len(action)) for _, device := range action { logger.Tracef("Action on device '%s'", device.Device) isFound := false for _, address := range addresses.Devices { if address.Name == device.Device { // Do the needful isFound = true logger.Tracef("Found device '%s'. Sending command(s) to %s", device.Device, address.Url) go commandDevice(address.Url+addresses.BasicEvent, device) break } } if !isFound { logger.Warnf("Did **NOT** find device '%s' in device address list", device.Device) } } } // Function is run once per device. All devices are done simultaneously func commandDevice(url string, device deviceAction) { logger.Tracef("%s: Steps: %d", device.Device, device.Steps) if device.Steps == 0 || device.Steps == 1 { sendCommand(url, device, device.Content) return } start := device.Start if start == -1 { start = getCurrentDimValue(url, device.Device, device.StartAction, device.StartContent) } logger.Tracef("%s: Stepping the change. %d to %d over %d steps", device.Device, start, device.End, device.Steps) // We want to fade something... millisecondsPerTick := device.Seconds * 1000 / device.Steps logger.Tracef("%s: %d ms per step over %d seconds", device.Device, millisecondsPerTick, device.Seconds) deltaPerTick := (device.End - start) / (device.Steps - 1) logger.Tracef("%s: %d change per command", device.Device, deltaPerTick) currentValue := start currentSteps := 0 ticker := time.NewTicker(time.Duration(millisecondsPerTick) * time.Millisecond) quit := make(chan struct{}) go func() { for { select { case <- ticker.C: currentSteps++ if currentValue >= 0 && currentValue <= 100 { finalCommand := strings.Replace(device.Content, "${val}", strconv.Itoa(currentValue), -1) logger.Tracef("%s: Calculated command: %s", device.Device, finalCommand) sendCommand(url, device, finalCommand) } else { logger.Errorf("%s: CurrentValue out of range! %d", device.Device, currentValue) } currentValue += deltaPerTick // do stuff if currentSteps >= device.Steps { if device.EndOff == true { // We want off! finalCommand := strings.Replace(device.Content, "${val}", strconv.Itoa(device.End), -1) finalCommand = strings.Replace(finalCommand, "1", "0", -1) logger.Tracef("%s: Calculated command: %s", device.Device, finalCommand) sendCommand(url, device, finalCommand) } logger.Tracef("%s: done. Final values %d steps, %d currentValue", device.Device, currentSteps, currentValue) close(quit) } case <- quit: ticker.Stop() return } } }() } func sendCommand(url string, device deviceAction, content string) { req, err := http.NewRequest("POST", url, nil) if err != nil { logger.Errorf("Error building http request to device %s: %s", device.Device, err) } req.Header.Add("SOAPACTION", device.Action) req.Header.Add("Content-type", `text/xml; charset="utf-8"`) req.Body = ioutil.NopCloser(bytes.NewBufferString(content)) res, err := client.Do(req) if err != nil { logger.Errorf("Error on http request to device %s: %s", device.Device, err) }else{ responseBytes, _ := ioutil.ReadAll(res.Body) logger.Tracef("%s: Response from lights: %s", device.Device, responseBytes) } } func getCurrentDimValue(url string, name string, action string, content string) int { req, err := http.NewRequest("POST", url, nil) rc := 0 if err != nil { logger.Errorf("Error building http request to device %s: %s", name, err) } req.Header.Add("SOAPACTION", action) req.Header.Add("Content-type", `text/xml; charset="utf-8"`) req.Body = ioutil.NopCloser(bytes.NewBufferString(content)) res, err := client.Do(req) if err != nil { logger.Errorf("Error on http request to device %s: %s", name, err) }else{ responseBytes, err := ioutil.ReadAll(res.Body) logger.Tracef("%s: Response from lights: %s", name, responseBytes) if err != nil { logger.Errorf("Error reading response from device %s: %s", name, err) } var envelope soapResponse xml.Unmarshal(responseBytes, &envelope) rc = envelope.Body.GetBinaryStateResponse.Brightness logger.Tracef("%s: Unmarshal: #v", name, envelope) logger.Tracef("%s: Brightness: %d", name, rc) logger.Tracef("%s: State: %t", name, envelope.Body.GetBinaryStateResponse.State) logger.Tracef("%s: Fader: %s", name, envelope.Body.GetBinaryStateResponse.Fader) } return rc } func readAddresses() controlData { var rc controlData bytes, err := ioutil.ReadFile("controlData.json") if err != nil { logger.Errorf("could not read controlData.json: %s", err) logger.Errorf("trying a scan") refreshControl() bytes, err = ioutil.ReadFile("controlData.json") if err != nil { logger.Errorf("still could not read controlData.json: %s", err) return rc } } err = json.Unmarshal(bytes, &rc) if err != nil { logger.Errorf("error unmarshalling controlData.json: %s", err) } return rc } func readMovieMode() soapActions { var rc soapActions bytes, err := ioutil.ReadFile("movieMode.json") if err != nil { logger.Errorf("could not read movieMode.json: %s", err) return rc } err = json.Unmarshal(bytes, &rc) if err != nil { logger.Errorf("error unmarshalling movieMode.json: %s", err) } return rc } func refreshControl() { scan("controlData.json") } func scan(filename string) { devices, err := wemodiscovery.Scan(wemodiscovery.DTAllBelkin, 2) if err != nil { logger.Errorf("error during scan: %s", err) return } basicEvent := "" json := `{"devices":[` isFirst := true for _, device := range devices { if !isFirst { json += "," } isFirst = false device.Load(1 * time.Second) logger.Infof("Device %s: %s %s\n", device.Scan.DeviceId, device.Scan.Location, device.FriendlyName) deviceurl, err := url.Parse(device.Scan.Location) if err != nil { logger.Errorf("URL did not parse. Device %s: %s %s\n", device.Scan.DeviceId, device.Scan.Location, device.FriendlyName) continue } json += `{"name": "` + device.FriendlyName + `", "url":"` + deviceurl.Scheme + `://` + deviceurl.Host + `"}` if basicEvent == "" { for _, service := range device.ServiceList { if service.ServiceType == "urn:Belkin:service:basicevent:1" { basicEvent = service.ControlURL break } } } } json += `],"basicEvent": "` + basicEvent + `"}` ioutil.WriteFile(filename, []byte(json), 0644) }