big refactorisation

This commit is contained in:
Laurent Le Houerou 2020-05-27 12:49:40 +04:00
parent d68ae1fb4c
commit 2dd8c960d1
23 changed files with 746 additions and 2079 deletions

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2017 jeffreydwalter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,12 +0,0 @@
all: test build
build:
go build -v ./...
test:
go test -v ./...
clean:
go clean

130
README.md
View File

@ -1,130 +0,0 @@
# arlo-go
![](https://godoc.org/github.com/jeffreydwalter/arlo-go?status.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/jeffreydwalter/arlo-go)](https://goreportcard.com/report/github.com/jeffreydwalter/arlo-go)
![](gopher-arlo.png)
> Go package for interacting with Netgear's Arlo camera system.
---
### Now in Go!
I love Go. That is why I decided to write this library! I am the creator of the first [arlo](https://github.com/jeffreydwalter/arlo) library written in Python.
My goal is to bring parity to the Python version asap. If you know what you're doing in Go, I would appreciate any feedback on the general structure of the library, bugs found, contributions, etc.
---
It is by no means complete, although it does expose quite a bit of the Arlo interface in an easy to use Go pacakge. As such, this package does not come with unit tests (feel free to add them, or I will eventually) or guarantees.
**All [contributions](https://github.com/jeffreydwalter/arlo-go/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) are welcome and appreciated!**
**Please, feel free to [contribute](https://github.com/jeffreydwalter/arlo-go/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) to this repo or buy Jeff a beer!** [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=R77B7UXMLA6ML&lc=US&item_name=Jeff%20Needs%20Beer&item_number=buyjeffabeer&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted)
---
### Generous Benefactors (Thank you!)
No beers for Jeff yet! 🍺
---
### Awesomely Smart Contributors (Thank you!)
* [bwagner5](https://github.com/bwagner5) - Dec 8, 2019 - Migrated package from dep to go modules.
If You'd like to make a diffrence in the world and get your name on this most prestegious list, have a look at our [help wanted](https://github.com/jeffreydwalter/arlo-go/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) section!
---
### Filing an Issue
Please read the [Issue Guidelines and Policies](https://github.com/jeffreydwalter/arlo-go/wiki/Issue-Guidelines-and-Policies) wiki page **BEFORE** you file an issue. Thanks.
---
## Install
```bash
# Install latest stable package
$ go get github.com/jeffreydwalter/arlo-go
```
```golang
package main
import (
"fmt"
"log"
"sync"
"time"
"github.com/jeffreydwalter/arlo-go"
)
const (
USERNAME = "user@example.com"
PASSWORD = "supersecretpassword"
)
func main() {
// Instantiating the Arlo object automatically calls Login(), which returns an oAuth token that gets cached.
// Subsequent successful calls to login will update the oAuth token.
arlo, err := arlo.Login(USERNAME, PASSWORD)
if err != nil {
log.Printf("Failed to login: %s\n", err)
return
}
// At this point you're logged into Arlo.
now := time.Now()
start := now.Add(-7 * 24 * time.Hour)
// Get all of the recordings for a date range.
library, err := arlo.GetLibrary(start, now)
if err != nil {
log.Println(err)
return
}
// We need to wait for all of the recordings to download.
var wg sync.WaitGroup
for _, recording := range *library {
// Let the wait group know about the go routine that we're about to run.
wg.Add(1)
// The go func() here makes this script download the files concurrently.
// If you want to download them serially for some reason, just remove the go func() call.
go func() {
fileToWrite, err := os.Create(fmt.Sprintf("downloads/%s_%s.mp4", time.Unix(0, recording.UtcCreatedDate*int64(time.Millisecond)).Format(("2006-01-02_15.04.05")), recording.UniqueId))
defer fileToWrite.Close()
if err != nil {
log.Fatal(err)
}
// The videos produced by Arlo are pretty small, even in their longest, best quality settings.
// DownloadFile() efficiently streams the file from the http.Response.Body directly to a file.
if err := arlo.DownloadFile(recording.PresignedContentUrl, fileToWrite); err != nil {
log.Println(err)
} else {
log.Printf("Downloaded video %s from %s", recording.CreatedDate, recording.PresignedContentUrl)
}
// Mark this go routine as done in the wait group.
wg.Done()
}()
}
// Wait here until all of the go routines are done.
wg.Wait()
// The below example demonstrates how you could delete the cloud recordings after downloading them.
// Simply uncomment the below code to start using it.
// Delete all of the videos you just downloaded from the Arlo library.
// Notice that you can pass the "library" object we got back from the GetLibrary() call.
/* if err := arlo.BatchDeleteRecordings(library); err != nil {
log.Println(err)
return
} */
// If we made it here without an exception, then the videos were successfully deleted.
/* log.Println("Batch deletion of videos completed successfully.") */
}
```
** (coming soon) For more code examples check out the [wiki](https://github.com/jeffreydwalter/arlo-go/wiki)**

218
arlo.go
View File

@ -17,121 +17,106 @@
package arlo package arlo
import ( import (
"context"
"fmt" "fmt"
"net/http"
"sync" "sync"
"time" "time"
"github.com/jeffreydwalter/arlo-go/internal/request" "github.com/go-resty/resty/v2"
"github.com/pkg/errors"
) )
type Arlo struct { type Arlo struct {
user string user string
pass string pass string
client *request.Client client *resty.Client
Account Account Account Account
Basestations Basestations Basestations Basestations
Cameras Cameras Cameras Cameras
rwmutex sync.RWMutex rwmutex sync.RWMutex
} }
func newArlo(user string, pass string) (arlo *Arlo) { func NewArlo() (arlo *Arlo) {
// Add important headers. c := resty.New().
baseHeaders := make(http.Header) SetHostURL(BaseUrl).
baseHeaders.Add("DNT", "1") SetTimeout(30 * time.Second)
baseHeaders.Add("schemaVersion", "1")
baseHeaders.Add("Host", "my.arlo.com")
baseHeaders.Add("Referer", "https://my.arlo.com/")
c, _ := request.NewClient(BaseUrl, baseHeaders)
return &Arlo{ return &Arlo{
user: user,
pass: pass,
client: c, client: c,
} }
} }
func Login(user string, pass string) (arlo *Arlo, err error) { func (a *Arlo) Login(ctx context.Context, user string, pass string) error {
arlo = newArlo(user, pass)
body := map[string]string{"email": arlo.user, "password": arlo.pass}
resp, err := arlo.post(LoginV2Uri, "", body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to login")
}
defer resp.Body.Close()
var loginResponse LoginResponse var loginResponse LoginResponse
if err := resp.Decode(&loginResponse); err != nil { _, err := a.client.R().
return nil, err SetBody(map[string]string{
"email": user,
"password": pass,
}).
SetResult(&loginResponse). // or SetResult(AuthSuccess{}).
Post(LoginV2Uri)
if err != nil {
return fmt.Errorf("failed to login: %v", err)
}
if !loginResponse.Success {
return fmt.Errorf("failed to login")
} }
if loginResponse.Success {
// Cache the auth token. // Cache the auth token.
arlo.client.AddHeader("Authorization", loginResponse.Data.Token) a.client.SetHeader("Authorization", loginResponse.Data.Token)
// Save the account info with the arlo struct. // Save the account info with the arlo struct.
arlo.Account = loginResponse.Data a.Account = loginResponse.Data
// Get the devices, which also caches them on the arlo object. // Get the devices, which also caches them on the arlo object.
if _, err := arlo.GetDevices(); err != nil { if _, err := a.GetDevices(ctx); err != nil {
return nil, errors.WithMessage(err, "failed to login") return fmt.Errorf("getting devices: %v", err)
}
} else {
return nil, errors.New("failed to login")
} }
return arlo, nil return nil
} }
func (a *Arlo) Logout() error { func (a *Arlo) Logout() error {
resp, err := a.put(LogoutUri, "", nil, nil)
return checkRequest(resp, err, "failed to logout") var response Status
_, err := a.client.R().
SetResult(&response).
Put(LogoutUri)
if err != nil {
return fmt.Errorf("logging out: %v", err)
}
if response.Success == false {
return fmt.Errorf("logging out: %s", response.Reason)
}
return nil
} }
func (a *Arlo) CheckSession() (session *Session, err error) { func (a *Arlo) GetSession() (*Session, error) {
msg := "failed to get session"
resp, err := a.get(SessionUri, "", nil)
if err != nil {
return nil, errors.WithMessage(err, msg)
}
defer resp.Body.Close()
var response SessionResponse var response SessionResponse
if err := resp.Decode(&response); err != nil { _, err := a.client.R().
return nil, err SetResult(&response).
Get(SessionUri)
if err != nil {
return nil, fmt.Errorf("getting session: %v", err)
} }
if response.Success == false { if response.Success == false {
return nil, errors.WithMessage(errors.New(response.Reason), msg) return nil, fmt.Errorf("getting session: %s", response.Reason)
} }
return &response.Data, nil return &response.Data, nil
} }
// GetDevices returns an array of all devices. func (a *Arlo) GetDevices(ctx context.Context) (*Devices, error) {
// When you call Login, this method is called and all devices are cached in the arlo object.
func (a *Arlo) GetDevices() (devices *Devices, err error) {
resp, err := a.get(fmt.Sprintf(DevicesUri, time.Now().Format("20060102")), "", nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to get devices")
}
defer resp.Body.Close()
var response DeviceResponse var response DeviceResponse
if err := resp.Decode(&response); err != nil { _, err := a.client.R().
return nil, err SetResult(&response).
Get(fmt.Sprintf(DevicesUri, time.Now().Format("20060102")))
if err != nil {
return nil, fmt.Errorf("getting devices: %v", err)
} }
if !response.Success { if !response.Success {
return nil, errors.New("failed to get devices") return nil, fmt.Errorf("failed to get devices")
} }
if len(response.Data) == 0 { if len(response.Data) == 0 {
return nil, errors.New("no devices found") return nil, fmt.Errorf("no device found")
} }
// Cache a pointer to the arlo object with each device. // Cache a pointer to the arlo object with each device.
@ -140,74 +125,69 @@ func (a *Arlo) GetDevices() (devices *Devices, err error) {
} }
// Disconnect all of the basestations from the EventStream. // Disconnect all of the basestations from the EventStream.
for i := range a.Basestations { for _, basestation := range a.Basestations {
if err := a.Basestations[i].Disconnect(); err != nil { if err := basestation.Disconnect(); err != nil {
return nil, errors.WithMessage(err, "failed to get devices") return nil, fmt.Errorf("disconnecting device %s: %v", basestation.DeviceName, err)
} }
} }
a.rwmutex.Lock() a.rwmutex.Lock()
// Cache the devices as their respective types. // Cache the devices as their respective types.
a.Cameras = *response.Data.GetCameras() a.Cameras = response.Data.GetCameras()
a.Basestations = *response.Data.GetBasestations() a.Basestations = response.Data.GetBasestations()
a.rwmutex.Unlock() a.rwmutex.Unlock()
// subscribe each basestation to the EventStream. // subscribe each basestation to the EventStream.
for i := range a.Basestations { for _, basestation := range a.Basestations {
if err := a.Basestations[i].Subscribe(); err != nil { if err := basestation.Subscribe(ctx); err != nil {
return nil, errors.WithMessage(err, "failed to get devices") return nil, fmt.Errorf("subscribing device %s: %v", basestation.DeviceName, err)
} }
} }
return &response.Data, nil return &response.Data, nil
} }
// GetProfile returns the user profile for the currently logged in user. // GetProfile returns the user profile for the currently logged in user.
func (a *Arlo) GetProfile() (profile *UserProfile, err error) { func (a *Arlo) GetProfile() (*UserProfile, error) {
resp, err := a.get(ProfileUri, "", nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to get user profile")
}
defer resp.Body.Close()
var response UserProfileResponse var response UserProfileResponse
if err := resp.Decode(&response); err != nil { _, err := a.client.R().
return nil, err SetResult(&response).
} Get(ProfileUri)
if !response.Success { if err != nil {
return nil, errors.New("failed to get user profile") return nil, fmt.Errorf("getting user profile: %v", err)
}
if response.Success == false {
return nil, fmt.Errorf("getting user profile: %s", response.Reason)
} }
return &response.Data, nil return &response.Data, nil
} }
// UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given. //// UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given.
func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) error { //func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) error {
resp, err := a.post(CameraOrderUri, "", d, nil) // resp, err := a.post(CameraOrderUri, "", d, nil)
return checkRequest(resp, err, "failed to display order") // return checkRequest(resp, err, "failed to display order")
} //}
//
// UpdateProfile takes a first and last name, and updates the user profile with that information. //// UpdateProfile takes a first and last name, and updates the user profile with that information.
func (a *Arlo) UpdateProfile(firstName, lastName string) error { //func (a *Arlo) UpdateProfile(firstName, lastName string) error {
body := map[string]string{"firstName": firstName, "lastName": lastName} // body := map[string]string{"firstName": firstName, "lastName": lastName}
resp, err := a.put(ProfileUri, "", body, nil) // resp, err := a.put(ProfileUri, "", body, nil)
return checkRequest(resp, err, "failed to update profile") // return checkRequest(resp, err, "failed to update profile")
} //}
//
func (a *Arlo) UpdatePassword(pass string) error { //func (a *Arlo) UpdatePassword(pass string) error {
body := map[string]string{"currentPassword": a.pass, "newPassword": pass} // body := map[string]string{"currentPassword": a.pass, "newPassword": pass}
resp, err := a.post(UpdatePasswordUri, "", body, nil) // resp, err := a.post(UpdatePasswordUri, "", body, nil)
if err := checkRequest(resp, err, "failed to update password"); err != nil { // if err := checkRequest(resp, err, "failed to update password"); err != nil {
return err // return err
} // }
//
a.pass = pass // a.pass = pass
//
return nil // return nil
} //}
//
func (a *Arlo) UpdateFriends(f Friend) error { //func (a *Arlo) UpdateFriends(f Friend) error {
resp, err := a.put(FriendsUri, "", f, nil) // resp, err := a.put(FriendsUri, "", f, nil)
return checkRequest(resp, err, "failed to update friends") // return checkRequest(resp, err, "failed to update friends")
} //}

View File

@ -1,17 +0,0 @@
/*
* Copyright (c) 2018 Jeffrey Walter <jeffreydwalter@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package arlo

View File

@ -1,522 +0,0 @@
/*
* Copyright (c) 2018 Jeffrey Walter <jeffreydwalter@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package arlo
import (
"fmt"
"time"
"github.com/pkg/errors"
)
/*
The methods in this file are all related to Arlo Baby (afaik).
They may apply to other camera types that have audio playback or nightlight capabilities.
*/
/*
The follow methods are all related to the audio features of Arlo Baby.
*/
// SetVolume sets the volume of the audio playback to a level from 0-100.
func (c *Camera) SetVolume(volume int) (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: SpeakerProperties{
Speaker: VolumeProperties{
Mute: false,
Volume: volume,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to set audio volume"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
}
// Mute mutes the audio playback.
func (c *Camera) Mute() (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: SpeakerProperties{
Speaker: VolumeProperties{
Mute: true,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to mute audio"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
}
// UnMute un-mutes the audio playback.
func (c *Camera) UnMute() (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: SpeakerProperties{
Speaker: VolumeProperties{
Mute: false,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to un-mute audio"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
}
// Play plays an audio track, specified by the track ID, from a given position starting from 0 seconds.
func (c *Camera) Play(trackId string, position int) error {
// Defaulting to 'hugh little baby', which is a supplied track. Hopefully, the ID is the same for everyone.
if trackId == "" {
trackId = "2391d620-e491-4412-99f6-e9a40d6046ed"
}
if position < 0 {
position = 0
}
payload := EventStreamPayload{
Action: "playTrack",
Resource: "audioPlayback/player",
PublishResponse: false,
Properties: PlayTrackProperties{trackId, position},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to play audio"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return errors.WithMessage(err, msg)
}
if err := b.NotifyEventStream(payload, msg); err != nil {
return errors.WithMessage(err, msg)
}
return nil
}
// Pause pauses audio playback.
func (c *Camera) Pause() error {
payload := EventStreamPayload{
Action: "pause",
Resource: "audioPlayback/player",
PublishResponse: false,
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to pause audio"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return errors.WithMessage(err, msg)
}
if err := b.NotifyEventStream(payload, msg); err != nil {
return errors.WithMessage(err, msg)
}
return nil
}
// Next moves audio playback to the next track.
func (c *Camera) Next() error {
payload := EventStreamPayload{
Action: "nextTrack",
Resource: "audioPlayback/player",
PublishResponse: false,
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to skip audio"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return errors.WithMessage(err, msg)
}
if err := b.NotifyEventStream(payload, msg); err != nil {
return errors.WithMessage(err, msg)
}
return nil
}
// Shuffle toggles the audio play back mode to shuffle or not.
func (c *Camera) Shuffle(on bool) (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: "audioPlayback/config",
PublishResponse: true,
Properties: ShuffleProperties{
Config: BaseShuffleProperties{
ShuffleActive: on,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
var msg string
if on {
msg = "failed to enable shuffle"
} else {
msg = "failed to disable shuffle"
}
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
}
func (c *Camera) Continuous() (response *EventStreamResponse, err error) {
return c.SetLoopBackMode("continuous")
}
func (c *Camera) SingleTrack() (response *EventStreamResponse, err error) {
return c.SetLoopBackMode("singleTrack")
}
func (c *Camera) SetLoopBackMode(loopbackMode string) (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: "audioPlayback/config",
PublishResponse: true,
Properties: LoopbackModeProperties{
Config: BaseLoopbackModeProperties{
LoopbackMode: loopbackMode,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to set loop back mode to %s"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, fmt.Sprintf(msg, loopbackMode))
}
return b.makeEventStreamRequest(payload, msg)
}
func (c *Camera) GetAudioPlayback() (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "get",
Resource: "audioPlayback",
PublishResponse: false,
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to get audio playback"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
}
func (c *Camera) EnableSleepTimer(sleepTime int64 /* milliseconds */, sleepTimeRel int) (response *EventStreamResponse, err error) {
if sleepTime == 0 {
sleepTime = 300 + (time.Now().UnixNano() / 1000000) /* milliseconds */
}
payload := EventStreamPayload{
Action: "set",
Resource: "audioPlayback/config",
PublishResponse: true,
Properties: SleepTimerProperties{
Config: BaseSleepTimerProperties{
SleepTime: sleepTime,
SleepTimeRel: sleepTimeRel,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to enable sleep timer"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
}
func (c *Camera) DisableSleepTimer(sleepTimeRel int) (response *EventStreamResponse, err error) {
if sleepTimeRel == 0 {
sleepTimeRel = 300
}
payload := EventStreamPayload{
Action: "set",
Resource: "audioPlayback/config",
PublishResponse: true,
Properties: SleepTimerProperties{
Config: BaseSleepTimerProperties{
SleepTime: 0,
SleepTimeRel: sleepTimeRel,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to disable sleep timer"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
}
/*
The follow methods are all related to the nightlight features of Arlo Baby.
NOTE: The current state is in: cameras[0]["properties"][0]["nightLight"] returned from the basestation.GetAssociatedCamerasState() method.
*/
func (c *Camera) NightLight(on bool) (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: NightLightProperties{
NightLight: BaseNightLightProperties{
Enabled: on,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
var msg string
if on {
msg = "failed to turn night light on"
} else {
msg = "failed to turn night light off"
}
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
}
func (c *Camera) SetNightLightBrightness(level int) (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: NightLightProperties{
NightLight: BaseNightLightProperties{
Brightness: level,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to set night light brightness"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
}
// SetNightLightMode set the night light mode. Valid values are: "rainbow" or "rgb".
func (c *Camera) SetNightLightMode(mode string) (response *EventStreamResponse, err error) {
msg := "failed to set night light brightness"
if mode != "rainbow" && mode != "rgb" {
return nil, errors.WithMessage(errors.New("mode can only be \"rainbow\" or \"rgb\""), msg)
}
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: NightLightProperties{
NightLight: BaseNightLightProperties{
Mode: mode,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
}
// SetNightLightColor sets the night light color to the RGB value specified by the three parameters, which have valid values from 0-255.
func (c *Camera) SetNightLightColor(red, blue, green int) (response *EventStreamResponse, err error) {
// Sanity check; if the values are above or below the allowed limits, set them to their limit.
if red < 0 {
red = 0
} else if red > 255 {
red = 255
}
if blue < 0 {
blue = 0
} else if blue > 255 {
blue = 255
}
if green < 0 {
green = 0
} else if green > 255 {
green = 255
}
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: NightLightProperties{
NightLight: BaseNightLightProperties{
RGB: NightLightRGBProperties{
Red: red,
Blue: blue,
Green: green,
},
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to set night light color"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
}
func (c *Camera) EnableNightLightTimer(sleepTime int64 /* milliseconds */, sleepTimeRel int) (response *EventStreamResponse, err error) {
if sleepTime == 0 {
sleepTime = 300 + (time.Now().UnixNano() / 1000000) /* milliseconds */
}
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: NightLightProperties{
NightLight: BaseNightLightProperties{
SleepTime: sleepTime,
SleepTimeRel: sleepTimeRel,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to enable night light timer"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
}
func (c *Camera) DisableNightLightTimer(sleepTimeRel int) (response *EventStreamResponse, err error) {
if sleepTimeRel == 0 {
sleepTimeRel = 300
}
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: NightLightProperties{
NightLight: BaseNightLightProperties{
SleepTime: 0,
SleepTimeRel: sleepTimeRel,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to disable night light timer"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
}

View File

@ -17,14 +17,16 @@
package arlo package arlo
import ( import (
"context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
"github.com/pkg/errors" log "github.com/sirupsen/logrus"
) )
const eventStreamTimeout = 10 * time.Second const eventStreamTimeout = 30 * time.Second
const pingTime = 30 * time.Second const pingTime = 30 * time.Second
// A Basestation is a Device that's not type "camera" (basestation, arloq, arloqs, etc.). // A Basestation is a Device that's not type "camera" (basestation, arloq, arloqs, etc.).
@ -34,41 +36,108 @@ type Basestation struct {
eventStream *eventStream eventStream *eventStream
} }
type BaseStationState struct {
InterfaceVersion int `json:"interfaceVersion"`
APIVersion int `json:"apiVersion"`
State string `json:"state"`
SwVersion string `json:"swVersion"`
HwVersion string `json:"hwVersion"`
ModelID string `json:"modelId"`
Capabilities []string `json:"capabilities"`
McsEnabled bool `json:"mcsEnabled"`
AutoUpdateEnabled bool `json:"autoUpdateEnabled"`
UpdateAvailable interface{} `json:"updateAvailable"`
TimeZone string `json:"timeZone"`
OlsonTimeZone string `json:"olsonTimeZone"`
UploadBandwidthSaturated bool `json:"uploadBandwidthSaturated"`
AntiFlicker struct {
Mode int `json:"mode"`
AutoDefault int `json:"autoDefault"`
} `json:"antiFlicker"`
LowBatteryAlert struct {
Enabled bool `json:"enabled"`
} `json:"lowBatteryAlert"`
LowSignalAlert struct {
Enabled bool `json:"enabled"`
} `json:"lowSignalAlert"`
Claimed bool `json:"claimed"`
TimeSyncState string `json:"timeSyncState"`
Connectivity []struct {
Type string `json:"type"`
Connected bool `json:"connected"`
} `json:"connectivity"`
Groups []interface{} `json:"groups"`
LocalCert struct {
OwnCert string `json:"ownCert"`
PeerCerts []string `json:"peerCerts"`
} `json:"localCert"`
}
type GetModesResponse struct {
Active string `json:"active"`
Modes []*Mode `json:"modes"`
}
type Mode struct {
Name string `json:"name"`
Type string `json:"type,omitempty"`
RulesIds []string `json:"rules"`
ID string `json:"id"`
}
type GetRulesResponse struct {
Rules []Rule `json:"rules"`
}
type Rule struct {
Name string `json:"name"`
Protected bool `json:"protected"`
Triggers []struct {
Type string `json:"type"`
DeviceID string `json:"deviceId"`
Sensitivity int `json:"sensitivity"`
} `json:"triggers"`
Actions []struct {
Type string `json:"type"`
Recipients []string `json:"recipients,omitempty"`
DeviceID string `json:"deviceId,omitempty"`
StopCondition struct {
Type string `json:"type"`
DeviceID string `json:"deviceId"`
} `json:"stopCondition,omitempty"`
} `json:"actions"`
ID string `json:"id"`
}
// Basestations is a slice of Basestation objects. // Basestations is a slice of Basestation objects.
type Basestations []Basestation type Basestations []*Basestation
// Find returns a basestation with the device id passed in. // Find returns a basestation with the device id passed in.
func (bs *Basestations) Find(deviceId string) *Basestation { func (bs *Basestations) Find(deviceId string) *Basestation {
for _, b := range *bs { for _, b := range *bs {
if b.DeviceId == deviceId { if b.DeviceId == deviceId {
return &b return b
} }
} }
return nil return nil
} }
// makeEventStreamRequest is a helper function sets up a response channel, sends a message to the event stream, and blocks waiting for the response. // makeEventStreamRequest is a helper function sets up a response channel, sends a message to the event stream, and blocks waiting for the response.
func (b *Basestation) makeEventStreamRequest(payload EventStreamPayload, msg string) (response *EventStreamResponse, err error) { func (b *Basestation) makeEventStreamRequest(payload EventStreamPayload) (*EventStreamResponse, error) {
transId := genTransId() transId := genTransId()
payload.TransId = transId payload.TransId = transId
if err := b.IsConnected(); err != nil { if err := b.IsConnected(); err != nil {
//if err := b.Subscribe(); err != nil { return nil, fmt.Errorf("event stream not connected")
return nil, errors.WithMessage(errors.WithMessage(err, msg), "failed to reconnect to event stream")
//}
} }
subscriber := make(subscriber) responseChan := make(chan *EventStreamResponse)
errorChan := make(chan error)
// Add the response channel to the event stream queue so the response can be written to it. b.eventStream.subscribe(transId, responseChan, errorChan)
b.eventStream.subscribe(transId, subscriber)
// Make sure we close and remove the response channel before returning.
defer b.eventStream.unsubscribe(transId) defer b.eventStream.unsubscribe(transId)
// Send the payload to the event stream. // Send the payload to the event stream.
if err := b.NotifyEventStream(payload, msg); err != nil { if err := b.NotifyEventStream(payload); err != nil {
return nil, err return nil, fmt.Errorf("notifying event stream: %v", err)
} }
timer := time.NewTimer(eventStreamTimeout) timer := time.NewTimer(eventStreamTimeout)
@ -77,72 +146,89 @@ func (b *Basestation) makeEventStreamRequest(payload EventStreamPayload, msg str
// Wait for the response to come back from the event stream on the response channel. // Wait for the response to come back from the event stream on the response channel.
select { select {
// If we get a response, return it to the caller. // If we get a response, return it to the caller.
case response := <-subscriber: case response := <-responseChan:
return response, nil return response, nil
case err = <-b.eventStream.Error: case err := <-b.eventStream.Error:
return nil, errors.Wrap(err, msg) return nil, fmt.Errorf("event stream error: %v", err)
// If the event stream is closed, return an error about it. // If the event stream is closed, return an error about it.
case <-b.eventStream.Disconnected: case <-b.eventStream.DisconnectedChan:
err = errors.New("event stream was closed before response was read") return nil, fmt.Errorf("event stream was closed before response was read")
return nil, errors.WithMessage(err, msg)
// If we timeout, return an error about it. // If we timeout, return an error about it.
case <-timer.C: case <-timer.C:
err = fmt.Errorf("event stream response timed out after %.0f second", eventStreamTimeout.Seconds()) return nil, fmt.Errorf("event stream response timed out after %.0f second", eventStreamTimeout.Seconds())
return nil, errors.WithMessage(err, msg)
} }
} }
func (b *Basestation) IsConnected() error { func (b *Basestation) IsConnected() error {
// If the event stream is closed, return an error about it. // If the event stream is closed, return an error about it.
select { select {
case <-b.eventStream.Disconnected: case <-b.eventStream.DisconnectedChan:
return errors.New("basestation not connected to event stream") return fmt.Errorf("basestation not connected to event stream")
default: default:
return nil return nil
} }
} }
func (b *Basestation) Subscribe() error { func (b *Basestation) Subscribe(ctx context.Context) error {
b.eventStream = newEventStream(BaseUrl+fmt.Sprintf(NotifyResponsesPushServiceUri, b.arlo.Account.Token), &http.Client{Jar: b.arlo.client.HttpClient.Jar}) b.eventStream = newEventStream(BaseUrl+fmt.Sprintf(NotifyResponsesPushServiceUri, b.arlo.Account.Token), &http.Client{Jar: b.arlo.client.GetClient().Jar})
connectedChan, err := b.eventStream.listen(ctx)
if err != nil {
return fmt.Errorf("setting up event stream: %v", err)
}
forLoop: forLoop:
for { for {
// We blocking here because we can't really do anything with the event stream until we're connected.
// Once we have confirmation that we're connected to the event stream, we will "subscribe" to events.
select { select {
case connected := <-b.eventStream.listen(): case <-ctx.Done():
if connected { return fmt.Errorf("failed to subscribe to the event stream: requesting shutdown")
break forLoop case connected := <-connectedChan:
} else { if !connected {
return errors.New("failed to subscribe to the event stream") return fmt.Errorf("failed to subscribe to the event stream")
} }
case <-b.eventStream.Disconnected: break forLoop
err := errors.New("event stream was closed") case <-b.eventStream.DisconnectedChan:
return errors.WithMessage(err, "failed to subscribe to the event stream") return fmt.Errorf("failed to subscribe to the event stream: event stream was closed")
} }
} }
if err := b.Ping(); err != nil { if err := b.Ping(); err != nil {
return errors.WithMessage(err, "failed to subscribe to the event stream") _ = b.Disconnect()
return fmt.Errorf("Pingloop > error while pinging: %v > disconnect event stream", err)
} }
// The Arlo event stream requires a "ping" every 30s. // The Arlo event stream requires a "ping" every 30s.
go func() { go func(ctx context.Context) {
ticker := time.NewTicker(pingTime)
for { for {
time.Sleep(pingTime) select {
case <-ctx.Done():
return
case _ = <-ticker.C:
if err := b.Ping(); err != nil { if err := b.Ping(); err != nil {
b.Disconnect() log.Errorf("Pingloop > error while pinging: %v > disconnect event stream", err)
break _ = b.Disconnect()
return
} }
} }
}() }
}(ctx)
return nil return nil
} }
func (b *Basestation) Unsubscribe() error { func (b *Basestation) Unsubscribe() error {
resp, err := b.arlo.get(UnsubscribeUri, b.XCloudId, nil) var response Status
return checkRequest(resp, err, "failed to unsubscribe from event stream") _, err := b.arlo.client.R().
SetResult(&response).
SetHeader("xcloudId", b.XCloudId).
Put(UnsubscribeUri)
if err != nil {
return fmt.Errorf("unsubscribing from event stream: %v", err)
}
if response.Success == false {
return fmt.Errorf("unsubscribing from event stream: %s", response.Reason)
}
return nil
} }
func (b *Basestation) Disconnect() error { func (b *Basestation) Disconnect() error {
@ -164,57 +250,75 @@ func (b *Basestation) Ping() error {
To: b.DeviceId, To: b.DeviceId,
} }
if _, err := b.makeEventStreamRequest(payload, "failed to ping the event stream"); err != nil { if _, err := b.makeEventStreamRequest(payload); err != nil {
return err return err
} }
return nil return nil
} }
func (b *Basestation) NotifyEventStream(payload EventStreamPayload, msg string) error { func (b *Basestation) NotifyEventStream(payload EventStreamPayload) error {
resp, err := b.arlo.post(fmt.Sprintf(NotifyUri, b.DeviceId), b.XCloudId, payload, nil) var response Status
if err := checkRequest(resp, err, msg); err != nil { _, err := b.arlo.client.R().
return errors.WithMessage(err, "failed to notify event stream") SetBody(payload).
SetResult(&response).
SetHeader("xcloudId", b.XCloudId).
Post(fmt.Sprintf(NotifyUri, b.DeviceId))
if err != nil {
return fmt.Errorf("notifying event stream: %v", err)
}
if response.Success == false {
return fmt.Errorf("notifying event stream: %s", response.Reason)
} }
defer resp.Body.Close()
return nil return nil
} }
func (b *Basestation) GetState() (response *EventStreamResponse, err error) { func (b *Basestation) makeRequest(action string, resource string, publishResponse bool, properties interface{}, result interface{}) error {
payload := EventStreamPayload{ payload := EventStreamPayload{
Action: "get", Action: action,
Resource: "basestation", Resource: resource,
PublishResponse: false, PublishResponse: publishResponse,
Properties: properties,
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
To: b.DeviceId, To: b.DeviceId,
} }
resp, err := b.makeEventStreamRequest(payload)
return b.makeEventStreamRequest(payload, "failed to get basestation state") if err != nil {
return fmt.Errorf("making event stream request: %v", err)
}
if result != nil {
err = json.Unmarshal(resp.RawProperties, result)
if err != nil {
return fmt.Errorf("unmarshalling properties: %v", err)
}
}
return nil
} }
func (b *Basestation) GetAssociatedCamerasState() (response *EventStreamResponse, err error) { func (b *Basestation) GetState() (*BaseStationState, error) {
payload := EventStreamPayload{ var state BaseStationState
Action: "get", err := b.makeRequest("get", "basestation", false, nil, &state)
Resource: "cameras", if err != nil {
PublishResponse: false, return nil, fmt.Errorf("getting basestation %s state: %v", b.DeviceName, err)
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
To: b.DeviceId,
} }
return &state, nil
return b.makeEventStreamRequest(payload, "failed to get associated cameras state")
} }
func (b *Basestation) GetRules() (response *EventStreamResponse, err error) { func (b *Basestation) GetAllCameraState() ([]CameraState, error) {
payload := EventStreamPayload{ var states []CameraState
Action: "get", err := b.makeRequest("get", "cameras", false, nil, &states)
Resource: "rules", if err != nil {
PublishResponse: false, return nil, fmt.Errorf("getting associated cameras state: %v", err)
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
To: b.DeviceId,
} }
return states, nil
}
return b.makeEventStreamRequest(payload, "failed to get rules") func (b *Basestation) GetRules() ([]Rule, error) {
var resp GetRulesResponse
err := b.makeRequest("get", "rules", false, nil, &resp)
if err != nil {
return nil, fmt.Errorf("getting rules: %v", err)
}
return resp.Rules, nil
} }
func (b *Basestation) GetCalendarMode() (response *EventStreamResponse, err error) { func (b *Basestation) GetCalendarMode() (response *EventStreamResponse, err error) {
@ -226,52 +330,59 @@ func (b *Basestation) GetCalendarMode() (response *EventStreamResponse, err erro
To: b.DeviceId, To: b.DeviceId,
} }
return b.makeEventStreamRequest(payload, "failed to get schedule") return b.makeEventStreamRequest(payload)
} }
// SetCalendarMode toggles calendar mode. // SetCalendarMode toggles calendar mode.
// NOTE: The Arlo API seems to disable calendar mode when switching to other modes, if it's enabled. // NOTE: The Arlo API seems to disable calendar mode when switching to other modes, if it's enabled.
// You should probably do the same, although, the UI reflects the switch from calendar mode to say armed mode without explicitly setting calendar mode to inactive. // You should probably do the same, although, the UI reflects the switch from calendar mode to say armed mode without explicitly setting calendar mode to inactive.
func (b *Basestation) SetCalendarMode(active bool) (response *EventStreamResponse, err error) { func (b *Basestation) SetCalendarMode(active bool) error {
payload := EventStreamPayload{ resp := make(map[string]bool)
Action: "set", err := b.makeRequest("set", "schedule", true, struct {
Resource: "schedule", Active bool `json:"active"`
PublishResponse: true, }{
Properties: BasestationScheduleProperties{
Active: active, Active: active,
}, }, &resp)
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), if err != nil {
To: b.DeviceId, return fmt.Errorf("setting calendar mode %t: %v", active, err)
} }
activemode, ok := resp["active"]
return b.makeEventStreamRequest(payload, "failed to set schedule") if !ok {
return fmt.Errorf("active mode not present in response")
}
if activemode != active {
return fmt.Errorf("active mode is not the mode requested: requested %t, set %t", active, activemode)
}
return nil
} }
func (b *Basestation) GetModes() (response *EventStreamResponse, err error) { func (b *Basestation) GetModes() (*GetModesResponse, error) {
payload := EventStreamPayload{ var resp GetModesResponse
Action: "get", err := b.makeRequest("get", "modes", false, nil, &resp)
Resource: "modes", if err != nil {
PublishResponse: false, return nil, fmt.Errorf("getting modes: %v", err)
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
To: b.DeviceId,
} }
return &resp, nil
return b.makeEventStreamRequest(payload, "failed to get modes")
} }
func (b *Basestation) SetCustomMode(mode string) (response *EventStreamResponse, err error) { func (b *Basestation) SetCustomMode(mode string) error {
payload := EventStreamPayload{ resp := make(map[string]string)
Action: "set", err := b.makeRequest("set", "modes", true, struct {
Resource: "modes", Active string `json:"active"`
PublishResponse: true, }{
Properties: BasestationModeProperties{
Active: mode, Active: mode,
}, }, &resp)
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), if err != nil {
To: b.DeviceId, return fmt.Errorf("setting custom mode %s: %v", mode, err)
} }
activemode, ok := resp["active"]
return b.makeEventStreamRequest(payload, "failed to set mode") if !ok {
return fmt.Errorf("active mode not present in response")
}
if activemode != mode {
return fmt.Errorf("active mode is not the mode requested: requested %s, set %s", mode, activemode)
}
return nil
} }
func (b *Basestation) DeleteMode(mode string) (response *EventStreamResponse, err error) { func (b *Basestation) DeleteMode(mode string) (response *EventStreamResponse, err error) {
@ -283,15 +394,23 @@ func (b *Basestation) DeleteMode(mode string) (response *EventStreamResponse, er
To: b.DeviceId, To: b.DeviceId,
} }
return b.makeEventStreamRequest(payload, "failed to set mode") return b.makeEventStreamRequest(payload)
} }
func (b *Basestation) Arm() (response *EventStreamResponse, err error) { func (b *Basestation) Arm() error {
return b.SetCustomMode("mode1") err := b.SetCustomMode("mode1")
if err != nil {
return fmt.Errorf("arming (mode1): %v", err)
}
return nil
} }
func (b *Basestation) Disarm() (response *EventStreamResponse, err error) { func (b *Basestation) Disarm() error {
return b.SetCustomMode("mode0") err := b.SetCustomMode("mode0")
if err != nil {
return fmt.Errorf("disarming (mode0): %v", err)
}
return nil
} }
func (b *Basestation) SirenOn() (response *EventStreamResponse, err error) { func (b *Basestation) SirenOn() (response *EventStreamResponse, err error) {
@ -309,7 +428,7 @@ func (b *Basestation) SirenOn() (response *EventStreamResponse, err error) {
To: b.DeviceId, To: b.DeviceId,
} }
return b.makeEventStreamRequest(payload, "failed to get modes") return b.makeEventStreamRequest(payload)
} }
func (b *Basestation) SirenOff() (response *EventStreamResponse, err error) { func (b *Basestation) SirenOff() (response *EventStreamResponse, err error) {
@ -327,5 +446,5 @@ func (b *Basestation) SirenOff() (response *EventStreamResponse, err error) {
To: b.DeviceId, To: b.DeviceId,
} }
return b.makeEventStreamRequest(payload, "failed to get modes") return b.makeEventStreamRequest(payload)
} }

478
camera.go
View File

@ -18,24 +18,88 @@ package arlo
import ( import (
"fmt" "fmt"
"strings"
"time"
"github.com/pkg/errors"
) )
// A Camera is a Device of type "camera". // A Camera is a Device of type "camera".
// This type is here just for semantics. Some methods explicitly require a device of a certain type. // This type is here just for semantics. Some methods explicitly require a device of a certain type.
type Camera Device type Camera Device
type CameraState struct {
InterfaceVersion int `json:"interfaceVersion"`
SerialNumber string `json:"serialNumber"`
BatteryLevel int `json:"batteryLevel"`
BatteryTech string `json:"batteryTech"`
ChargerTech string `json:"chargerTech"`
ChargingState string `json:"chargingState"`
ChargeOnly bool `json:"chargeOnly"`
ChargeNotificationLedEnable bool `json:"chargeNotificationLedEnable"`
AudioMicAGC int `json:"audioMicAGC"`
SignalStrength int `json:"signalStrength"`
Brightness int `json:"brightness"`
Mirror bool `json:"mirror"`
Flip bool `json:"flip"`
PowerSaveMode int `json:"powerSaveMode"`
Zoom struct {
Topleftx int `json:"topleftx"`
Toplefty int `json:"toplefty"`
Bottomrightx int `json:"bottomrightx"`
Bottomrighty int `json:"bottomrighty"`
} `json:"zoom"`
Mic struct {
Mute bool `json:"mute"`
Volume int `json:"volume"`
} `json:"mic"`
Speaker struct {
Mute bool `json:"mute"`
Volume int `json:"volume"`
} `json:"speaker"`
StreamingMode string `json:"streamingMode"`
ContinuousStreamState string `json:"continuousStreamState"`
Motion struct {
Sensitivity int `json:"sensitivity"`
Zones []interface{} `json:"zones"`
} `json:"motion"`
Resolution struct {
Width int `json:"width"`
Height int `json:"height"`
} `json:"resolution"`
IdleLedEnable bool `json:"idleLedEnable"`
PrivacyActive bool `json:"privacyActive"`
StandbyActive bool `json:"standbyActive"`
SetupActive bool `json:"setupActive"`
ConnectionState string `json:"connectionState"`
ActivityState string `json:"activityState"`
SwVersion string `json:"swVersion"`
HwVersion string `json:"hwVersion"`
ModelID string `json:"modelId"`
MotionSetupModeEnabled bool `json:"motionSetupModeEnabled"`
MotionSetupModeSensitivity int `json:"motionSetupModeSensitivity"`
MotionDetected bool `json:"motionDetected"`
AudioDetected bool `json:"audioDetected"`
HasStreamed bool `json:"hasStreamed"`
LocalRecordingActive bool `json:"localRecordingActive"`
OlsonTimeZone string `json:"olsonTimeZone"`
Name string `json:"name"`
NightVisionMode int `json:"nightVisionMode"`
VideoMode string `json:"videoMode"`
Hdr string `json:"hdr"`
UpdateAvailable interface{} `json:"updateAvailable"`
BlockNotifications struct {
Block bool `json:"block"`
Duration int `json:"duration"`
EndTime int `json:"endTime"`
} `json:"blockNotifications"`
BestLocalLiveStreaming string `json:"bestLocalLiveStreaming"`
}
// Cameras is a slice of Camera objects. // Cameras is a slice of Camera objects.
type Cameras []Camera type Cameras []*Camera
// Find returns a camera with the device id passed in. // Find returns a camera with the device id passed in.
func (cs *Cameras) Find(deviceId string) *Camera { func (cs *Cameras) Find(deviceId string) *Camera {
for _, c := range *cs { for _, c := range *cs {
if c.DeviceId == deviceId { if c.DeviceId == deviceId {
return &c return c
} }
} }
@ -55,14 +119,11 @@ func (c *Camera) On() (response *EventStreamResponse, err error) {
To: c.ParentId, To: c.ParentId,
} }
msg := "failed to turn camera on"
b := c.arlo.Basestations.Find(c.ParentId) b := c.arlo.Basestations.Find(c.ParentId)
if b == nil { if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
} }
return b.makeEventStreamRequest(payload, msg) return b.makeEventStreamRequest(payload)
} }
// On turns a camera off; meaning it won't detect and record events. // On turns a camera off; meaning it won't detect and record events.
@ -78,14 +139,11 @@ func (c *Camera) Off() (response *EventStreamResponse, err error) {
To: c.ParentId, To: c.ParentId,
} }
msg := "failed to turn camera off"
b := c.arlo.Basestations.Find(c.ParentId) b := c.arlo.Basestations.Find(c.ParentId)
if b == nil { if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
} }
return b.makeEventStreamRequest(payload, msg) return b.makeEventStreamRequest(payload)
} }
// SetBrightness sets the camera brightness. // SetBrightness sets the camera brightness.
@ -109,15 +167,11 @@ func (c *Camera) SetBrightness(brightness int) (response *EventStreamResponse, e
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId, To: c.ParentId,
} }
msg := "failed to set camera brightness"
b := c.arlo.Basestations.Find(c.ParentId) b := c.arlo.Basestations.Find(c.ParentId)
if b == nil { if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
} }
return b.makeEventStreamRequest(payload, msg) return b.makeEventStreamRequest(payload)
} }
func (c *Camera) EnableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) { func (c *Camera) EnableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) {
@ -135,15 +189,11 @@ func (c *Camera) EnableMotionAlerts(sensitivity int, zones []string) (response *
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId, To: c.ParentId,
} }
msg := "failed to enable motion alerts"
b := c.arlo.Basestations.Find(c.ParentId) b := c.arlo.Basestations.Find(c.ParentId)
if b == nil { if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
} }
return b.makeEventStreamRequest(payload, msg) return b.makeEventStreamRequest(payload)
} }
func (c *Camera) DisableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) { func (c *Camera) DisableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) {
@ -162,14 +212,11 @@ func (c *Camera) DisableMotionAlerts(sensitivity int, zones []string) (response
To: c.ParentId, To: c.ParentId,
} }
msg := "failed to enable motion alerts"
b := c.arlo.Basestations.Find(c.ParentId) b := c.arlo.Basestations.Find(c.ParentId)
if b == nil { if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
} }
return b.makeEventStreamRequest(payload, msg) return b.makeEventStreamRequest(payload)
} }
func (c *Camera) EnableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) { func (c *Camera) EnableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) {
@ -187,14 +234,11 @@ func (c *Camera) EnableAudioAlerts(sensitivity int) (response *EventStreamRespon
To: c.ParentId, To: c.ParentId,
} }
msg := "failed to enable audio alerts"
b := c.arlo.Basestations.Find(c.ParentId) b := c.arlo.Basestations.Find(c.ParentId)
if b == nil { if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
} }
return b.makeEventStreamRequest(payload, msg) return b.makeEventStreamRequest(payload)
} }
func (c *Camera) DisableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) { func (c *Camera) DisableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) {
@ -212,192 +256,11 @@ func (c *Camera) DisableAudioAlerts(sensitivity int) (response *EventStreamRespo
To: c.ParentId, To: c.ParentId,
} }
msg := "failed to disable audio alerts"
b := c.arlo.Basestations.Find(c.ParentId) b := c.arlo.Basestations.Find(c.ParentId)
if b == nil { if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
} }
return b.makeEventStreamRequest(payload, msg) return b.makeEventStreamRequest(payload)
}
// PushToTalk starts a push-to-talk session.
// FIXME: This feature requires more API calls to make it actually work, and I haven't figure out how to fully implement it.
// It appears that the audio stream is Real-Time Transport Protocol (RTP), which requires a player (ffmpeg?) to consume the stream.
func (c *Camera) PushToTalk() error {
/*
processResponse: function(e) {
if (g.pc)
if (e.properties && "answerSdp" == e.properties.type) {
var t = e.properties.data
, i = {
type: "answer",
sdp: t
};
r.debug(i),
g.pc.setRemoteDescription(new g.SessionDescription(i), u, d)
} else if (e.properties && "answerCandidate" == e.properties.type)
if (g.candidateCache)
g.candidateCache.push(e.properties.data);
else {
var n = e.properties.data
, a = window.mozRTCIceCandidate || window.RTCIceCandidate
, o = new a({
candidate: n,
sdpMLineIndex: 0
});
r.debug(o),
g.pc.addIceCandidate(o)
}
},
startConnection: function(t) {
g.loading = !0,
g.error = !1,
g.candidateCache = [];
var i = t.deviceId
, o = t.parentId
, u = t.uniqueId;
g.device = t;
var p = {
method: "GET",
url: l.getPttUrl(u),
data: "",
headers: {
Authorization: s.ssoToken,
"Content-Type": "application/json; charset=utf-8",
"Data-Type": "json"
}
};
r.debug("getting ptt data: " + JSON.stringify(p));
n(p).then(function(u) {
if (!u.data.success)
return e.$broadcast("show_error", u.data),
void (g.error = u.data.data.message || !0);
var m = u.data.data.data;
g.uSessionId = u.data.data.uSessionId,
_.each(m, function(e) {
e.url && (e.urls = e.url,
delete e.url)
});
var f = new g.PeerConnection({
iceServers: m,
iceCandidatePoolSize: 0
});
f.onicecandidate = function(e) {
if (null != e.candidate) {
r.debug(e.candidate);
var a = {
action: "pushToTalk",
from: t.userId,
publishResponse: !1,
resource: "cameras/" + i,
responseUrl: "",
to: o,
transId: "web!98b0c88b!1429756137177",
properties: {
uSessionId: g.uSessionId,
type: "offerCandidate",
data: e.candidate.candidate
}
};
p = {
method: "POST",
url: l.getPttNotifyUrl(o),
data: a,
headers: {
xcloudId: t.xCloudId,
Authorization: s.ssoToken
}
},
n(p)
} else
r.debug("Failed to get any more candidate")
}
,
f.oniceconnectionstatechange = function(e) {
r.debug("ICE Connection State Change:" + f.iceConnectionState),
"connected" == f.iceConnectionState || "completed" == f.iceConnectionState ? g.loading = !1 : "disconnected" != f.iceConnectionState && "failed" != f.iceConnectionState || (g.stopConnection(),
g.error = a("i18n")("camera_label_ptt_failed_to_connect"))
}
,
g.pc = f,
(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia).call(navigator, {
audio: !0,
video: !1
}, function(e) {
g.stream = e,
g.stream.getAudioTracks()[0].enabled = !1,
f.addStream(e),
f.createOffer(function(e) {
f.setLocalDescription(e, c, d),
r.debug(e.sdp);
var a = {
action: "pushToTalk",
from: t.userId,
publishResponse: !0,
resource: "cameras/" + i,
responseUrl: "",
to: o,
transId: "web!98b0c88b!1429756137177",
properties: {
uSessionId: g.uSessionId,
type: "offerSdp",
data: e.sdp
}
};
p = {
method: "POST",
url: l.getPttNotifyUrl(o),
data: a,
headers: {
xcloudId: t.xCloudId,
Authorization: s.ssoToken
}
},
n(p)
}, d)
}, d)
})
},
stopConnection: function() {
if (g.pc) {
var e = {
action: "pushToTalk",
from: g.device.userId,
publishResponse: !1,
resource: "cameras/" + g.device.deviceId,
responseUrl: "",
to: g.device.deviceId,
transId: "web!98b0c88b!1429756137177",
properties: {
uSessionId: g.uSessionId,
type: "endSession"
}
}
, t = {
method: "POST",
url: l.getPttNotifyUrl(g.device.deviceId),
data: e,
headers: {
xcloudId: g.device.xCloudId,
Authorization: s.ssoToken
}
};
n(t);
try {
g.stream.getAudioTracks()[0].stop(),
g.stream = null
} catch (e) {}
g.pc.close(),
g.pc = null,
g.loading = !0
}
}
};
*/
resp, err := c.arlo.get(fmt.Sprintf(PttUri, c.UniqueId), c.XCloudId, nil)
return checkRequest(resp, err, "failed to enable push to talk")
} }
// action: disabled OR recordSnapshot OR recordVideo // action: disabled OR recordSnapshot OR recordVideo
@ -422,178 +285,9 @@ func (c *Camera) SetAlertNotificationMethods(action string, email, push bool) (r
To: c.ParentId, To: c.ParentId,
} }
msg := "failed to set alert notification methods"
b := c.arlo.Basestations.Find(c.ParentId) b := c.arlo.Basestations.Find(c.ParentId)
if b == nil { if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
} }
return b.makeEventStreamRequest(payload, msg) return b.makeEventStreamRequest(payload)
}
// StartStream returns a json object containing the rtmps url to the requested video stream.
// You will need something like ffmpeg to read the rtmps stream.
// If you call StartStream(), you have to start reading data from the stream, or streaming will be cancelled
// and taking a snapshot may fail (since it requires the stream to be active).
func (c *Camera) StartStream() (url string, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: map[string]string{
"activityState": "startUserStream",
"cameraId": c.DeviceId,
},
TransId: genTransId(),
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to start stream"
resp, err := c.arlo.post(StartStreamUri, c.XCloudId, payload, nil)
if err != nil {
return "", errors.WithMessage(err, msg)
}
defer resp.Body.Close()
response := new(StreamResponse)
if err := resp.Decode(response); err != nil {
return "", err
}
if !response.Success {
return "", errors.WithMessage(errors.New("status was false"), msg)
}
response.Data.URL = strings.Replace(response.Data.URL, "rtsp://", "rtsps://", 1)
return response.Data.URL, nil
}
// TakeSnapshot causes the camera to snapshot while recording.
// NOTE: You MUST call StartStream() before calling this function.
// If you call StartStream(), you have to start reading data from the stream, or streaming will be cancelled
// and taking a snapshot may fail (since it requires the stream to be active).
// NOTE: You should not use this function is you just want a snapshot and aren't intending to stream.
// Use TriggerFullFrameSnapshot() instead.
//
// NOTE: Use DownloadSnapshot() to download the actual image file.
// TODO: Need to refactor the even stream code to allow handling of events whose transIds don't correlate. :/
func (c *Camera) TakeSnapshot() (response *EventStreamResponse, err error) {
return nil, errors.New("TakeSnapshot not implemented")
/*
msg := "failed to take snapshot"
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
resp, err := c.arlo.post(TakeSnapshotUri, c.XCloudId, body, nil)
if err := checkRequest(resp, err, msg); err != nil {
return nil, errors.WithMessage(err, msg)
}
*/
// TODO: Need to write the code to handle the event stream message.
/*
def callback(self, event):
if event.get("deviceId") == camera.get("deviceId") and event.get("resource") == "mediaUploadNotification":
presigned_content_url = event.get("presignedContentUrl")
if presigned_content_url is not None:
r return presigned_content_url
*/
}
// TriggerFullFrameSnapshot causes the camera to record a full-frame snapshot.
// The presignedFullFrameSnapshotUrl url is returned.
// Use DownloadSnapshot() to download the actual image file.
// TODO: Need to refactor the even stream code to allow handling of events whose transIds don't correlate. :/
func (c *Camera) TriggerFullFrameSnapshot() (response *EventStreamResponse, err error) {
return nil, errors.New("TriggerFullFrameSnapshot not implemented")
/*
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: map[string]string{
"activityState": "fullFrameSnapshot",
},
TransId: genTransId(),
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to trigger full-frame snapshot"
b := c.arlo.Basestations.Find(c.ParentId)
if b == nil {
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
return nil, errors.WithMessage(err, msg)
}
return b.makeEventStreamRequest(payload, msg)
*/
/*
def callback(self, event):
if event.get("from") == basestation.get("deviceId") and event.get("resource") == "cameras/"+camera.get("deviceId") and event.get("action") == "fullFrameSnapshotAvailable":
return event.get("properties", {}).get("presignedFullFrameSnapshotUrl")
return None
*/
}
// StartRecording causes the camera to start recording and returns a url that you must start reading from using ffmpeg
// or something similar.
func (c *Camera) StartRecording() (url string, err error) {
msg := "failed to start recording"
url, err = c.StartStream()
if err != nil {
return "", errors.WithMessage(err, msg)
}
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
resp, err := c.arlo.post(StartRecordUri, c.XCloudId, body, nil)
if err := checkRequest(resp, err, msg); err != nil {
return "", errors.WithMessage(err, msg)
}
return url, nil
}
// StopRecording causes the camera to stop recording.
func (c *Camera) StopRecording() error {
msg := "failed to stop recording"
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
resp, err := c.arlo.post(StopRecordUri, c.XCloudId, body, nil)
if err := checkRequest(resp, err, msg); err != nil {
return errors.WithMessage(err, msg)
}
return nil
}
// This function downloads a Cvr Playlist file for the period fromDate to toDate.
func (c *Camera) GetCvrPlaylist(fromDate, toDate time.Time) (playlist *CvrPlaylist, err error) {
msg := "failed to get cvr playlist"
resp, err := c.arlo.get(fmt.Sprintf(PlaylistUri, c.UniqueId, fromDate.Format("20060102"), toDate.Format("20060102")), c.XCloudId, nil)
if err != nil {
return nil, errors.WithMessage(err, msg)
}
defer resp.Body.Close()
response := new(CvrPlaylistResponse)
if err := resp.Decode(&response); err != nil {
return nil, err
}
if !response.Success {
return nil, errors.New(msg)
}
return &response.Data, nil
} }

