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