wemo/wemo.go

340 lines
9.9 KiB
Go

package main
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"strings"
"strconv"
"time"
"git.lerch.org/lobo/wemo/logger"
"git.lerch.org/lobo/wemo/wemodiscovery"
)
type basementPost struct {
MovieMode bool
}
type controlDataDevice struct {
Name, Url string
}
type controlData struct {
Devices []controlDataDevice
BasicEvent string
}
type deviceAction struct {
Device, Action, Content string
StartAction, StartContent, StartField string
Steps, Seconds, Start, End int
EndOff bool
}
type soapActions struct {
On, Off []deviceAction
}
type soapResponse struct {
XMLName xml.Name `xml:"Envelope"`
Body getBinaryStateBody `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
}
type getBinaryStateBody struct {
XMLName xml.Name
GetBinaryStateResponse getBinaryStateBodyResponse `xml:"urn:Belkin:service:basicevent:1 GetBinaryStateResponse"`
}
type getBinaryStateBodyResponse struct {
XMLName xml.Name
State bool `xml:"BinaryState"`
Brightness int `xml:"brightness"`
Fader string `xml:"fader"`
}
var command string
var client http.Client
func main() {
getState := "\"urn:Belkin:service:basicevent:1#GetBinaryState\""
getStateContent := "<?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:GetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\"></u:GetBinaryState></s:Body></s:Envelope>"
command = os.Getenv("CMD")
port := os.Getenv("PORT")
if port == "" { port = ":8081" } else { port = ":" + port }
logger.Infof("listening on port %s", port)
if len(os.Args) > 1 {
if os.Args[1] == "on" {
movieMode(true)
time.Sleep(10 * time.Second) // Allow the thread time to do magic
}else if os.Args[1] == "off" {
movieMode(false)
time.Sleep(10 * time.Second) // Allow the thread time to do magic
}else if os.Args[1] == "scan" {
refreshControl()
println("control file updated")
}else if os.Args[1] == "getState" {
logger.Infof("getState")
addresses := readAddresses()
for _, address := range addresses.Devices {
if address.Name == os.Args[2] {
dim := getCurrentDimValue(address.Url+addresses.BasicEvent, address.Name, getState, getStateContent)
println("dim value is ", dim)
}
}
}else{
println("wemo [on|off|scan|getState] [device name]")
}
// movieMode(false)
os.Exit(0)
}
// POST /basement { movieMode: true } OR { movieMode: false }
http.HandleFunc("/basement", func(w http.ResponseWriter, r *http.Request) {
var postBody basementPost
switch r.Method {
case "GET":
query := r.URL.Query().Get("moviemode")
switch query {
case "on", "true", "1":
postBody.MovieMode = true
case "off", "false", "0":
postBody.MovieMode = false
default:
http.Error(w, "Missing query string", 400)
return
}
case "POST":
postBodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Could not read body", 400)
return
}
json.Unmarshal(postBodyBytes, &postBody)
default:
http.Error(w, "Not found", 404)
return
}
fmt.Fprintf(w, "MovieMode: %t", postBody.MovieMode)
movieMode(postBody.MovieMode)
})
http.HandleFunc("*", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not found", 404)
})
log.Fatal(http.ListenAndServe(port, nil))
}
func movieMode(desiredState bool) {
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
}
start := device.Start
if start == -1 {
start = getCurrentDimValue(url, device.Device, device.StartAction, device.StartContent)
}
logger.Tracef("%s: Stepping the change. %d to %d over %d steps", 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 - start) / (device.Steps - 1)
logger.Tracef("%s: %d change per command", device.Device, deltaPerTick)
currentValue := 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 getCurrentDimValue(url string, name string, action string, content string) int {
req, err := http.NewRequest("POST", url, nil)
rc := 0
if err != nil {
logger.Errorf("Error building http request to device %s: %s", name, err)
}
req.Header.Add("SOAPACTION", 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", name, err)
}else{
responseBytes, err := ioutil.ReadAll(res.Body)
logger.Tracef("%s: Response from lights: %s", name, responseBytes)
if err != nil {
logger.Errorf("Error reading response from device %s: %s", name, err)
}
var envelope soapResponse
xml.Unmarshal(responseBytes, &envelope)
rc = envelope.Body.GetBinaryStateResponse.Brightness
logger.Tracef("%s: Unmarshal: #v", name, envelope)
logger.Tracef("%s: Brightness: %d", name, rc)
logger.Tracef("%s: State: %t", name, envelope.Body.GetBinaryStateResponse.State)
logger.Tracef("%s: Fader: %s", name, envelope.Body.GetBinaryStateResponse.Fader)
}
return rc
}
func readAddresses() controlData {
var rc controlData
bytes, err := ioutil.ReadFile("controlData.json")
if err != nil {
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)
}
return rc
}
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 {
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)
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)
}