Skip to content
15 changes: 15 additions & 0 deletions devices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package nest

/**
* Helper function to find a Thermostat in the Devices struct by its name
* Returns nil if no Thermostat was found
*/
func (d *Devices) FindThermostat(name string) *Thermostat {
for _, t := range d.Thermostats {
if t.Name == name {
return t
}
}

return nil
}
80 changes: 48 additions & 32 deletions nest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package nest

import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
)
Expand All @@ -23,6 +24,8 @@ const (
Heat
// HeatCool sets HvacMode to "heat-cool"
HeatCool
// Eco sets the HVacMode to "eco"
Eco
// Off sets HvacMode to "off"
Off
// Home sets Away mode to "home"
Expand All @@ -33,30 +36,64 @@ const (
AutoAway
)

/* Configure a httpClient that will handle redirects */
var httpClient = &http.Client{
CheckRedirect: func(redirRequest *http.Request, via []*http.Request) error {
// Go's http.DefaultClient does not forward headers when a redirect 3xx
// response is recieved. Thus, the header (which in this case contains the
// Authorization token) needs to be passed forward to the redirect
// destinations.
redirRequest.Header = via[0].Header

// Go's http.DefaultClient allows 10 redirects before returning an
// an error. We have mimicked this default behavior.s
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}

return nil
},
}

/*
New creates a new Nest client

client := New("1234", "STATE", "<secret>", "<auth-code>")
*/
func New(clientID string, state string, clientSecret string, authorizationCode string) *Client {
return &Client{
ID: clientID,
ClientID: clientID,
State: state,
Secret: clientSecret,
ClientSecret: clientSecret,
AuthorizationCode: authorizationCode,
AccessTokenURL: AccessTokenURL,
APIURL: APIURL,
}
}

func NewWithAuthorization(AccessToken string) *Client {
return &Client{
Token: AccessToken,
APIURL: APIURL,
}
}

