fully operable system - wemo's duration arg doesn't seem to do anything useful

This commit is contained in:
Emil Lerch 2019-07-27 15:04:08 -07:00
parent 869f2007be
commit 683a3774c3
Signed by: lobo
GPG Key ID: CEC5F37C1BE5A481
7 changed files with 304 additions and 17 deletions

3
go.mod
View File

@ -3,6 +3,7 @@ module git.lerch.org/lobo/wemo
go 1.12 go 1.12
require ( require (
git.lerch.org/lobo/wemo/logger v0.0.0
git.lerch.org/lobo/wemo/wemodiscovery 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/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // 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/wemodiscovery v0.0.0 => ./wemodiscovery
replace git.lerch.org/lobo/wemo/logger v0.0.0 => ./logger

3
logger/go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.lerch.org/lobo/wemo/logger
go 1.12

65
logger/logger.go Normal file
View File

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

47
movieMode.json Normal file
View File

@ -0,0 +1,47 @@
{
"on": [
{
"device": "Basement",
"comment": "Basement lights will turn off",
"action": "\"urn:Belkin:service:basicevent:1#SetBinaryState\"",
"content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body><u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\"><BinaryState>1</BinaryState><brightness>${val}</brightness></u:SetBinaryState></s:Body></s:Envelope>",
"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": "<?xml version=\"1.0\" encoding=\"utf-8\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body><u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\"><BinaryState>1</BinaryState><brightness>${val}</brightness></u:SetBinaryState></s:Body></s:Envelope>",
"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": "<?xml version=\"1.0\" encoding=\"utf-8\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body><u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\"><BinaryState>1</BinaryState><brightness>${val}</brightness></u:SetBinaryState></s:Body></s:Envelope>",
"steps": 15,
"seconds": 5,
"start": 20,
"end": 100
},
{
"device": "Bar",
"comment": "Bar lights up to 100%",
"action": "\"urn:Belkin:service:basicevent:1#SetBinaryState\"",
"content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body><u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\"><BinaryState>1</BinaryState><brightness>${val}</brightness></u:SetBinaryState></s:Body></s:Envelope>",
"steps": 15,
"seconds": 5,
"start": 10,
"end": 100
}
]
}

191
wemo.go
View File

@ -1,14 +1,19 @@
package main package main
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"net/url"
"os" "os"
"strings"
"strconv"
"time" "time"
"git.lerch.org/lobo/wemo/logger"
"git.lerch.org/lobo/wemo/wemodiscovery" "git.lerch.org/lobo/wemo/wemodiscovery"
) )
@ -16,24 +21,38 @@ type basementPost struct {
MovieMode bool MovieMode bool
} }
type controlDataDevice struct {
Name, Url string
}
type controlData struct { type controlData struct {
Bar, Basement, Jack string Devices []controlDataDevice
BasicEvent string 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 command string
var client http.Client
func main() { func main() {
command = os.Getenv("CMD") command = os.Getenv("CMD")
if len(os.Args) > 1 { if len(os.Args) > 1 {
if os.Args[1] == "scan" { if os.Args[1] == "on" {
scan()
os.Exit(0)
}
movieMode(true) movieMode(true)
time.Sleep(60 * time.Second) }else{
movieMode(false) movieMode(false)
}
time.Sleep(10 * time.Second)
// movieMode(false)
os.Exit(0) os.Exit(0)
} }
// POST /basement { movieMode: true } OR { movieMode: false } // POST /basement { movieMode: true } OR { movieMode: false }
@ -61,30 +80,176 @@ func main() {
} }
func movieMode(desiredState bool) { func movieMode(desiredState bool) {
fmt.Fprintf(os.Stdout, "setting movieMode: %t", desiredState) fmt.Fprintf(os.Stdout, "setting movieMode: %t\n", desiredState)
// addresses := readAddresses() 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, "<BinaryState>1</BinaryState>", "<BinaryState>0</BinaryState>", -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 { func readAddresses() controlData {
var rc controlData var rc controlData
bytes, err := ioutil.ReadFile("controlData.json") bytes, err := ioutil.ReadFile("controlData.json")
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "could not read controlData.json: %s", err) 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 return rc
} }
json.Unmarshal(bytes, &rc) }
err = json.Unmarshal(bytes, &rc)
if err != nil {
logger.Errorf("error unmarshalling controlData.json: %s", err)
}
return 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) devices, err := wemodiscovery.Scan(wemodiscovery.DTAllBelkin, 2)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error during scan: %s", err) logger.Errorf("error during scan: %s", err)
return return
} }
basicEvent := ""
json := `{"devices":[`
isFirst := true
for _, device := range devices { for _, device := range devices {
if !isFirst {
json += ","
}
isFirst = false
device.Load(1 * time.Second) 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)
}

View File

@ -3,6 +3,9 @@ module git.lerch.org/lobo/wemo/wemodiscovery
go 1.12 go 1.12
require ( require (
git.lerch.org/lobo/wemo/logger v0.0.0
github.com/fromkeith/gossdp v0.0.0-20180102154144-1b2c43f6886e github.com/fromkeith/gossdp v0.0.0-20180102154144-1b2c43f6886e
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect
) )
replace git.lerch.org/lobo/wemo/logger v0.0.0 => ../logger

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/fromkeith/gossdp" "github.com/fromkeith/gossdp"
"git.lerch.org/lobo/wemo/logger"
) )
var responses []gossdp.ResponseMessage var responses []gossdp.ResponseMessage
@ -27,7 +28,7 @@ func Scan(dt DeviceType, waitTimeSeconds int) ([]*Device, error) {
responses = []gossdp.ResponseMessage{} responses = []gossdp.ResponseMessage{}
l := belkinListener{} l := belkinListener{}
c, err := gossdp.NewSsdpClientWithLogger(l, gossdp.DefaultLogger{}) c, err := gossdp.NewSsdpClientWithLogger(l, &logger.LeveledLogger{})
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to start ssdp discovery client: %s", err) return nil, fmt.Errorf("failed to start ssdp discovery client: %s", err)
} }