aboutsummaryrefslogtreecommitdiff
path: root/bridge.go
blob: 581e180454d7145f197b2b960c1800aa57feb789 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
package hue

// username: 319b36233bd2328f3e40731b23479207
import (
    "log"
    "os"
    "encoding/xml"
    "encoding/json"
    "net/http"
    "io/ioutil"
    "runtime"
    "fmt"
    "strings"
    "bytes"
    "io"
    "errors"
)

type Bridge struct {
    IPAddress   string
    Username    string
    Info        BridgeInfo
}

type BridgeInfo struct {
    XMLName	    xml.Name	`xml:"root"`
    Device      Device      `xml:"device"`
}

type Device struct {
    XMLName	            xml.Name    `xml:"device"`
    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"`
}

func (self *Bridge) Get(path string) ([]byte, io.Reader, error) {
    resp, err := http.Get("http://" + self.IPAddress + path)
    if self.Error(resp, err) {
        return []byte{}, nil, err
    }
    return handleResponse(resp)
}

// bridge.Post will send an http POST to the bridge with
// a body formatted with parameters.
func (self *Bridge) Post(path string, params map[string]string) ([]byte, io.Reader, error) {
    // Add the params to the request
    request, err := json.Marshal(params)
    if err != nil {
        trace("", err)
        return []byte{}, nil, nil
    }

    // Send the request and handle the response
    uri := fmt.Sprintf("http://" + self.IPAddress + path)
    resp, err := http.Post(uri, "text/json", bytes.NewReader(request))
    if self.Error(resp, err) {
        return []byte{}, nil, nil
    }
    return handleResponse(resp)
}

// HandleResponse manages the http.Response content from a
// bridge Get/Put/Post/Delete by checking it for errors
// and invalid return types.
func handleResponse(resp *http.Response) ([]byte, io.Reader, error) {
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        trace("Error parsing bridge description xml.", nil)
        return []byte{}, nil, err
    }
    reader := bytes.NewReader(body)

    return body, reader, nil
}

// bridge.Error handles all bridge response status errors
func (self *Bridge) Error(resp *http.Response, err error) (bool) {
    if err != nil {
        trace("", err)
        return true
    } else if resp.StatusCode != 200 {
        // TODO: handle other status codes
        trace(fmt.Sprintf("Bridge status error: %d", resp.StatusCode), nil)
        return true
    }
    return false
}

// Error Struct
// http://www.developers.meethue.com/documentation/error-messages
type Error struct {
    ID          int
    Description string
    Details     string
}

// Error Return Values
// http://www.developers.meethue.com/documentation/error-messages
var (
    // Not from Hue documentation
    NoErr = Error{}
    ErrResponse = Error{0, "Could not read or parse response from bridge",
        "Data structure for return type may be invalid."}


    // Generic Errors from Hue SDK
    ErrAuth         = Error{1, "Unauthorized User",
        `This will be returned if an invalid username is used in the request,
        or if the username does not have the rights to modify the resource.`}
    ErrJson         = Error{2, "Body contains invalid JSON.",
        "This will be returned if the body of the message contains invalid JSON."}
    ErrResource     = Error{3, "Resource, <resource>, not available.",
        `This will be returned if the addressed resource does not exist.
         E.g. the user specifies a light ID that does not exist.`}
    ErrMethod       = Error{4, "Method, <method_name>, not available for resource, <resource>",
        `This will be returned if the method (GET/POST/PUT/DELETE)
         used is not supported by the URL e.g. DELETE is not
         supported on the /config resource`}
    ErrParamMissing = Error{5, "Missing parameters in body.", `Will be returned if
         required parameters are not present in the message body. The presence
         of invalid parameters should not trigger this error as long as all
         required parameters are present.`}
    ErrParamNA      = Error{6, "Parameter, <parameter>, not available.",
        `This will be returned if a parameter sent in the message body does
         not exist. This error is specific to PUT commands; invalid parameters
         in other commands are simply ignored.`}
    ErrParamInvalid = Error{7, "Invalid value, <value>, for parameter, <parameter>",
        `This will be returned if the value set for a parameter is of the
         incorrect format or is out of range.`}
    ErrParamStatic  = Error{8, "Parameter, <parameter>, is not modifiable",
        `This will be returned if an attempt to modify
         a read only parameter is made.`}
    ErrItemOverflow = Error{11, "Too many items to list.",
        "List in request contains too many items"}
    ErrPortalConn   = Error{12, "Portal connection required.",
        `Command requires portal connection.
        Returned if portalservices is “false“ or the portal connection is down`}
    ErrorInternal   = Error{901, "Internal error, <error code>",
        `This will be returned if there is an internal error in the
         processing of the command. This indicates an error in the
         bridge, not in the message being sent.`}

    // Command Specific Errors from Hue SDK
    ErrLink   = Error{101, "Link button not pressed.",
        `/config/linkbutton is false. Link button has
         not been pressed in last 30 seconds.`}
    ErrDHCP   = Error{110, "DHCP cannot be disabled.",
        "DHCP can only be disabled if there is a valid static IP configuration"}
    ErrUpdate = Error{111, "Invalid updatestate.",
        "Checkforupdate can only be set in updatestate 0 and 1."}
    // TODO: Need to add 201, 301, 305, 306, 402, 403, 501, 502, 601...
)

// NewBridge defines hardware that is compatible with Hue.
func NewBridge(ip string, username string) *Bridge {
    bridge := Bridge {
        IPAddress: ip,
        Username: username,
    }

    GetBridgeInfo(&bridge)
    return &bridge
}

// GetBridgeInfo retreives the description.xml file from the bridge.
func GetBridgeInfo(self *Bridge) error {
    _, reader, err := self.Get("/description.xml")
    if err != nil {
        return err
    }
    data := BridgeInfo{}
    err = xml.NewDecoder(reader).Decode(&data)
    if err != nil {
        return err
    }
    self.Info = data
    return nil
}

// CreateUser posts to ./api on the bridge to create a new whitelisted user.
func CreateUser(bridge *Bridge, deviceType string) (string, error) {
    // Send an HTTP POST with the body content
    params := map[string]string{"devicetype": deviceType}
    body, _, err := bridge.Post("/api", params)
    if err != nil {
        return "", err
    }

    // Parse the result and return it
    result := string(body)
    errFound := strings.Contains(result, "error")
    noLink := strings.Contains(result, "link button not pressed")
    if errFound && noLink {
        return "", errors.New("Bridge link button not pressed.")
    }
    return "", nil
}

// Log the date, time, file location, line number, and function.
// Message can be "" or Err can be nil (not both)
func trace(message string, err error) {
    pc := make([]uintptr, 10)
    runtime.Callers(2, pc)
    f := runtime.FuncForPC(pc[0])
    file, line := f.FileLine(pc[0])
    if err != nil {
        log.Printf("%s:%d %s: %s\n", file, line, f.Name(), err)
    } else {
        log.Printf("%s:%d %s: %s\n", file, line, f.Name(), message)
    }
}