/*
Authorize fetches and sets the Nest API token
https://developer.nest.com/documentation/how-to-auth

client.Authorize()
*/
func (c *Client) Authorize() *APIError {
resp, err := http.Post(c.authURL(), "application/x-www-form-urlencoded", nil)
req, err := http.NewRequest(http.MethodPost, c.AccessTokenURL, nil)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("client_id", c.ClientID)
req.Header.Add("client_secret", c.ClientSecret)
req.Header.Add("grant_type", "authorization_code")

var client = &http.Client{}

resp, err := client.Do(req)
if err != nil {
return &APIError{
Error: "http_error",
Expand Down Expand Up @@ -119,30 +156,17 @@ func (c *Client) Devices() (*Devices, *APIError) {

// getDevices does an HTTP get with or without a stream on devices
func (c *Client) getDevices(action int) (*http.Response, error) {
if c.RedirectURL == "" {
req, _ := http.NewRequest("GET", c.APIURL+"/devices.json?auth="+c.Token, nil)
resp, err := http.DefaultClient.Do(req)
if resp.Request.URL != nil {
c.RedirectURL = resp.Request.URL.Scheme + "://" + resp.Request.URL.Host
}
return resp, err
}
req, err := http.NewRequest(http.MethodGet, c.APIURL+"/devices.json", nil)
req.Header.Add("Content-Type", "\"application/json\"")
req.Header.Add("Authorization", c.Token)

req, _ := http.NewRequest("GET", c.RedirectURL+"/devices.json?auth="+c.Token, nil)
if action == Stream {
req.Header.Set("Accept", "text/event-stream")
}
resp, err := http.DefaultClient.Do(req)
return resp, err
}

// authURL sets the full authorization URL for the Nest API
func (c *Client) authURL() string {
location := c.AccessTokenURL + "?code=" + c.AuthorizationCode
location += "&client_id=" + c.ID
location += "&client_secret=" + c.Secret
location += "&grant_type=authorization_code"
return location
response, err := httpClient.Do(req)

return response, err
}

// associateClientToDevices ensures each device knows its client details
Expand All @@ -153,15 +177,7 @@ func (c *Client) associateClientToDevices(devices *Devices) {
for _, value := range devices.SmokeCoAlarms {
value.Client = c
}
}

// setRedirectURL sets the URL if not already set
func (c *Client) setRedirectURL() (int, error) {
if c.RedirectURL == "" {
resp, err := c.getDevices(NoStream)
if err != nil || resp.StatusCode != 200 {
return resp.StatusCode, err
}
for _, value := range devices.Cameras {
value.Client = c
}
return 0, nil
}
8 changes: 4 additions & 4 deletions nest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ func TestNew(t *testing.T) {
client := New(ClientID, State, ClientSecret, BadAuthorizationCode)
client.AccessTokenURL = ts.URL
err := client.Authorize()
So(client.ID, ShouldEqual, ClientID)
So(client.ClientID, ShouldEqual, ClientID)
So(client.State, ShouldEqual, State)
So(client.Secret, ShouldEqual, ClientSecret)
So(client.ClientSecret, ShouldEqual, ClientSecret)
So(client.AuthorizationCode, ShouldEqual, BadAuthorizationCode)
Convey("Given we gave the oauth2 API a bad authorization code we should get an error", func() {
So(err, ShouldNotBeNil)
Expand All @@ -38,9 +38,9 @@ func TestNew(t *testing.T) {
client := New(ClientID, State, ClientSecret, AuthorizationCode)
client.AccessTokenURL = ts.URL
err := client.Authorize()
So(client.ID, ShouldEqual, ClientID)
So(client.ClientID, ShouldEqual, ClientID)
So(client.State, ShouldEqual, State)
So(client.Secret, ShouldEqual, ClientSecret)
So(client.ClientSecret, ShouldEqual, ClientSecret)
So(client.AuthorizationCode, ShouldEqual, AuthorizationCode)
Convey("Given we gave the oauth2 API a valid authorization code we get an access token back", func() {
So(err, ShouldBeNil)
Expand Down
3 changes: 1 addition & 2 deletions stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ import (
/*
DevicesStream emits events from the Nest devices REST streaming API

client.DevicesStream(func(event *Devices) {
client.DevicesStream(func(event *Devices, err error) {
fmt.Println(event)
})
*/
func (c *Client) DevicesStream(callback func(devices *Devices, err error)) {
c.setRedirectURL()
for {
c.streamDevices(callback)
}
Expand Down
34 changes: 31 additions & 3 deletions structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@ type APIError struct {

// Client represents a client object
type Client struct {
ID string
ClientID string
State string
AuthorizationCode string
Secret string
ClientSecret string
Token string
ExpiresIn int
AccessTokenURL string
APIURL string
RedirectURL string
}

// Access represents a Nest access token object
Expand Down Expand Up @@ -56,6 +55,7 @@ type StructuresEvent struct {
type Devices struct {
Thermostats map[string]*Thermostat `json:"thermostats,omitempty"`
SmokeCoAlarms map[string]*SmokeCoAlarm `json:"smoke_co_alarms,omitempty"`
Cameras map[string]*Camera `json:"cameras, omitempty"`
}

/*
Expand Down Expand Up @@ -130,6 +130,34 @@ type SmokeCoAlarm struct {
Client *Client
}

/*
Camera represents a Nest camera object
*/
type Camera struct {
Name string `json:"name"`
SoftwareVersion string `json:"software_version"`
WhereID string `json:"where_id"`
DeviceID string `json:"device_id"`
StructureID string `json:"structure_id"`
IsOnline bool `json:"is_online"`
IsStreaming bool `json:"is_streaming"`
IsAudioInputEnabled bool `json:"is_audio_input_enabled"`
LastIsOnlineChange time.Time `json:"last_is_online_change"`
IsVideoHistoryEnabled bool `json:"is_video_history_enabled"`
LastEvent struct {
HasSound bool `json:"has_sound"`
HasMotion bool `json:"has_motion"`
HasPerson bool `json:"has_person"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
} `json:"last_event"`
WhereName string `json:"where_name"`
NameLong string `json:"name_long"`
WebURL string `json:"web_url"`
AppURL string `json:"app_url"`
Client *Client
}

/*
Structure represents a Next structure object
https://developer.nest.com/documentation/api#structures
Expand Down
39 changes: 19 additions & 20 deletions structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ Structures Stream emits events from the Nest structures REST streaming API
})
*/
func (c *Client) StructuresStream(callback func(structures map[string]*Structure, err error)) {
c.setRedirectURL()
for {
c.streamStructures(callback)
}
Expand Down Expand Up @@ -95,8 +94,11 @@ func (s *Structure) SetETA(tripID string, begin time.Time, end time.Time) *APIEr
EstimatedArrivalWindowEnd: end,
}
data, _ := json.Marshal(eta)
req, _ := http.NewRequest("PUT", s.Client.RedirectURL+"/structures/"+s.StructureID+"/eta.json?auth="+s.Client.Token, bytes.NewBuffer(data))
resp, err := http.DefaultClient.Do(req)
req, _ := http.NewRequest("PUT", s.Client.APIURL+"/structures/"+s.StructureID+"/eta.json", bytes.NewBuffer(data))
req.Header.Add("Authorization", s.Client.Token)

resp, err := httpClient.Do(req)

if err != nil {
apiError := &APIError{
Error: "http_error",
Expand Down Expand Up @@ -169,30 +171,28 @@ func (c *Client) watchStructuresStream(resp *http.Response, callback func(struct

// getStructures does an HTTP get
func (c *Client) getStructures(action int) (*http.Response, error) {
if c.RedirectURL == "" {
req, _ := http.NewRequest("GET", c.APIURL+"/structures.json?auth="+c.Token, nil)
resp, err := http.DefaultClient.Do(req)
if resp.Request.URL != nil {
c.RedirectURL = resp.Request.URL.Scheme + "://" + resp.Request.URL.Host
}
return resp, err
}
req, err := http.NewRequest(http.MethodGet, c.APIURL+"/structures.json", nil)
req.Header.Add("Content-Type", "\"application/json\"")
req.Header.Add("Authorization", c.Token)

req, _ := http.NewRequest("GET", c.RedirectURL+"/structures.json?auth="+c.Token, nil)
if action == Stream {
req.Header.Set("Accept", "text/event-stream")
}
resp, err := http.DefaultClient.Do(req)
return resp, err

response, err := httpClient.Do(req)

return response, err
}

// setStructure sends the request to the Nest REST API
func (s *Structure) setStructure(body []byte) *APIError {
url := s.Client.RedirectURL + "/structures/" + s.StructureID + "?auth=" + s.Client.Token
client := &http.Client{}
req, _ := http.NewRequest("PUT", url, bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
req, err := http.NewRequest(http.MethodPut, s.Client.APIURL+"/structures/"+s.StructureID, bytes.NewBuffer(body))
req.Header.Add("Content-Type", "\"application/json\"")
req.Header.Add("Authorization", s.Client.Token)

resp, err := httpClient.Do(req)
defer resp.Body.Close()

if err != nil {
apiError := &APIError{
Error: "http_error",
Expand All @@ -201,7 +201,6 @@ func (s *Structure) setStructure(body []byte) *APIError {
return apiError
}
body, _ = ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if resp.StatusCode == 200 {
structure := &Structure{}
json.Unmarshal(body, structure)
Expand Down
34 changes: 28 additions & 6 deletions thermostat.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,36 @@ func (t *Thermostat) SetHvacMode(mode int) *APIError {
requestMode["hvac_mode"] = "heat"
case HeatCool:
requestMode["hvac_mode"] = "heat-cool"
case Eco:
requestMode["hvac_mode"] = "eco"
case Off:
requestMode["hvac_mode"] = "off"
default:
return generateAPIError("Invalid HvacMode requested - must be cool, heat, heat-cool or off")
return generateAPIError("Invalid HvacMode requested - must be cool, heat, heat-cool, eco, or off")
}
body, _ := json.Marshal(requestMode)
return t.setThermostat(body)
}

func (t *Thermostat) GetHvacMode() (mode int, err *APIError) {
switch t.HvacMode {
case "cool":
mode = Cool
case "heat":
mode = Heat
case "heat-cool":
mode = HeatCool
case "eco":
mode = Eco
case "off":
mode = Off
default:
err = generateAPIError("Invalid HvacMode found, was " + t.HvacMode)
}

return
}

/*
SetTargetTempC sets the thermostat to an intended temp in celcius
https://developer.nest.com/documentation/api#target_temperature_c
Expand Down Expand Up @@ -114,11 +135,12 @@ func (t *Thermostat) SetTargetTempHighLowF(high int, low int) *APIError {

// setThermostat sends the request to the Nest REST API
func (t *Thermostat) setThermostat(body []byte) *APIError {
url := t.Client.RedirectURL + "/devices/thermostats/" + t.DeviceID + "?auth=" + t.Client.Token
client := &http.Client{}
req, _ := http.NewRequest("PUT", url, bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
req, err := http.NewRequest(http.MethodPut, t.Client.APIURL+"/devices/thermostats/"+t.DeviceID, bytes.NewBuffer(body))
req.Header.Add("Content-Type", "\"application/json\"")
req.Header.Add("Authorization", t.Client.Token)

resp, err := httpClient.Do(req)

if err != nil {
apiError := &APIError{
Error: "http_error",
Expand Down
Loading