30
cmd/main.go Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"context"
"git.lehouerou.net/laurent/arlo-go"
log "github.com/sirupsen/logrus"
)
func main() {
a := arlo.NewArlo()
err := a.Login(context.Background(), "hass@lehouerou.net", "TiPXMVLUeZfUg6RrmwzK")
if err != nil {
log.Errorf("login: %v", err)
return
}
for _, device := range a.Cameras {
log.Infof("%s", device.DeviceName)
}
for _, b := range a.Basestations {
err := b.SetCustomMode("mode3")
if err != nil {
log.Error(err)
}
log.Info("ok")
}
select {}
}

View File

@ -46,7 +46,7 @@ type Device struct {
PresignedFullFrameSnapshotUrl string `json:"presignedFullFrameSnapshotUrl"` PresignedFullFrameSnapshotUrl string `json:"presignedFullFrameSnapshotUrl"`
PresignedLastImageUrl string `json:"presignedLastImageUrl"` PresignedLastImageUrl string `json:"presignedLastImageUrl"`
PresignedSnapshotUrl string `json:"presignedSnapshotUrl"` PresignedSnapshotUrl string `json:"presignedSnapshotUrl"`
MediaObjectCount uint8 `json:"mediaObjectCount"` MediaObjectCount uint32 `json:"mediaObjectCount"`
ModelId string `json:"modelId"` ModelId string `json:"modelId"`
Owner Owner `json:"owner"` Owner Owner `json:"owner"`
ParentId string `json:"parentId"` ParentId string `json:"parentId"`
@ -59,7 +59,7 @@ type Device struct {
} }
// Devices is a slice of Device objects. // Devices is a slice of Device objects.
type Devices []Device type Devices []*Device
// A DeviceOrder holds a map of device ids and a numeric index. The numeric index is the device order. // A DeviceOrder holds a map of device ids and a numeric index. The numeric index is the device order.
// Device order is mainly used by the UI to determine which order to show the devices. // Device order is mainly used by the UI to determine which order to show the devices.
@ -76,10 +76,10 @@ type DeviceOrder struct {
} }
// Find returns a device with the device id passed in. // Find returns a device with the device id passed in.
func (ds *Devices) Find(deviceId string) *Device { func (ds Devices) Find(deviceId string) *Device {
for _, d := range *ds { for _, d := range ds {
if d.DeviceId == deviceId { if d.DeviceId == deviceId {
return &d return d
} }
} }
@ -87,22 +87,22 @@ func (ds *Devices) Find(deviceId string) *Device {
} }
func (ds Devices) FindCameras(basestationId string) Cameras { func (ds Devices) FindCameras(basestationId string) Cameras {
cs := new(Cameras) cs := Cameras{}
for _, d := range ds { for _, d := range ds {
if d.ParentId == basestationId { if d.ParentId == basestationId {
*cs = append(*cs, Camera(d)) cam := Camera(*d)
cs = append(cs, &cam)
} }
} }
return cs
return *cs
} }
func (d Device) IsBasestation() bool { func (d Device) IsBasestation() bool {
return d.DeviceType == DeviceTypeBasestation || d.DeviceId == d.ParentId return d.DeviceType == DeviceTypeBasestation
} }
func (d Device) IsCamera() bool { func (d Device) IsCamera() bool {
switch(d.DeviceType) { switch d.DeviceType {
case case
DeviceTypeCamera, DeviceTypeCamera,
DeviceTypeArloQ: DeviceTypeArloQ:
@ -127,11 +127,11 @@ func (d Device) IsSiren() bool {
// I did this because some device types, like arloq, don't have a basestation. // I did this because some device types, like arloq, don't have a basestation.
// So, when interacting with them you must treat them like a basestation and a camera. // So, when interacting with them you must treat them like a basestation and a camera.
// Cameras also includes devices of this type, so you can get the same data there or cast. // Cameras also includes devices of this type, so you can get the same data there or cast.
func (ds Devices) GetBasestations() *Basestations { func (ds Devices) GetBasestations() Basestations {
basestations := new(Basestations) var basestations Basestations
for _, d := range ds { for _, d := range ds {
if d.IsBasestation() || !d.IsCamera() { if d.IsBasestation() {
*basestations = append(*basestations, Basestation{Device: d}) basestations = append(basestations, &Basestation{Device: *d})
} }
} }
return basestations return basestations
@ -141,19 +141,20 @@ func (ds Devices) GetBasestations() *Basestations {
// I did this because some device types, like arloq, don't have a basestation. // I did this because some device types, like arloq, don't have a basestation.
// So, when interacting with them you must treat them like a basestation and a camera. // So, when interacting with them you must treat them like a basestation and a camera.
// Basestations also includes devices of this type, so you can get the same data there or cast. // Basestations also includes devices of this type, so you can get the same data there or cast.
func (ds Devices) GetCameras() *Cameras { func (ds Devices) GetCameras() Cameras {
cameras := new(Cameras) var cameras Cameras
for _, d := range ds { for _, d := range ds {
if d.IsCamera() || !d.IsBasestation() { if d.IsCamera() {
*cameras = append(*cameras, Camera(d)) cam := Camera(*d)
cameras = append(cameras, &cam)
} }
} }
return cameras return cameras
} }
// UpdateDeviceName sets the name of the given device to the name argument. // UpdateDeviceName sets the name of the given device to the name argument.
func (d *Device) UpdateDeviceName(name string) error { //func (d *Device) UpdateDeviceName(name string) error {
body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId} // body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId}
resp, err := d.arlo.put(RenameDeviceUri, d.XCloudId, body, nil) // resp, err := d.arlo.put(RenameDeviceUri, d.XCloudId, body, nil)
return checkRequest(resp, err, "failed to update device name") // return checkRequest(resp, err, "failed to update device name")
} //}

View File

@ -18,132 +18,148 @@ package arlo
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"sync" "sync"
"github.com/pkg/errors"
"github.com/r3labs/sse" "github.com/r3labs/sse"
log "github.com/sirupsen/logrus"
) )
var (
FAILED_TO_PUBLISH = errors.New("failed to publish")
FAILED_TO_DECODE_JSON = errors.New("failed to decode json")
FAILED_TO_SUBSCRIBE = errors.New("failed to subscribe to seeclient")
)
type subscriber chan *EventStreamResponse
type subscribers map[string]subscriber
type subscriptions struct {
subscribers
rwmutex sync.RWMutex
}
type eventStream struct { type eventStream struct {
SSEClient *sse.Client SSEClient *sse.Client
Events chan *sse.Event Events chan *sse.Event
Error chan error Error chan error
Verbose bool Verbose bool
Disconnected chan interface{} DisconnectedChan chan interface{}
once *sync.Once once *sync.Once
subscriptions subscribers map[string]chan *EventStreamResponse
subscribersMutex sync.RWMutex
errorsubsribers map[string]chan error
errorMutex sync.RWMutex
} }
func newEventStream(url string, client *http.Client) *eventStream { func newEventStream(url string, client *http.Client) *eventStream {
e := &eventStream{
Events: make(chan *sse.Event),
subscribers: make(map[string]chan *EventStreamResponse),
subscribersMutex: sync.RWMutex{},
errorsubsribers: make(map[string]chan error),
errorMutex: sync.RWMutex{},
DisconnectedChan: make(chan interface{}),
once: new(sync.Once),
}
SSEClient := sse.NewClient(url) SSEClient := sse.NewClient(url)
SSEClient.Connection = client SSEClient.Connection = client
SSEClient.OnDisconnect(func(c *sse.Client) {
return &eventStream{ e.disconnect()
SSEClient: SSEClient, })
Events: make(chan *sse.Event), e.SSEClient = SSEClient
subscriptions: subscriptions{make(map[string]subscriber), sync.RWMutex{}}, return e
Error: make(chan error),
Disconnected: make(chan interface{}),
once: new(sync.Once),
}
} }
func (e *eventStream) disconnect() { func (e *eventStream) disconnect() {
e.once.Do(func() { e.once.Do(func() {
close(e.Disconnected) close(e.DisconnectedChan)
}) })
} }
func (e *eventStream) listen() (connected chan bool) { func (e *eventStream) listen(ctx context.Context) (chan bool, error) {
connected = make(chan bool) connectedChan := make(chan bool)
go func() {
e.SSEClient.OnDisconnect(func(c *sse.Client) {
e.disconnect()
// fmt.Printf("\n\n\n\nClIENT DISCONNECTED!!!!!\n\n\n\n")
})
err := e.SSEClient.SubscribeChanRaw(e.Events) err := e.SSEClient.SubscribeChanRaw(e.Events)
if err != nil { if err != nil {
e.Error <- FAILED_TO_SUBSCRIBE return nil, fmt.Errorf("failed to subscribe to seeclient")
} }
go func() {
for { for {
select { select {
case event := <-e.Events: case <-ctx.Done():
//fmt.Println("Got event message.") e.disconnect()
/* return
fmt.Print(".") case event, ok := <-e.Events:
fmt.Printf("EVENT: %s\n", event.Event) if !ok {
fmt.Printf("DATA: %s\n", event.Data) return
*/ }
if event != nil && event.Data != nil { if event == nil || event.Data == nil {
notifyResponse := &EventStreamResponse{} log.Warn("EventStream > nil event or nil data in event")
b := bytes.NewBuffer(event.Data) continue
err := json.NewDecoder(b).Decode(notifyResponse) }
fmt.Printf("DATA : %s\n", event.Data)
var notifyResponse EventStreamResponse
err := json.NewDecoder(bytes.NewBuffer(event.Data)).Decode(&notifyResponse)
if err != nil { if err != nil {
e.Error <- FAILED_TO_DECODE_JSON log.Warnf("EventStream > failed to decode event: %s", event.Data)
break continue
}
bytesProperties, err := json.Marshal(notifyResponse.EventStreamPayload.Properties)
if err != nil {
log.Warnf("EventStream > failed to marshal raw properties: %s", event.Data)
continue
}
notifyResponse.RawProperties = bytesProperties
if notifyResponse.Status == "connected" {
connectedChan <- true
continue
}
if notifyResponse.Status == "disconnected" {
e.disconnect()
continue
}
if notifyResponse.Action == "logout" {
log.Warn("EventStream > logged out")
e.disconnect()
continue
}
e.subscribersMutex.RLock()
subscriber, ok := e.subscribers[notifyResponse.TransId]
e.subscribersMutex.RUnlock()
if ok {
subscriber <- &notifyResponse
} }
// FIXME: This is a shitty way to handle this. It's potentially leaking a chan. case <-e.DisconnectedChan:
if notifyResponse.Status == "connected" { connectedChan <- false
connected <- true
} else if notifyResponse.Status == "disconnected" {
e.disconnect()
} else if notifyResponse.Action == "logout" {
e.disconnect()
} else {
e.subscriptions.rwmutex.RLock()
subscriber, ok := e.subscribers[notifyResponse.TransId]
e.subscriptions.rwmutex.RUnlock()
if ok {
subscriber <- notifyResponse
}
}
}
case <-e.Disconnected:
connected <- false
return return
} }
} }
}() }()
return connected return connectedChan, nil
} }
func (s *subscriptions) unsubscribe(transId string) { func (e *eventStream) unsubscribe(transId string) {
s.rwmutex.Lock() e.subscribersMutex.Lock()
defer s.rwmutex.Unlock() if c, ok := e.subscribers[transId]; ok {
if _, ok := s.subscribers[transId]; ok { close(c)
close(s.subscribers[transId]) delete(e.subscribers, transId)
delete(s.subscribers, transId)
} }
e.subscribersMutex.Unlock()
e.errorMutex.Lock()
if c, ok := e.errorsubsribers[transId]; ok {
close(c)
delete(e.errorsubsribers, transId)
}
e.errorMutex.Unlock()
} }
func (s *subscriptions) subscribe(transId string, subscriber subscriber) { func (e *eventStream) subscribe(transId string, subscriber chan *EventStreamResponse, errorChan chan error) {
s.rwmutex.Lock() e.subscribersMutex.Lock()
s.subscribers[transId] = subscriber e.subscribers[transId] = subscriber
s.rwmutex.Unlock() e.subscribersMutex.Unlock()
e.errorMutex.Lock()
e.errorsubsribers[transId] = errorChan
e.errorMutex.Unlock()
} }

8
go.mod
View File

@ -1,9 +1,9 @@
module github.com/jeffreydwalter/arlo-go module git.lehouerou.net/laurent/arlo-go
go 1.13 go 1.14
require ( require (
github.com/pkg/errors v0.8.1 github.com/go-resty/resty/v2 v2.3.0
github.com/r3labs/sse v0.0.0-20191120111931-24eacf438413 github.com/r3labs/sse v0.0.0-20191120111931-24eacf438413
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 github.com/sirupsen/logrus v1.6.0
) )

23
go.sum
View File

@ -1,18 +1,41 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-resty/resty v1.12.0 h1:L1P5qymrXL5H/doXe2pKUr1wxovAI5ilm2LdVLbwThc=
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
github.com/jeffreydwalter/arlo-go v0.0.0-20200420231349-d68ae1fb4cb7 h1:vFCcYqhV2UgUB2ROxwdW1fVoOVE33jr4snGsRKKIBQ8=
github.com/jeffreydwalter/arlo-go v0.0.0-20200420231349-d68ae1fb4cb7/go.mod h1:eNo//rDcum+KKc3Itlb1YzEzX+7dBuRzdD93eS5pd2Q=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/r3labs/sse v0.0.0-20191120111931-24eacf438413 h1:bF3heZD0lrJF16uVKKJsorZjBS7ET5Y9QKYtgno7X4Q= github.com/r3labs/sse v0.0.0-20191120111931-24eacf438413 h1:bF3heZD0lrJF16uVKKJsorZjBS7ET5Y9QKYtgno7X4Q=
github.com/r3labs/sse v0.0.0-20191120111931-24eacf438413/go.mod h1:S8xSOnV3CgpNrWd0GQ/OoQfMtlg2uPRSuTzcSGrzwK8= github.com/r3labs/sse v0.0.0-20191120111931-24eacf438413/go.mod h1:S8xSOnV3CgpNrWd0GQ/OoQfMtlg2uPRSuTzcSGrzwK8=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1,162 +0,0 @@
/*
* Copyright (c) 2018 Jeffrey Walter <jeffreydwalter@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package request
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"sync"
"time"
"github.com/pkg/errors"
"golang.org/x/net/publicsuffix"
)
type Client struct {
BaseURL *url.URL
BaseHeaders *http.Header
HttpClient *http.Client
rwmutex sync.RWMutex
}
func NewClient(baseURL string, baseHeaders http.Header) (*Client, error) {
var err error
var jar *cookiejar.Jar
options := cookiejar.Options{PublicSuffixList: publicsuffix.List}
if jar, err = cookiejar.New(&options); err != nil {
return nil, errors.Wrap(err, "failed to create client object")
}
var u *url.URL
if u, err = url.Parse(baseURL); err != nil {
return nil, errors.Wrap(err, "failed to create client object")
}
header := make(http.Header)
header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_2 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Mobile/15B202 NETGEAR/v1 (iOS Vuezone)")
header.Set("Content-Type", "application/json")
header.Set("Accept", "application/json")
return &Client{
BaseURL: u,
BaseHeaders: &header,
HttpClient: &http.Client{Jar: jar, Timeout: 30 * time.Second},
}, nil
}
func (c *Client) AddHeader(key, value string) {
c.rwmutex.Lock()
c.BaseHeaders.Set(key, value)
c.rwmutex.Unlock()
}
func (c *Client) Get(uri string, header http.Header) (*Response, error) {
req, err := c.newRequest("GET", uri, nil, header)
if err != nil {
return nil, errors.WithMessage(err, "get request "+uri+" failed")
}
return c.do(req)
}
func (c *Client) Post(uri string, body interface{}, header http.Header) (*Response, error) {
req, err := c.newRequest("POST", uri, body, header)
if err != nil {
return nil, errors.WithMessage(err, "post request "+uri+" failed")
}
return c.do(req)
}
func (c *Client) Put(uri string, body interface{}, header http.Header) (*Response, error) {
req, err := c.newRequest("PUT", uri, body, header)
if err != nil {
return nil, errors.WithMessage(err, "put request "+uri+" failed")
}
return c.do(req)
}
func (c *Client) newRequest(method string, uri string, body interface{}, header http.Header) (*Request, error) {
var buf io.ReadWriter
if body != nil {
buf = new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(body)
if err != nil {
return nil, errors.Wrap(err, "failed to create request object")
}
}
// log.Printf("\n\nBODY (%s): %s\n\n", uri, buf)
u := c.BaseURL.String() + uri
req, err := http.NewRequest(method, u, buf)
if err != nil {
return nil, errors.Wrap(err, "failed to create request object")
}
c.rwmutex.RLock()
baseHeaders := *c.BaseHeaders
c.rwmutex.RUnlock()
for k, v := range baseHeaders {
for _, h := range v {
//log.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h)
req.Header.Set(k, h)
}
}
for k, v := range header {
for _, h := range v {
//log.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h)
req.Header.Set(k, h)
}
}
return &Request{
Request: *req,
}, nil
}
func (c *Client) newResponse(resp *http.Response) (*Response, error) {
return &Response{
Response: *resp,
}, nil
}
func (c *Client) do(req *Request) (*Response, error) {
// log.Printf("\n\nCOOKIES (%s): %v\n\n", req.URL, c.HttpClient.Jar.Cookies(req.URL))
// log.Printf("\n\nHEADERS (%s): %v\n\n", req.URL, req.Header)
resp, err := c.HttpClient.Do(&req.Request)
if err != nil {
return nil, errors.Wrap(err, "failed to execute http request")
}
if resp.StatusCode >= http.StatusBadRequest {
defer resp.Body.Close()
return nil, errors.New("http request failed with status: " + resp.Status)
}
return c.newResponse(resp)
}

View File

@ -1,25 +0,0 @@
/*
* Copyright (c) 2018 Jeffrey Walter <jeffreydwalter@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package request
import (
"net/http"
)
type Request struct {
http.Request
}

View File

@ -1,87 +0,0 @@
/*
* Copyright (c) 2018 Jeffrey Walter <jeffreydwalter@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package request
import (
"encoding/json"
"io"
"log"
"mime"
"net/http"
"os"
"reflect"
"github.com/pkg/errors"
)
type Response struct {
http.Response
}
func (resp *Response) GetContentType() (string, error) {
mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return "", errors.Wrap(err, "failed to get content type")
}
return mediaType, nil
}
func (resp *Response) Decode(s interface{}) error {
defer resp.Body.Close()
mediaType, err := resp.GetContentType()
if err != nil {
return errors.WithMessage(err, "failed to decode response body")
}
switch mediaType {
case "application/json":
err := json.NewDecoder(resp.Body).Decode(&s)
if err != nil {
return errors.Wrap(err, "failed to create "+reflect.TypeOf(s).String()+" object")
}
default:
return errors.New("unsupported content type: " + mediaType)
}
return nil
}
func (resp *Response) Download(to string) (error, int64) {
defer resp.Body.Close()
// Create output file
newFile, err := os.Create(to)
if err != nil {
log.Fatal(err)
}
defer newFile.Close()
// Write bytes from HTTP response to file.
// response.Body satisfies the reader interface.
// newFile satisfies the writer interface.
// That allows us to use io.Copy which accepts
// any type that implements reader and writer interface
bytesWritten, err := io.Copy(newFile, resp.Body)
if err != nil {
log.Fatal(err)
}
return nil, bytesWritten
}

View File

@ -1,94 +0,0 @@
/*
* Copyright (c) 2018 Jeffrey Walter <jeffreydwalter@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package util
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
)
func PrettyPrint(data interface{}) string {
j, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Sprint("error:", err)
}
return fmt.Sprint(string(j))
}
func FloatToHex(x float64) string {
var result []byte
quotient := int(x)
fraction := x - float64(quotient)
for quotient > 0 {
quotient = int(x / 16)
remainder := int(x - (float64(quotient) * 16))
if remainder > 9 {
result = append([]byte{byte(remainder + 55)}, result...)
} else {
for _, c := range strconv.Itoa(int(remainder)) {
result = append([]byte{byte(c)}, result...)
}
}
x = float64(quotient)
}
if fraction == 0 {
return string(result)
}
result = append(result, '.')
for fraction > 0 {
fraction = fraction * 16
integer := int(fraction)
fraction = fraction - float64(integer)
if integer > 9 {
result = append(result, byte(integer+55))
} else {
for _, c := range strconv.Itoa(int(integer)) {
result = append(result, byte(c))
}
}
}
return string(result)
}
func HeaderToArray(header http.Header) (res []string) {
for name, values := range header {
for _, value := range values {
res = append(res, fmt.Sprintf("%s: %s", name, value))
}
}
return
}
func HeaderToMap(header http.Header) map[string]string {
h := make(map[string]string)
for name, values := range header {
for _, value := range values {
h[name] = value
}
}
return h
}

View File

@ -16,12 +16,6 @@
package arlo package arlo
import (
"time"
"github.com/pkg/errors"
)
// LibraryMetaData is the library meta data. // LibraryMetaData is the library meta data.
type LibraryMetaData struct { type LibraryMetaData struct {
DateTo string `json:"dateTo"` DateTo string `json:"dateTo"`
@ -53,169 +47,87 @@ type Recording struct {
type Library []Recording type Library []Recording
func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (libraryMetaData *LibraryMetaData, err error) { //func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (libraryMetaData *LibraryMetaData, err error) {
msg := "failed to get library metadata" // msg := "failed to get library metadata"
//
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")} // body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
resp, err := a.post(MetadataUri, "", body, nil) // resp, err := a.post(MetadataUri, "", body, nil)
if err != nil { // if err != nil {
return nil, errors.WithMessage(err, msg) // return nil, errors.WithMessage(err, msg)
} // }
defer resp.Body.Close() // defer resp.Body.Close()
//
response := new(LibraryMetaDataResponse) // response := new(LibraryMetaDataResponse)
if err := resp.Decode(&response); err != nil { // if err := resp.Decode(&response); err != nil {
return nil, err // return nil, err
} // }
//
if !response.Success { // if !response.Success {
return nil, errors.New(msg) // return nil, errors.New(msg)
} // }
//
return &response.Data, nil // return &response.Data, nil
} //}
//
func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (library *Library, err error) { //func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (library *Library, err error) {
msg := "failed to get library" // msg := "failed to get library"
//
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")} // body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
resp, err := a.post(RecordingsUri, "", body, nil) // resp, err := a.post(RecordingsUri, "", body, nil)
if err != nil { // if err != nil {
return nil, errors.WithMessage(err, msg) // return nil, errors.WithMessage(err, msg)
} // }
defer resp.Body.Close() // defer resp.Body.Close()
//
response := new(LibraryResponse) // response := new(LibraryResponse)
if err := resp.Decode(&response); err != nil { // if err := resp.Decode(&response); err != nil {
return nil, err // return nil, err
} // }
//
if !response.Success { // if !response.Success {
return nil, errors.New(msg) // return nil, errors.New(msg)
} // }
//
return &response.Data, nil // return &response.Data, nil
} //}
//
/* ///*
Delete a single video recording from arlo. // Delete a single video recording from arlo.
//
All of the date info and device id you need to pass into this method are given in the results of the GetLibrary() call. // All of the date info and device id you need to pass into this method are given in the results of the GetLibrary() call.
//
NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required. // NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
*/ //*/
func (a *Arlo) DeleteRecording(r *Recording) error { //func (a *Arlo) DeleteRecording(r *Recording) error {
body := map[string]Library{"data": {*r}} // body := map[string]Library{"data": {*r}}
resp, err := a.post(RecycleUri, "", body, nil) // resp, err := a.post(RecycleUri, "", body, nil)
return checkRequest(resp, err, "failed to delete recording") // return checkRequest(resp, err, "failed to delete recording")
} //}
//
/* ///*
Delete a batch of video recordings from arlo. // Delete a batch of video recordings from arlo.
//
The GetLibrary() call response json can be passed directly to this method if you'd like to delete the same list of videos you queried for. // The GetLibrary() call response json can be passed directly to this method if you'd like to delete the same list of videos you queried for.
//
NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required. // NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
*/ //*/
func (a *Arlo) BatchDeleteRecordings(l *Library) error { //func (a *Arlo) BatchDeleteRecordings(l *Library) error {
body := map[string]Library{"data": *l} // body := map[string]Library{"data": *l}
resp, err := a.post(RecycleUri, "", body, nil) // resp, err := a.post(RecycleUri, "", body, nil)
return checkRequest(resp, err, "failed to delete recordings") // return checkRequest(resp, err, "failed to delete recordings")
} //}
//
// SendAnalyticFeedback is only really used by the GUI. It is a response to a prompt asking you whether an object which //// SendAnalyticFeedback is only really used by the GUI. It is a response to a prompt asking you whether an object which
// was tagged by it's AI in your recording was tagged correctly. //// was tagged by it's AI in your recording was tagged correctly.
func (a *Arlo) SendAnalyticFeedback(r *Recording) error { //func (a *Arlo) SendAnalyticFeedback(r *Recording) error {
category := "Person" // Other // category := "Person" // Other
body := map[string]map[string]interface{}{"data": {"utcCreatedDate": r.UtcCreatedDate, "category": category, "createdDate": r.CreatedDate}} // body := map[string]map[string]interface{}{"data": {"utcCreatedDate": r.UtcCreatedDate, "category": category, "createdDate": r.CreatedDate}}
resp, err := a.put(AnalyticFeedbackUri, "", body, nil) // resp, err := a.put(AnalyticFeedbackUri, "", body, nil)
return checkRequest(resp, err, "failed to send analytic feedback about recording") // return checkRequest(resp, err, "failed to send analytic feedback about recording")
} //}
//
// GetActiveAutomationDefinitions gets the mode metadata (this API replaces the older GetModes(), which still works). //// GetActiveAutomationDefinitions gets the mode metadata (this API replaces the older GetModes(), which still works).
func (a *Arlo) GetActiveAutomationDefinitions() error { //func (a *Arlo) GetActiveAutomationDefinitions() error {
resp, err := a.get(ActiveAutomationUri, "", nil) // resp, err := a.get(ActiveAutomationUri, "", nil)
return checkRequest(resp, err, "failed to get active automation definitions") // return checkRequest(resp, err, "failed to get active automation definitions")
} //}
/*
func (a *Arlo) SetActiveAutomationMode() error {
body := struct{}{} //map[string]map[string]interface{}{"data": {"utcCreatedDate": r.UtcCreatedDate, "category": category, "createdDate": r.CreatedDate}}
resp, err := a.put(AnalyticFeedbackUri, "", body, nil)
return checkRequest(resp, err, "failed to send analytic feedback about recording")
}
*/
/*
[
{
"activeModes": [
"mode1"
],
"activeSchedules": [],
"gatewayId": "48935B7SA9847",
"schemaVersion": 1,
"timestamp": 1536781758034,
"type": "activeAutomations",
"uniqueId": "336-4764296_48935B7SA9847"
}
]
*/
/*
setActiveAutomationMode: function(r, a) {
var s = {
activeAutomations: [{
deviceId: a.gatewayId,
timestamp: _.now(),
activeModes: [r],
activeSchedules: []
}]
}
, l = {
method: "POST",
data: s,
url: d.getActiveAutomationUrl(a.gatewayId),
headers: {
Authorization: o.ssoToken,
schemaVersion: 1
}
};
return n.debug("calling set active automation mode with config:" + JSON.stringify(l)),
i(l).then(function(i) {
if (n.debug("got set active automation mode result:" + JSON.stringify(i)),
i && i.data && !i.data.success)
return e.$broadcast(c.appEvents.SHOW_ERROR, i.data),
t.reject(i.data)
})
},
setActiveAutomationSchedule: function(r) {
var r = {
activeAutomations: [{
deviceId: r.deviceId,
timestamp: _.now(),
activeModes: [],
activeSchedules: [r.scheduleId]
}]
}
, a = {
method: "POST",
data: r,
url: d.getActiveAutomationUrl(r.deviceId),
headers: {
Authorization: o.ssoToken,
schemaVersion: 1
}
}
, s = this;
return n.debug("calling set active automation schedule with config:" + JSON.stringify(a)),
i(a).then(function(i) {
return n.debug("got set active automation schedule result:" + JSON.stringify(i)),
i && i.data && !i.data.success ? (e.$broadcast(c.appEvents.SHOW_ERROR, i.data),
t.reject(i.data)) : i && i.data && i.data.success ? (_.filter(s.activeAutomationDefinitions, function(e) {
e.gatewayId == i.config.data.activeAutomations[0].deviceId && (e.activeModes = i.config.data.activeAutomations[0].activeModes,
e.activeSchedules = i.config.data.activeAutomations[0].activeSchedules)
}),
i.config.data.activeAutomations[0]) : void 0
})
},
*/

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -85,4 +85,5 @@ type RecordingResponse struct {
type EventStreamResponse struct { type EventStreamResponse struct {
EventStreamPayload EventStreamPayload
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
RawProperties []byte
} }

View File

@ -251,14 +251,6 @@ type SirenProperties struct {
Pattern string `json:"pattern"` Pattern string `json:"pattern"`
} }
type BasestationModeProperties struct {
Active string `json:"active"`
}
type BasestationScheduleProperties struct {
Active bool `json:"active"`
}
type CameraProperties struct { type CameraProperties struct {
PrivacyActive bool `json:"privacyActive"` PrivacyActive bool `json:"privacyActive"`
Brightness int `json:"brightness,omitempty"` Brightness int `json:"brightness,omitempty"`

111
util.go
View File

@ -25,100 +25,69 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/jeffreydwalter/arlo-go/internal/request"
"github.com/jeffreydwalter/arlo-go/internal/util"
"github.com/pkg/errors"
) )
func checkRequest(resp *request.Response, err error, msg string) error { func FloatToHex(x float64) string {
if err != nil { var result []byte
return errors.WithMessage(err, msg) quotient := int(x)
} fraction := x - float64(quotient)
defer resp.Body.Close()
var status Status for quotient > 0 {
if err := resp.Decode(&status); err != nil { quotient = int(x / 16)
return err remainder := int(x - (float64(quotient) * 16))
if remainder > 9 {
result = append([]byte{byte(remainder + 55)}, result...)
} else {
for _, c := range strconv.Itoa(int(remainder)) {
result = append([]byte{byte(c)}, result...)
}
} }
if status.Success == false { x = float64(quotient)
return errors.WithMessage(errors.New(status.Reason), msg)
} }
return nil if fraction == 0 {
return string(result)
}
result = append(result, '.')
for fraction > 0 {
fraction = fraction * 16
integer := int(fraction)
fraction = fraction - float64(integer)
if integer > 9 {
result = append(result, byte(integer+55))
} else {
for _, c := range strconv.Itoa(int(integer)) {
result = append(result, byte(c))
}
}
}
return string(result)
} }
func genTransId() string { func genTransId() string {
source := rand.NewSource(time.Now().UnixNano()) random := rand.New(rand.NewSource(time.Now().UnixNano()))
random := rand.New(source)
e := random.Float64() * math.Pow(2, 32) e := random.Float64() * math.Pow(2, 32)
ms := time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond)) ms := time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
return fmt.Sprintf("%s!%s!%s", TransIdPrefix, strings.ToLower(util.FloatToHex(e)), strconv.Itoa(int(ms))) return fmt.Sprintf("%s!%s!%s", TransIdPrefix, strings.ToLower(FloatToHex(e)), strconv.Itoa(int(ms)))
} }
func (a *Arlo) get(uri, xCloudId string, header http.Header) (*request.Response, error) {
a.client.AddHeader("xcloudId", xCloudId)
return a.client.Get(uri, header)
}
func (a *Arlo) put(uri, xCloudId string, body interface{}, header http.Header) (*request.Response, error) {
a.client.AddHeader("xcloudId", xCloudId)
return a.client.Put(uri, body, header)
}
func (a *Arlo) post(uri, xCloudId string, body interface{}, header http.Header) (*request.Response, error) {
a.client.AddHeader("xcloudId", xCloudId)
return a.client.Post(uri, body, header)
}
/*
func (a *Arlo) DownloadFile(url, to string) error {
msg := fmt.Sprintf("failed to download file (%s) => (%s)", url, to)
resp, err := a.get(url, "", nil)
if err != nil {
return errors.WithMessage(err, msg)
}
defer resp.Body.Close()
f, err := os.Create(to)
if err != nil {
return errors.WithMessage(err, msg)
}
_, err = io.Copy(f, resp.Body)
defer f.Close()
if err != nil {
return errors.WithMessage(err, msg)
}
return nil
}
*/
func (a *Arlo) DownloadFile(url string, w io.Writer) error { func (a *Arlo) DownloadFile(url string, w io.Writer) error {
msg := fmt.Sprintf("failed to download file (%s)", url)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return errors.WithMessage(err, msg) return fmt.Errorf("getting %s: %v", url, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
_, err = io.Copy(w, resp.Body) _, err = io.Copy(w, resp.Body)
if err != nil { if err != nil {
return errors.WithMessage(err, msg) return fmt.Errorf("copying body to writer: %v", err)
} }
return nil return nil
} }
func FromUnixMicro(µs int64) time.Time { return time.Unix(0, 1000*µs) }
func FromUnixMilli(ms int64) time.Time { return time.Unix(0, 1000000*ms) }