From 3b5f835f112ac057be321cb40823d7073831af7e Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Wed, 24 Jul 2019 17:42:52 -0700 Subject: [PATCH] move scanning features into wemo golang package --- .gitignore | 1 + build | 2 + diag/index.js | 23 ------- diag/package-lock.json | 120 ----------------------------------- diag/package.json | 14 ---- go.mod | 13 ++++ go.sum | 26 ++++++++ wemo.go | 90 ++++++++++++++++++++++++++ wemodiscovery/device.go | 42 ++++++++++++ wemodiscovery/device_type.go | 34 ++++++++++ wemodiscovery/go.mod | 8 +++ wemodiscovery/go.sum | 8 +++ wemodiscovery/scan.go | 75 ++++++++++++++++++++++ 13 files changed, 299 insertions(+), 157 deletions(-) create mode 100755 build delete mode 100644 diag/index.js delete mode 100644 diag/package-lock.json delete mode 100644 diag/package.json create mode 100644 go.mod create mode 100644 go.sum create mode 100644 wemo.go create mode 100644 wemodiscovery/device.go create mode 100644 wemodiscovery/device_type.go create mode 100644 wemodiscovery/go.mod create mode 100644 wemodiscovery/go.sum create mode 100644 wemodiscovery/scan.go diff --git a/.gitignore b/.gitignore index 3c3629e..f6664a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +wemo diff --git a/build b/build new file mode 100755 index 0000000..babd3a5 --- /dev/null +++ b/build @@ -0,0 +1,2 @@ +#!/bin/sh +CGO_ENABLED=0 go build diff --git a/diag/index.js b/diag/index.js deleted file mode 100644 index 3d0192f..0000000 --- a/diag/index.js +++ /dev/null @@ -1,23 +0,0 @@ -let Wemo = require('wemo-client'); -let wemo = new Wemo(); - -wemo.discover(function(err, deviceInfo) { - console.log('Wemo Device Found: %j', deviceInfo); - - // Get the client for the found device - let client = wemo.client(deviceInfo); - - // You definitely want to listen to error events (e.g. device went offline), - // Node will throw them as an exception if they are left unhandled - client.on('error', function(err) { - console.log('Error: %s', err.code); - }); - - // Handle BinaryState events - client.on('binaryState', function(value) { - console.log('Binary State changed to: %s', value); - }); - - // Turn the switch on - //client.setBinaryState(1); -}); diff --git a/diag/package-lock.json b/diag/package-lock.json deleted file mode 100644 index 416abac..0000000 --- a/diag/package-lock.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "name": "wemo", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "requires": { - "lodash": "^4.17.14" - } - }, - "bluebird": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", - "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" - }, - "lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node-ssdp": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/node-ssdp/-/node-ssdp-3.3.0.tgz", - "integrity": "sha512-hFBkfUJytKC2x64jljojAbktG8aOL0C1YuNjCK54ZGBBg2382J3oTuK17T+aFgmy47noKHE5arLnYppo0JjcLw==", - "requires": { - "async": "^2.6.0", - "bluebird": "^3.5.1", - "debug": "^3.1.0", - "extend": "^3.0.1", - "ip": "^1.1.5" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "wemo-client": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/wemo-client/-/wemo-client-0.15.0.tgz", - "integrity": "sha512-zf266rvxXMtC7y9nR1Xu/yeYQCGgUPjYRQ1E4LZPxNiC5csDLIZBFklpUU3alcxSr+vqUpVtLDZz/iWiLX8sVw==", - "requires": { - "debug": "^2.6.9", - "entities": "^1.1.1", - "ip": "^1.1.5", - "node-ssdp": "^3.2.5", - "xml2js": "^0.4.19", - "xmlbuilder": "^8.2.2" - } - }, - "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - }, - "dependencies": { - "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" - } - } - }, - "xmlbuilder": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", - "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=" - } - } -} diff --git a/diag/package.json b/diag/package.json deleted file mode 100644 index 56f9501..0000000 --- a/diag/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "wemo", - "version": "1.0.0", - "description": "Movie mode for downstairs", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "dependencies": { - "wemo-client": "^0.15.0" - } -} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..400ec45 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module git.lerch.org/lobo/wemo + +go 1.12 + +require ( + 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 + golang.org/x/text v0.3.2 // indirect + golang.org/x/tools v0.0.0-20190723021737-8bb11ff117ca // indirect +) + +replace git.lerch.org/lobo/wemo/wemodiscovery v0.0.0 => ./wemodiscovery diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..88478ae --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +github.com/fromkeith/gossdp v0.0.0-20180102154144-1b2c43f6886e h1:cG4ivpkHpkmWTaaLrgekDVR0xAr87V697T2c+WnUdiY= +github.com/fromkeith/gossdp v0.0.0-20180102154144-1b2c43f6886e/go.mod h1:7xQpS/YtlWo38XfIqje9GgtlPuBRatYcL23GlYBtgWM= +github.com/go-home-iot/gossdp v0.0.0-20160327224030-49870f3db38d h1:ebIOreKlyZrQkj2SV6m6f6H9A6T7UcHY8YAfuYvY4Hc= +github.com/go-home-iot/gossdp v0.0.0-20160327224030-49870f3db38d/go.mod h1:nUnTkSnQ3tPjjZaxpQJU7boXxTla6IJGT/1jUZrFtfw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190723021737-8bb11ff117ca/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y= +rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/wemo.go b/wemo.go new file mode 100644 index 0000000..34ffa2c --- /dev/null +++ b/wemo.go @@ -0,0 +1,90 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "time" + + "git.lerch.org/lobo/wemo/wemodiscovery" +) + +type basementPost struct { + MovieMode bool +} + +type controlData struct { + Bar, Basement, Jack string + BasicEvent string +} + +var command string + +func main() { + command = os.Getenv("CMD") + + if len(os.Args) > 1 { + if os.Args[1] == "scan" { + scan() + os.Exit(0) + } + movieMode(true) + time.Sleep(60 * time.Second) + movieMode(false) + os.Exit(0) + } + // POST /basement { movieMode: true } OR { movieMode: false } + http.HandleFunc("/basement", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Not found", 404) + return + } + postBodyBytes, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "Could not read body", 500) + return + } + var postBody basementPost + json.Unmarshal(postBodyBytes, &postBody) + 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(":8081", nil)) +} + +func movieMode(desiredState bool) { + fmt.Fprintf(os.Stdout, "setting movieMode: %t", desiredState) + // addresses := readAddresses() +} + +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 + } + json.Unmarshal(bytes, &rc) + return rc +} + +func scan() { + devices, err := wemodiscovery.Scan(wemodiscovery.DTAllBelkin, 2) + if err != nil { + fmt.Fprintf(os.Stderr, "error during scan: %s", err) + return + } + + for _, device := range devices { + 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) + } +} diff --git a/wemodiscovery/device.go b/wemodiscovery/device.go new file mode 100644 index 0000000..3a95a35 --- /dev/null +++ b/wemodiscovery/device.go @@ -0,0 +1,42 @@ +package wemodiscovery + +import ( + "time" + + "github.com/fromkeith/gossdp" +) + +// Device contains information about a device that has been found on the network +type Device struct { + Scan gossdp.ResponseMessage + DeviceType string `xml:"deviceType"` + FriendlyName string `xml:"friendlyName"` + Manufacturer string `xml:"manufacturer"` + ManufacturerURL string `xml:"manufacturerURL"` + ModelDescription string `xml:"modelDescription"` + ModelName string `xml:"modelName"` + ModelNumber string `xml:"modelNumber"` + ModelURL string `xml:"modelURL"` + SerialNumber string `xml:"serialNumber"` + UDN string `xml:"UDN"` + UPC string `xml:"UPC"` + MACAddress string `xml:"macAddress"` + FirmwareVersion string `xml:"firmwareVersion"` + IconVersion string `xml:"iconVersion"` + BinaryState int `xml:"binaryState"` + ServiceList []Service `xml:"serviceList>service"` + Timeout time.Duration +} + +// Service contains information about a service exposed by the Belkin device +type Service struct { + ServiceType string `xml:"serviceType"` + ServiceID string `xml:"serviceId"` + ControlURL string `xml:"controlURL"` + EventSubURL string `xml:"eventSubURL"` + SCPDURL string `xml:"SCPDURL"` +} + +type root struct { + Device *Device `xml:"device"` +} diff --git a/wemodiscovery/device_type.go b/wemodiscovery/device_type.go new file mode 100644 index 0000000..12eb78e --- /dev/null +++ b/wemodiscovery/device_type.go @@ -0,0 +1,34 @@ +package wemodiscovery + +// DeviceType represents an identifier for the type of Belkin device you want to +// scan the network for +type DeviceType string + +const ( + // DTBridge - belkin bridge + DTBridge DeviceType = "urn:Belkin:device:bridge:1" + + // DTSwitch - belkin switch + DTSwitch = "urn:Belkin:device:controllee:1" + + // DTMotion - belkin motion sensor + DTMotion = "urn:Belkin:device:sensor:1" + + // DTMaker - belkin maker + DTMaker = "urn:Belkin:device:Maker:1" + + // DTInsight - belkin insight + DTInsight = "urn:Belkin:device:insight:1" + + // DTLightSwitch - belkin light switch + DTLightSwitch = "urn:Belkin:device:lightswitch:1" + + // DTDimmer - belkin dimmer + DTDimmer = "urn:Belkin:device:dimmer:1" + + // Get everything + DTAll = "ssdp:all" + + // Get everything + DTAllBelkin = "urn:Belkin:device:**" +) diff --git a/wemodiscovery/go.mod b/wemodiscovery/go.mod new file mode 100644 index 0000000..6cb50e7 --- /dev/null +++ b/wemodiscovery/go.mod @@ -0,0 +1,8 @@ +module git.lerch.org/lobo/wemo/wemodiscovery + +go 1.12 + +require ( + github.com/fromkeith/gossdp v0.0.0-20180102154144-1b2c43f6886e + golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect +) diff --git a/wemodiscovery/go.sum b/wemodiscovery/go.sum new file mode 100644 index 0000000..1ba8aab --- /dev/null +++ b/wemodiscovery/go.sum @@ -0,0 +1,8 @@ +github.com/fromkeith/gossdp v0.0.0-20180102154144-1b2c43f6886e h1:cG4ivpkHpkmWTaaLrgekDVR0xAr87V697T2c+WnUdiY= +github.com/fromkeith/gossdp v0.0.0-20180102154144-1b2c43f6886e/go.mod h1:7xQpS/YtlWo38XfIqje9GgtlPuBRatYcL23GlYBtgWM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/wemodiscovery/scan.go b/wemodiscovery/scan.go new file mode 100644 index 0000000..49184ab --- /dev/null +++ b/wemodiscovery/scan.go @@ -0,0 +1,75 @@ +package wemodiscovery + +import ( + "encoding/xml" + "fmt" + "net/http" + "io/ioutil" + "time" + + "github.com/fromkeith/gossdp" +) + +var responses []gossdp.ResponseMessage + +type belkinListener struct { + // Response func(message gossdp.ResponseMessage) +} + +func (l belkinListener) Response(message gossdp.ResponseMessage) { + responses = append(responses, message) +} + +// Scan detects Belkin devices on the network. The devices that are returned have +// limited information in the Scan field, to get more detailed information you will +// have to call Load() on the device +func Scan(dt DeviceType, waitTimeSeconds int) ([]*Device, error) { + responses = []gossdp.ResponseMessage{} + l := belkinListener{} + + c, err := gossdp.NewSsdpClientWithLogger(l, gossdp.DefaultLogger{}) + if err != nil { + return nil, fmt.Errorf("failed to start ssdp discovery client: %s", err) + } + + defer c.Stop() + go c.Start() + err = c.ListenFor(string(dt)) + if err != nil { + return nil, fmt.Errorf("discovery failed: %s", err) + } + + time.Sleep(time.Duration(waitTimeSeconds) * time.Second) + + devices := make([]*Device, len(responses)) + for i, response := range responses { + devices[i] = &Device{Scan: response} + } + return devices, nil +} + +// Load fetches all of the device specific information and updates the calling struct. The timeout +// parameter specifies how long to wait to connect and get a response before giving up +func (d *Device) Load(timeout time.Duration) error { + client := http.Client{Timeout: timeout} + resp, err := client.Get(d.Scan.Location) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return fmt.Errorf("error fetching device info: %s", err) + } + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response from device: %s", err) + } + + var root root + root.Device = d + err = xml.Unmarshal(b, &root) + if err != nil { + return err + } + + return nil +}