From 683a3774c3fc662120ac10318057adeeb9702919 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sat, 27 Jul 2019 15:04:08 -0700 Subject: [PATCH] fully operable system - wemo's duration arg doesn't seem to do anything useful --- go.mod | 3 + logger/go.mod | 3 + logger/logger.go | 65 ++++++++++++++ movieMode.json | 47 ++++++++++ wemo.go | 197 ++++++++++++++++++++++++++++++++++++++---- wemodiscovery/go.mod | 3 + wemodiscovery/scan.go | 3 +- 7 files changed, 304 insertions(+), 17 deletions(-) create mode 100644 logger/go.mod create mode 100644 logger/logger.go create mode 100644 movieMode.json diff --git a/go.mod b/go.mod index 400ec45..a3de4af 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module git.lerch.org/lobo/wemo go 1.12 require ( + git.lerch.org/lobo/wemo/logger v0.0.0 git.lerch.org/lobo/wemo/wemodiscovery v0.0.0 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect @@ -11,3 +12,5 @@ require ( ) replace git.lerch.org/lobo/wemo/wemodiscovery v0.0.0 => ./wemodiscovery + +replace git.lerch.org/lobo/wemo/logger v0.0.0 => ./logger diff --git a/logger/go.mod b/logger/go.mod new file mode 100644 index 0000000..70e12b1 --- /dev/null +++ b/logger/go.mod @@ -0,0 +1,3 @@ +module git.lerch.org/lobo/wemo/logger + +go 1.12 diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..addd8cc --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,65 @@ +package logger + +import ( + "log" + "os" +) + +type LoggerInterface interface { + Tracef(fmt string, args ...interface{}) + Infof(fmt string, args ...interface{}) + Warnf(fmt string, args ...interface{}) + Errorf(fmt string, args ...interface{}) +} + +// a default implementation of the LoggerInterface, simply using the 'log' library +type LeveledLogger struct { + level int +} + +var std = LeveledLogger{} + +var levels = map[string]int{ + "ERROR": 400, + "WARN" : 300, + "INFO" : 200, + "TRACE" : 100, +} + +func (l *LeveledLogger) levelPrint(minimumLevel int, fmt string, args ...interface{}) { + if l.level == 0 { + l.level = levels[os.Getenv("LOGLVL")] + if l.level == 0 { l.level = levels["WARN"] } + } + if minimumLevel >= l.level { + log.Printf(fmt+"\n", args...) + } +} + +func (l *LeveledLogger) Tracef(fmt string, args ...interface{}) { + l.levelPrint(levels["TRACE"], fmt, args...) +} + +func (l *LeveledLogger) Infof(fmt string, args ...interface{}) { + l.levelPrint(levels["INFO"], fmt, args...) +} +func (l *LeveledLogger) Warnf(fmt string, args ...interface{}) { + l.levelPrint(levels["WARN"], fmt, args...) +} +func (l *LeveledLogger) Errorf(fmt string, args ...interface{}) { + l.levelPrint(levels["ERROR"], fmt, args...) +} + +func Tracef(fmt string, args ...interface{}) { + std.levelPrint(levels["TRACE"], fmt, args...) +} + +func Infof(fmt string, args ...interface{}) { + std.levelPrint(levels["INFO"], fmt, args...) +} +func Warnf(fmt string, args ...interface{}) { + std.levelPrint(levels["WARN"], fmt, args...) +} +func Errorf(fmt string, args ...interface{}) { + std.levelPrint(levels["ERROR"], fmt, args...) +} diff --git a/movieMode.json b/movieMode.json new file mode 100644 index 0000000..6e39d35 --- /dev/null +++ b/movieMode.json @@ -0,0 +1,47 @@ +{ + "on": [ + { + "device": "Basement", + "comment": "Basement lights will turn off", + "action": "\"urn:Belkin:service:basicevent:1#SetBinaryState\"", + "content": "1${val}", + "steps": 15, + "seconds": 5, + "start": 100, + "end": 20, + "endOff": true + }, + { + "device": "Bar", + "comment": "Bar lights down to 10%", + "action": "\"urn:Belkin:service:basicevent:1#SetBinaryState\"", + "content": "1${val}", + "steps": 15, + "seconds": 5, + "start": 100, + "end": 0 + } + ], + "off": [ + { + "device": "Basement", + "comment": "Basement lights will turn on", + "action": "\"urn:Belkin:service:basicevent:1#SetBinaryState\"", + "content": "1${val}", + "steps": 15, + "seconds": 5, + "start": 20, + "end": 100 + }, + { + "device": "Bar", + "comment": "Bar lights up to 100%", + "action": "\"urn:Belkin:service:basicevent:1#SetBinaryState\"", + "content": "1${val}", + "steps": 15, + "seconds": 5, + "start": 10, + "end": 100 + } + ] +} diff --git a/wemo.go b/wemo.go index 34ffa2c..e356e0f 100644 --- a/wemo.go +++ b/wemo.go @@ -1,14 +1,19 @@ package main import ( + "bytes" "encoding/json" "fmt" "io/ioutil" "log" "net/http" + "net/url" "os" + "strings" + "strconv" "time" + "git.lerch.org/lobo/wemo/logger" "git.lerch.org/lobo/wemo/wemodiscovery" ) @@ -16,24 +21,38 @@ type basementPost struct { MovieMode bool } +type controlDataDevice struct { + Name, Url string +} type controlData struct { - Bar, Basement, Jack string + Devices []controlDataDevice BasicEvent string } +type deviceAction struct { + Device, Action, Content string + Steps, Seconds, Start, End int + EndOff bool +} + +type soapActions struct { + On, Off []deviceAction +} + var command string +var client http.Client func main() { command = os.Getenv("CMD") if len(os.Args) > 1 { - if os.Args[1] == "scan" { - scan() - os.Exit(0) + if os.Args[1] == "on" { + movieMode(true) + }else{ + movieMode(false) } - movieMode(true) - time.Sleep(60 * time.Second) - movieMode(false) + time.Sleep(10 * time.Second) + // movieMode(false) os.Exit(0) } // POST /basement { movieMode: true } OR { movieMode: false } @@ -61,30 +80,176 @@ func main() { } func movieMode(desiredState bool) { - fmt.Fprintf(os.Stdout, "setting movieMode: %t", desiredState) - // addresses := readAddresses() + 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 + } + logger.Tracef("%s: Stepping the change. %d to %d over %d steps", device.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 - device.Start) / (device.Steps - 1) + logger.Tracef("%s: %d change per command", device.Device, deltaPerTick) + currentValue := device.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 readAddresses() controlData { var rc controlData bytes, err := ioutil.ReadFile("controlData.json") if err != nil { - fmt.Fprintf(os.Stderr, "could not read controlData.json: %s", err) - return rc + 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) } - json.Unmarshal(bytes, &rc) return rc } -func scan() { +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 { - fmt.Fprintf(os.Stderr, "error during scan: %s", err) + 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) - fmt.Fprintf(os.Stdout, "Device %s: %s %s %s\n", device.Scan.DeviceId, device.Scan.Location, device.FriendlyName, device.Scan.Urn) + 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) } diff --git a/wemodiscovery/go.mod b/wemodiscovery/go.mod index 6cb50e7..ff84f18 100644 --- a/wemodiscovery/go.mod +++ b/wemodiscovery/go.mod @@ -3,6 +3,9 @@ module git.lerch.org/lobo/wemo/wemodiscovery go 1.12 require ( + git.lerch.org/lobo/wemo/logger v0.0.0 github.com/fromkeith/gossdp v0.0.0-20180102154144-1b2c43f6886e golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect ) + +replace git.lerch.org/lobo/wemo/logger v0.0.0 => ../logger diff --git a/wemodiscovery/scan.go b/wemodiscovery/scan.go index 49184ab..bb3de8f 100644 --- a/wemodiscovery/scan.go +++ b/wemodiscovery/scan.go @@ -8,6 +8,7 @@ import ( "time" "github.com/fromkeith/gossdp" + "git.lerch.org/lobo/wemo/logger" ) var responses []gossdp.ResponseMessage @@ -27,7 +28,7 @@ func Scan(dt DeviceType, waitTimeSeconds int) ([]*Device, error) { responses = []gossdp.ResponseMessage{} l := belkinListener{} - c, err := gossdp.NewSsdpClientWithLogger(l, gossdp.DefaultLogger{}) + c, err := gossdp.NewSsdpClientWithLogger(l, &logger.LeveledLogger{}) if err != nil { return nil, fmt.Errorf("failed to start ssdp discovery client: %s", err) }