271 lines
7.2 KiB
Go
271 lines
7.2 KiB
Go
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"
|
|
)
|
|
|
|
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
|
|
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")
|
|
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)
|
|
}else{
|
|
movieMode(false)
|
|
}
|
|
time.Sleep(10 * time.Second)
|
|
// 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
|
|
}
|
|
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 {
|
|
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)
|
|
}
|