Compare commits

...

3 Commits

14 changed files with 306 additions and 159 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules node_modules
wemo

2
build Executable file
View File

@ -0,0 +1,2 @@
#!/bin/sh
CGO_ENABLED=0 go build

View File

@ -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);
});

120
diag/package-lock.json generated
View File

@ -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="
}
}
}

View File

@ -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"
}
}

View File

@ -42,7 +42,7 @@ import uuid
SETUP_XML = """<?xml version="1.0"?> SETUP_XML = """<?xml version="1.0"?>
<root> <root>
<device> <device>
<deviceType>urn:MakerMusings:device:controllee:1</deviceType> <deviceType>urn:Belkin:device:controllee:1</deviceType>
<friendlyName>%(device_name)s</friendlyName> <friendlyName>%(device_name)s</friendlyName>
<manufacturer>Belkin International Inc.</manufacturer> <manufacturer>Belkin International Inc.</manufacturer>
<modelName>Emulated Socket</modelName> <modelName>Emulated Socket</modelName>
@ -359,7 +359,12 @@ class upnp_broadcast_responder(object):
def do_read(self, fileno): def do_read(self, fileno):
data, sender = self.recvfrom(1024) data, sender = self.recvfrom(1024)
if data: if data:
if data.find('M-SEARCH') == 0 and data.find('urn:Belkin:device:**') != -1: if data.find('M-SEARCH') == 0 and (
data.find('urn:Belkin:device:**') != -1 or
data.find('ssdp:all') != -1 or
data.find('urn:Belkin:device:controllee:1') != -1
):
for device in self.devices: for device in self.devices:
time.sleep(0.1) time.sleep(0.1)
device.respond_to_search(sender, 'urn:Belkin:device:**') device.respond_to_search(sender, 'urn:Belkin:device:**')

13
go.mod Normal file
View File

@ -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

26
go.sum Normal file
View File

@ -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=

90
wemo.go Normal file
View File

@ -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)
}
}

42
wemodiscovery/device.go Normal file
View File

@ -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"`
}

View File

@ -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:**"
)

8
wemodiscovery/go.mod Normal file
View File

@ -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
)

8
wemodiscovery/go.sum Normal file
View File

@ -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=

75
wemodiscovery/scan.go Normal file
View File

@ -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
}