// Package wemo ...
// Copyright 2014 Matt Ho
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package wemo
import (
"encoding/xml"
"errors"
"fmt"
"html"
"io/ioutil"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"golang.org/x/net/context"
"github.com/savaki/httpctx"
)
// Device struct
type Device struct {
Host string
Logger func(string, ...interface{}) (int, error)
}
// DeviceInfo struct
type DeviceInfo struct {
Device *Device `json:"-"`
DeviceType string `xml:"deviceType" json:"device-type"`
FriendlyName string `xml:"friendlyName" json:"friendly-name"`
MacAddress string `xml:"macAddress" json:"mac-address"`
FirmwareVersion string `xml:"firmwareVersion" json:"firmware-version"`
SerialNumber string `xml:"serialNumber" json:"serial-number"`
UDN string `xml:"UDN" json:"UDN"`
EndDevices EndDevices
}
// DeviceInfos slice
type DeviceInfos []*DeviceInfo
func (d DeviceInfos) Len() int { return len(d) }
func (d DeviceInfos) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
func (d DeviceInfos) Less(i, j int) bool { return d[i].FriendlyName < d[j].FriendlyName }
func (d *Device) printf(format string, args ...interface{}) {
if d.Logger != nil {
d.Logger(format, args...)
}
}
func unmarshalDeviceInfo(data []byte) (*DeviceInfo, error) {
resp := struct {
DeviceInfo DeviceInfo `xml:"device"`
}{}
err := xml.Unmarshal(data, &resp)
if err != nil {
return nil, err
}
return &resp.DeviceInfo, nil
}
// FetchDeviceInfo from device
func (d *Device) FetchDeviceInfo(ctx context.Context) (*DeviceInfo, error) {
var data []byte
uri := fmt.Sprintf("http://%s/setup.xml", d.Host)
err := httpctx.NewClient().Get(ctx, uri, nil, &data)
if err != nil {
return nil, err
}
deviceInfo, err := unmarshalDeviceInfo(data)
if err != nil {
return nil, err
}
deviceInfo.Device = d
if deviceInfo.DeviceType == "urn:Belkin:device:bridge:1" {
deviceInfo.EndDevices = *deviceInfo.Device.GetBridgeEndDevices(deviceInfo.UDN)
}
return deviceInfo, nil
}
// GetBinaryState ...
func (d *Device) GetBinaryState() int {
message := newGetBinaryStateMessage()
response, err := post(d.Host, "basicevent", "GetBinaryState", message)
if err != nil {
d.printf("unable to fetch BinaryState => %s\n", err)
return -1
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
d.printf("GetBinaryState returned status code => %d\n", response.StatusCode)
return -1
}
data, err := ioutil.ReadAll(response.Body)
if err != nil {
d.printf("unable to read data => %s\n", err)
return -1
}
re := regexp.MustCompile(`.*(\d+).*`)
matches := re.FindStringSubmatch(string(data))
if len(matches) != 2 {
d.printf("unable to find BinaryState response in message => %s\n", string(data))
return -1
}
result, _ := strconv.Atoi(matches[1])
return result
}
// Off toggle state to Off
func (d *Device) Off() {
d.changeState(false)
}
// On toggle state to On
func (d *Device) On() {
d.changeState(true)
}
// Toggle state
func (d *Device) Toggle() {
if binaryState := d.GetBinaryState(); binaryState == 0 {
d.On()
} else {
d.Off()
}
}
// SetState is a wrapper for changeState, which allows errors to be exposed to caller.
func (d *Device) SetState(newState bool) error {
return d.changeState(newState)
}
func (d *Device) changeState(newState bool) error {
message := newSetBinaryStateMessage(newState)
response, err := post(d.Host, "basicevent", "SetBinaryState", message)
if err != nil {
log.Println("unable to SetBinaryState")
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
data, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Println("couldn't read body from message => " + err.Error())
return err
}
content := string(data)
gripe := fmt.Sprintf("changeState(%v) => %s", newState, content)
log.Println(gripe)
return errors.New(gripe)
}
return nil
}
// InsightParams ...
type InsightParams struct {
Power int // mW
}
// GetInsightParams ...
func (d *Device) GetInsightParams() *InsightParams {
message := newGetInsightParamsMessage()
response, err := post(d.Host, "insight", "GetInsightParams", message)
if err != nil {
d.printf("unable to fetch Power => %s\n", err)
return nil
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
d.printf("GetInsightParams returned status code => %d\n", response.StatusCode)
return nil
}
data, err := ioutil.ReadAll(response.Body)
if err != nil {
d.printf("unable to read data => %s\n", err)
return nil
}
//
//
// 8|1471416661|8|3244|3182|15377|19|7300|1011115|1011115.000000|8000
//
re := regexp.MustCompile(`.*(.+).*`)
matches := re.FindStringSubmatch(string(data))
if len(matches) != 2 {
d.printf("unable to find GetInsightParams response in message => %s\n", string(data))
return nil
}
split := strings.Split(matches[1], "|")
if len(split) < 7 {
d.printf("unable to parse InsightParams response => %s\n", string(data))
return nil
}
power, err := strconv.Atoi(split[7])
if err != nil {
d.printf("failed to parse power: %v", err)
}
return &InsightParams{
Power: power,
}
}
// EndDevices ...
type EndDevices struct {
DeviceListType string `xml:"Body>GetEndDevicesResponse>DeviceLists>DeviceLists>DeviceList>DeviceListType"`
EndDeviceInfo []EndDeviceInfo `xml:"Body>GetEndDevicesResponse>DeviceLists>DeviceLists>DeviceList>DeviceInfos>DeviceInfo"`
}
// EndDeviceInfo ...
type EndDeviceInfo struct {
DeviceIndex string `xml:"DeviceIndex"`
DeviceID string `xml:"DeviceID"`
FriendlyName string `xml:"FriendlyName"`
FirmwareVersion string `xml:"FirmwareVersion"`
CapabilityIDs string `xml:"CapabilityIDs"`
CurrentState string `xml:"CurrentState"`
Manufacturer string `xml:"Manufacturer"`
ModelCode string `xml:"ModelCode"`
ProductName string `xml:"productName"`
WeMoCertified string `xml:"WeMoCertified"`
}
// GetBridgeEndDevices ...
func (d *Device) GetBridgeEndDevices(uuid string) *EndDevices {
b := newGetBridgeEndDevices(uuid)
response, err := post(d.Host, "bridge", "GetEndDevices", b)
if err != nil {
d.printf("unable to fetch bridge end devices => %s\n", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
d.printf("GetBridgeEndDevices returned status code => %d\n", response.StatusCode)
}
data, err := ioutil.ReadAll(response.Body)
if err != nil {
d.printf("unable to read data => %s\n", err)
}
resp := EndDevices{}
data = []byte(html.UnescapeString(string(data)))
err = xml.Unmarshal(data, &resp)
if err != nil {
d.printf("Unmarshal Error: %s\n", err)
}
return &resp
}
//Bulb ...
func (d *Device) Bulb(id, cmd, value string, group bool) error {
if id == "" {
return errors.New("No ID provided")
}
capability := "10006"
if cmd == "dim" {
capability = "10008"
s, err := strconv.ParseInt(value, 10, 32)
if err != nil {
return err
}
if s > 255 || s < 0 {
return errors.New("Dim value is out of bounds 0-255")
}
}
if cmd == "on" {
value = "1"
} else if cmd == "off" {
value = "0"
}
message := newSetBulbStatus(id, capability, value, group)
response, err := post(d.Host, "bridge", "SetDeviceStatus", message)
if err != nil {
return errors.New("unable to SetDeviceStatus")
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return errors.New(string(response.StatusCode))
}
return nil
}
//BulbStatusList ...
type BulbStatusList struct {
DeviceStatus []DeviceStatus `xml:"Body>GetDeviceStatusResponse>DeviceStatusList>DeviceStatusList>DeviceStatus"`
}
//DeviceStatus ...
type DeviceStatus struct {
DeviceID string `xml:"DeviceID"`
CapabilityValue string `xml:"CapabilityValue"`
}
//GetBulbStatus return map of [DeviceID]status values, function returns a map of deviceid to status as it is possible to have several DeviceID results returned.
func (d *Device) GetBulbStatus(ids string) (map[string]string, error) {
result := make(map[string]string)
message := newGetBulbStatus(ids)
response, err := post(d.Host, "bridge", "GetDeviceStatus", message)
if err != nil {
return nil, fmt.Errorf("unable to fetch Bulb status => %s\n", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GetBulbStatus returned status code => %d\n", response.StatusCode)
}
data, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("unable to read data => %s\n", err)
}
data = []byte(html.UnescapeString(string(data)))
statusInfo := BulbStatusList{}
err = xml.Unmarshal(data, &statusInfo)
if err != nil {
return nil, fmt.Errorf("Unmarshal Error: %s\n", err)
}
for k := range statusInfo.DeviceStatus {
result[statusInfo.DeviceStatus[k].DeviceID] = statusInfo.DeviceStatus[k].CapabilityValue
}
return result, nil
}