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
|
||||
|
||||
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
|
||||
|
|
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
|
||||
}
|
||||
]
|
||||
}
|
197
wemo.go
197
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, "<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 {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user