fully operable system - wemo's duration arg doesn't seem to do anything useful
This commit is contained in:
parent
869f2007be
commit
683a3774c3
3
go.mod
3
go.mod
|
@ -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
3
logger/go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module git.lerch.org/lobo/wemo/logger
|
||||||
|
|
||||||
|
go 1.12
|
65
logger/logger.go
Normal file
65
logger/logger.go
Normal 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
47
movieMode.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
193
wemo.go
193
wemo.go
|
@ -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 {
|
||||||
device.Load(1 * time.Second)
|
if !isFirst {
|
||||||
fmt.Fprintf(os.Stdout, "Device %s: %s %s %s\n", device.Scan.DeviceId, device.Scan.Location, device.FriendlyName, device.Scan.Urn)
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user