package nest import ( "bytes" "encoding/json" "errors" "net/http" "net/url" "strings" "sync" "time" ) type API struct { Token string ExpiresAt time.Time } type Auth struct { AccessToken string } var cache = map[string]*API{} var cacheMu sync.Mutex func NewAPI(clientID, clientSecret, refreshToken string) (*API, error) { cacheMu.Lock() defer cacheMu.Unlock() key := clientID + ":" + clientSecret + ":" + refreshToken now := time.Now() if api := cache[key]; api != nil && now.Before(api.ExpiresAt) { return api, nil } data := url.Values{ "grant_type": []string{"refresh_token"}, "client_id": []string{clientID}, "client_secret": []string{clientSecret}, "refresh_token": []string{refreshToken}, } client := &http.Client{Timeout: time.Second * 5000} res, err := client.PostForm("https://www.googleapis.com/oauth2/v4/token", data) if err != nil { return nil, err } if res.StatusCode != 200 { return nil, errors.New("nest: wrong status: " + res.Status) } var resv struct { AccessToken string `json:"access_token"` ExpiresIn time.Duration `json:"expires_in"` Scope string `json:"scope"` TokenType string `json:"token_type"` } if err = json.NewDecoder(res.Body).Decode(&resv); err != nil { return nil, err } api := &API{ Token: resv.AccessToken, ExpiresAt: now.Add(resv.ExpiresIn * time.Second), } cache[key] = api return api, nil } func (a *API) GetDevices(projectID string) (map[string]string, error) { uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices" req, err := http.NewRequest("GET", uri, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+a.Token) client := &http.Client{Timeout: time.Second * 5000} res, err := client.Do(req) if err != nil { return nil, err } if res.StatusCode != 200 { return nil, errors.New("nest: wrong status: " + res.Status) } var resv struct { Devices []Device } if err = json.NewDecoder(res.Body).Decode(&resv); err != nil { return nil, err } devices := map[string]string{} for _, device := range resv.Devices { if len(device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols) == 0 { continue } if device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols[0] != "WEB_RTC" { continue } i := strings.LastIndexByte(device.Name, '/') if i <= 0 { continue } name := device.Traits.SdmDevicesTraitsInfo.CustomName devices[name] = device.Name[i+1:] } return devices, nil } func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, string, time.Time, error) { var reqv struct { Command string `json:"command"` Params struct { Offer string `json:"offerSdp"` } `json:"params"` } reqv.Command = "sdm.devices.commands.CameraLiveStream.GenerateWebRtcStream" reqv.Params.Offer = offer b, err := json.Marshal(reqv) if err != nil { return "", "", time.Time{}, err } uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices/" + deviceID + ":executeCommand" req, err := http.NewRequest("POST", uri, bytes.NewReader(b)) if err != nil { return "", "", time.Time{}, err } req.Header.Set("Authorization", "Bearer "+a.Token) client := &http.Client{Timeout: time.Second * 5000} res, err := client.Do(req) if err != nil { return "", "", time.Time{}, err } if res.StatusCode != 200 { return "", "", time.Time{}, errors.New("nest: wrong status: " + res.Status) } var resv struct { Results struct { Answer string `json:"answerSdp"` ExpiresAt time.Time `json:"expiresAt"` MediaSessionId string `json:"mediaSessionId"` } `json:"results"` } if err = json.NewDecoder(res.Body).Decode(&resv); err != nil { return "", "", time.Time{}, err } return resv.Results.Answer, resv.Results.MediaSessionId, resv.Results.ExpiresAt, nil } func (a *API) ExtendStream(projectID, deviceID, mediaSessionID string) (string, time.Time, error) { var reqv struct { Command string `json:"command"` Params struct { MediaSessionID string `json:"mediaSessionId"` } `json:"params"` } reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream" reqv.Params.MediaSessionID = mediaSessionID b, err := json.Marshal(reqv) if err != nil { return "", time.Time{}, err } uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices/" + deviceID + ":executeCommand" req, err := http.NewRequest("POST", uri, bytes.NewReader(b)) if err != nil { return "", time.Time{}, err } req.Header.Set("Authorization", "Bearer "+a.Token) client := &http.Client{Timeout: time.Second * 5000} res, err := client.Do(req) if err != nil { return "", time.Time{}, err } if res.StatusCode != 200 { return "", time.Time{}, errors.New("nest: wrong status: " + res.Status) } var resv struct { Results struct { ExpiresAt time.Time `json:"expiresAt"` MediaSessionId string `json:"mediaSessionId"` } `json:"results"` } if err = json.NewDecoder(res.Body).Decode(&resv); err != nil { return "", time.Time{}, err } return resv.Results.MediaSessionId, resv.Results.ExpiresAt, nil } type Device struct { Name string `json:"name"` Type string `json:"type"` //Assignee string `json:"assignee"` Traits struct { SdmDevicesTraitsInfo struct { CustomName string `json:"customName"` } `json:"sdm.devices.traits.Info"` SdmDevicesTraitsCameraLiveStream struct { VideoCodecs []string `json:"videoCodecs"` AudioCodecs []string `json:"audioCodecs"` SupportedProtocols []string `json:"supportedProtocols"` } `json:"sdm.devices.traits.CameraLiveStream"` //SdmDevicesTraitsCameraImage struct { // MaxImageResolution struct { // Width int `json:"width"` // Height int `json:"height"` // } `json:"maxImageResolution"` //} `json:"sdm.devices.traits.CameraImage"` //SdmDevicesTraitsCameraPerson struct { //} `json:"sdm.devices.traits.CameraPerson"` //SdmDevicesTraitsCameraMotion struct { //} `json:"sdm.devices.traits.CameraMotion"` //SdmDevicesTraitsDoorbellChime struct { //} `json:"sdm.devices.traits.DoorbellChime"` //SdmDevicesTraitsCameraClipPreview struct { //} `json:"sdm.devices.traits.CameraClipPreview"` } `json:"traits"` //ParentRelations []struct { // Parent string `json:"parent"` // DisplayName string `json:"displayName"` //} `json:"parentRelations"` }