big refactorisation
This commit is contained in:
parent
d68ae1fb4c
commit
2dd8c960d1
21
LICENSE
21
LICENSE
@ -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.
|
12
Makefile
12
Makefile
@ -1,12 +0,0 @@
|
||||
|
||||
all: test build
|
||||
|
||||
build:
|
||||
go build -v ./...
|
||||
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
clean:
|
||||
go clean
|
||||
|
130
README.md
130
README.md
@ -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¤cy_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)**
|
224
arlo.go
224
arlo.go
@ -17,121 +17,106 @@
|
||||
package arlo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jeffreydwalter/arlo-go/internal/request"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
type Arlo struct {
|
||||
user string
|
||||
pass string
|
||||
client *request.Client
|
||||
client *resty.Client
|
||||
Account Account
|
||||
Basestations Basestations
|
||||
Cameras Cameras
|
||||
rwmutex sync.RWMutex
|
||||
}
|
||||
|
||||
func newArlo(user string, pass string) (arlo *Arlo) {
|
||||
func NewArlo() (arlo *Arlo) {
|
||||
|
||||
// Add important headers.
|
||||
baseHeaders := make(http.Header)
|
||||
baseHeaders.Add("DNT", "1")
|
||||
baseHeaders.Add("schemaVersion", "1")
|
||||
baseHeaders.Add("Host", "my.arlo.com")
|
||||
baseHeaders.Add("Referer", "https://my.arlo.com/")
|
||||
|
||||
c, _ := request.NewClient(BaseUrl, baseHeaders)
|
||||
c := resty.New().
|
||||
SetHostURL(BaseUrl).
|
||||
SetTimeout(30 * time.Second)
|
||||
|
||||
return &Arlo{
|
||||
user: user,
|
||||
pass: pass,
|
||||
client: c,
|
||||
}
|
||||
}
|
||||
|
||||
func Login(user string, pass string) (arlo *Arlo, err 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()
|
||||
|
||||
func (a *Arlo) Login(ctx context.Context, user string, pass string) error {
|
||||
var loginResponse LoginResponse
|
||||
if err := resp.Decode(&loginResponse); err != nil {
|
||||
return nil, err
|
||||
_, err := a.client.R().
|
||||
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.
|
||||
arlo.client.AddHeader("Authorization", loginResponse.Data.Token)
|
||||
|
||||
// Save the account info with the arlo struct.
|
||||
arlo.Account = loginResponse.Data
|
||||
|
||||
// Get the devices, which also caches them on the arlo object.
|
||||
if _, err := arlo.GetDevices(); err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to login")
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("failed to login")
|
||||
// Cache the auth token.
|
||||
a.client.SetHeader("Authorization", loginResponse.Data.Token)
|
||||
// Save the account info with the arlo struct.
|
||||
a.Account = loginResponse.Data
|
||||
// Get the devices, which also caches them on the arlo object.
|
||||
if _, err := a.GetDevices(ctx); err != nil {
|
||||
return fmt.Errorf("getting devices: %v", err)
|
||||
}
|
||||
|
||||
return arlo, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
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) {
|
||||
msg := "failed to get session"
|
||||
resp, err := a.get(SessionUri, "", nil)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, msg)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
func (a *Arlo) GetSession() (*Session, error) {
|
||||
var response SessionResponse
|
||||
if err := resp.Decode(&response); err != nil {
|
||||
return nil, err
|
||||
_, err := a.client.R().
|
||||
SetResult(&response).
|
||||
Get(SessionUri)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting session: %v", err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// GetDevices returns an array of all devices.
|
||||
// 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()
|
||||
|
||||
func (a *Arlo) GetDevices(ctx context.Context) (*Devices, error) {
|
||||
var response DeviceResponse
|
||||
if err := resp.Decode(&response); err != nil {
|
||||
return nil, err
|
||||
_, err := a.client.R().
|
||||
SetResult(&response).
|
||||
Get(fmt.Sprintf(DevicesUri, time.Now().Format("20060102")))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting devices: %v", err)
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return nil, errors.New("failed to get devices")
|
||||
return nil, fmt.Errorf("failed to get devices")
|
||||
}
|
||||
|
||||
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.
|
||||
@ -140,74 +125,69 @@ func (a *Arlo) GetDevices() (devices *Devices, err error) {
|
||||
}
|
||||
|
||||
// Disconnect all of the basestations from the EventStream.
|
||||
for i := range a.Basestations {
|
||||
if err := a.Basestations[i].Disconnect(); err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to get devices")
|
||||
for _, basestation := range a.Basestations {
|
||||
if err := basestation.Disconnect(); err != nil {
|
||||
return nil, fmt.Errorf("disconnecting device %s: %v", basestation.DeviceName, err)
|
||||
}
|
||||
}
|
||||
|
||||
a.rwmutex.Lock()
|
||||
// Cache the devices as their respective types.
|
||||
a.Cameras = *response.Data.GetCameras()
|
||||
a.Basestations = *response.Data.GetBasestations()
|
||||
a.Cameras = response.Data.GetCameras()
|
||||
a.Basestations = response.Data.GetBasestations()
|
||||
a.rwmutex.Unlock()
|
||||
|
||||
// subscribe each basestation to the EventStream.
|
||||
for i := range a.Basestations {
|
||||
if err := a.Basestations[i].Subscribe(); err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to get devices")
|
||||
for _, basestation := range a.Basestations {
|
||||
if err := basestation.Subscribe(ctx); err != nil {
|
||||
return nil, fmt.Errorf("subscribing device %s: %v", basestation.DeviceName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &response.Data, nil
|
||||
}
|
||||
|
||||
// GetProfile returns the user profile for the currently logged in user.
|
||||
func (a *Arlo) GetProfile() (profile *UserProfile, err error) {
|
||||
resp, err := a.get(ProfileUri, "", nil)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to get user profile")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
func (a *Arlo) GetProfile() (*UserProfile, error) {
|
||||
var response UserProfileResponse
|
||||
if err := resp.Decode(&response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err := a.client.R().
|
||||
SetResult(&response).
|
||||
Get(ProfileUri)
|
||||
|
||||
if !response.Success {
|
||||
return nil, errors.New("failed to get user profile")
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given.
|
||||
func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) error {
|
||||
resp, err := a.post(CameraOrderUri, "", d, nil)
|
||||
return checkRequest(resp, err, "failed to display order")
|
||||
}
|
||||
|
||||
// UpdateProfile takes a first and last name, and updates the user profile with that information.
|
||||
func (a *Arlo) UpdateProfile(firstName, lastName string) error {
|
||||
body := map[string]string{"firstName": firstName, "lastName": lastName}
|
||||
resp, err := a.put(ProfileUri, "", body, nil)
|
||||
return checkRequest(resp, err, "failed to update profile")
|
||||
}
|
||||
|
||||
func (a *Arlo) UpdatePassword(pass string) error {
|
||||
body := map[string]string{"currentPassword": a.pass, "newPassword": pass}
|
||||
resp, err := a.post(UpdatePasswordUri, "", body, nil)
|
||||
if err := checkRequest(resp, err, "failed to update password"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.pass = pass
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Arlo) UpdateFriends(f Friend) error {
|
||||
resp, err := a.put(FriendsUri, "", f, nil)
|
||||
return checkRequest(resp, err, "failed to update friends")
|
||||
}
|
||||
//// UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given.
|
||||
//func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) error {
|
||||
// resp, err := a.post(CameraOrderUri, "", d, nil)
|
||||
// return checkRequest(resp, err, "failed to display order")
|
||||
//}
|
||||
//
|
||||
//// UpdateProfile takes a first and last name, and updates the user profile with that information.
|
||||
//func (a *Arlo) UpdateProfile(firstName, lastName string) error {
|
||||
// body := map[string]string{"firstName": firstName, "lastName": lastName}
|
||||
// resp, err := a.put(ProfileUri, "", body, nil)
|
||||
// return checkRequest(resp, err, "failed to update profile")
|
||||
//}
|
||||
//
|
||||
//func (a *Arlo) UpdatePassword(pass string) error {
|
||||
// body := map[string]string{"currentPassword": a.pass, "newPassword": pass}
|
||||
// resp, err := a.post(UpdatePasswordUri, "", body, nil)
|
||||
// if err := checkRequest(resp, err, "failed to update password"); err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// a.pass = pass
|
||||
//
|
||||
// return nil
|
||||
//}
|
||||
//
|
||||
//func (a *Arlo) UpdateFriends(f Friend) error {
|
||||
// resp, err := a.put(FriendsUri, "", f, nil)
|
||||
// return checkRequest(resp, err, "failed to update friends")
|
||||
//}
|
||||
|
17
arlo_test.go
17
arlo_test.go
@ -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
|
522
arlobaby.go
522
arlobaby.go
@ -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)
|
||||
}
|
357
basestation.go
357
basestation.go
@ -17,14 +17,16 @@
|
||||
package arlo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"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
|
||||
|
||||
// A Basestation is a Device that's not type "camera" (basestation, arloq, arloqs, etc.).
|
||||
@ -34,41 +36,108 @@ type Basestation struct {
|
||||
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.
|
||||
type Basestations []Basestation
|
||||
type Basestations []*Basestation
|
||||
|
||||
// Find returns a basestation with the device id passed in.
|
||||
func (bs *Basestations) Find(deviceId string) *Basestation {
|
||||
for _, b := range *bs {
|
||||
if b.DeviceId == deviceId {
|
||||
return &b
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
func (b *Basestation) makeEventStreamRequest(payload EventStreamPayload, msg string) (response *EventStreamResponse, err error) {
|
||||
func (b *Basestation) makeEventStreamRequest(payload EventStreamPayload) (*EventStreamResponse, error) {
|
||||
transId := genTransId()
|
||||
payload.TransId = transId
|
||||
|
||||
if err := b.IsConnected(); err != nil {
|
||||
//if err := b.Subscribe(); err != nil {
|
||||
return nil, errors.WithMessage(errors.WithMessage(err, msg), "failed to reconnect to event stream")
|
||||
//}
|
||||
return nil, fmt.Errorf("event stream not connected")
|
||||
}
|
||||
|
||||
subscriber := make(subscriber)
|
||||
|
||||
// Add the response channel to the event stream queue so the response can be written to it.
|
||||
b.eventStream.subscribe(transId, subscriber)
|
||||
// Make sure we close and remove the response channel before returning.
|
||||
responseChan := make(chan *EventStreamResponse)
|
||||
errorChan := make(chan error)
|
||||
b.eventStream.subscribe(transId, responseChan, errorChan)
|
||||
defer b.eventStream.unsubscribe(transId)
|
||||
|
||||
// Send the payload to the event stream.
|
||||
if err := b.NotifyEventStream(payload, msg); err != nil {
|
||||
return nil, err
|
||||
if err := b.NotifyEventStream(payload); err != nil {
|
||||
return nil, fmt.Errorf("notifying event stream: %v", err)
|
||||
}
|
||||
|
||||
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.
|
||||
select {
|
||||
// If we get a response, return it to the caller.
|
||||
case response := <-subscriber:
|
||||
case response := <-responseChan:
|
||||
return response, nil
|
||||
case err = <-b.eventStream.Error:
|
||||
return nil, errors.Wrap(err, msg)
|
||||
case err := <-b.eventStream.Error:
|
||||
return nil, fmt.Errorf("event stream error: %v", err)
|
||||
// If the event stream is closed, return an error about it.
|
||||
case <-b.eventStream.Disconnected:
|
||||
err = errors.New("event stream was closed before response was read")
|
||||
return nil, errors.WithMessage(err, msg)
|
||||
case <-b.eventStream.DisconnectedChan:
|
||||
return nil, fmt.Errorf("event stream was closed before response was read")
|
||||
// If we timeout, return an error about it.
|
||||
case <-timer.C:
|
||||
err = fmt.Errorf("event stream response timed out after %.0f second", eventStreamTimeout.Seconds())
|
||||
return nil, errors.WithMessage(err, msg)
|
||||
return nil, fmt.Errorf("event stream response timed out after %.0f second", eventStreamTimeout.Seconds())
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Basestation) IsConnected() error {
|
||||
// If the event stream is closed, return an error about it.
|
||||
select {
|
||||
case <-b.eventStream.Disconnected:
|
||||
return errors.New("basestation not connected to event stream")
|
||||
case <-b.eventStream.DisconnectedChan:
|
||||
return fmt.Errorf("basestation not connected to event stream")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Basestation) Subscribe() error {
|
||||
b.eventStream = newEventStream(BaseUrl+fmt.Sprintf(NotifyResponsesPushServiceUri, b.arlo.Account.Token), &http.Client{Jar: b.arlo.client.HttpClient.Jar})
|
||||
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.GetClient().Jar})
|
||||
|
||||
connectedChan, err := b.eventStream.listen(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up event stream: %v", err)
|
||||
}
|
||||
forLoop:
|
||||
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 {
|
||||
case connected := <-b.eventStream.listen():
|
||||
if connected {
|
||||
break forLoop
|
||||
} else {
|
||||
return errors.New("failed to subscribe to the event stream")
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("failed to subscribe to the event stream: requesting shutdown")
|
||||
case connected := <-connectedChan:
|
||||
if !connected {
|
||||
return fmt.Errorf("failed to subscribe to the event stream")
|
||||
}
|
||||
case <-b.eventStream.Disconnected:
|
||||
err := errors.New("event stream was closed")
|
||||
return errors.WithMessage(err, "failed to subscribe to the event stream")
|
||||
break forLoop
|
||||
case <-b.eventStream.DisconnectedChan:
|
||||
return fmt.Errorf("failed to subscribe to the event stream: event stream was closed")
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
go func() {
|
||||
go func(ctx context.Context) {
|
||||
ticker := time.NewTicker(pingTime)
|
||||
for {
|
||||
time.Sleep(pingTime)
|
||||
if err := b.Ping(); err != nil {
|
||||
b.Disconnect()
|
||||
break
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case _ = <-ticker.C:
|
||||
if err := b.Ping(); err != nil {
|
||||
log.Errorf("Pingloop > error while pinging: %v > disconnect event stream", err)
|
||||
_ = b.Disconnect()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Basestation) Unsubscribe() error {
|
||||
resp, err := b.arlo.get(UnsubscribeUri, b.XCloudId, nil)
|
||||
return checkRequest(resp, err, "failed to unsubscribe from event stream")
|
||||
var response Status
|
||||
_, 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 {
|
||||
@ -164,57 +250,75 @@ func (b *Basestation) Ping() error {
|
||||
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 nil
|
||||
}
|
||||
|
||||
func (b *Basestation) NotifyEventStream(payload EventStreamPayload, msg string) error {
|
||||
resp, err := b.arlo.post(fmt.Sprintf(NotifyUri, b.DeviceId), b.XCloudId, payload, nil)
|
||||
if err := checkRequest(resp, err, msg); err != nil {
|
||||
return errors.WithMessage(err, "failed to notify event stream")
|
||||
func (b *Basestation) NotifyEventStream(payload EventStreamPayload) error {
|
||||
var response Status
|
||||
_, err := b.arlo.client.R().
|
||||
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
|
||||
}
|
||||
|
||||
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{
|
||||
Action: "get",
|
||||
Resource: "basestation",
|
||||
PublishResponse: false,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
PublishResponse: publishResponse,
|
||||
Properties: properties,
|
||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
||||
To: b.DeviceId,
|
||||
}
|
||||
|
||||
return b.makeEventStreamRequest(payload, "failed to get basestation state")
|
||||
resp, err := b.makeEventStreamRequest(payload)
|
||||
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) {
|
||||
payload := EventStreamPayload{
|
||||
Action: "get",
|
||||
Resource: "cameras",
|
||||
PublishResponse: false,
|
||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
||||
To: b.DeviceId,
|
||||
func (b *Basestation) GetState() (*BaseStationState, error) {
|
||||
var state BaseStationState
|
||||
err := b.makeRequest("get", "basestation", false, nil, &state)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting basestation %s state: %v", b.DeviceName, err)
|
||||
}
|
||||
|
||||
return b.makeEventStreamRequest(payload, "failed to get associated cameras state")
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
func (b *Basestation) GetRules() (response *EventStreamResponse, err error) {
|
||||
payload := EventStreamPayload{
|
||||
Action: "get",
|
||||
Resource: "rules",
|
||||
PublishResponse: false,
|
||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
||||
To: b.DeviceId,
|
||||
func (b *Basestation) GetAllCameraState() ([]CameraState, error) {
|
||||
var states []CameraState
|
||||
err := b.makeRequest("get", "cameras", false, nil, &states)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting associated cameras state: %v", err)
|
||||
}
|
||||
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) {
|
||||
@ -226,52 +330,59 @@ func (b *Basestation) GetCalendarMode() (response *EventStreamResponse, err erro
|
||||
To: b.DeviceId,
|
||||
}
|
||||
|
||||
return b.makeEventStreamRequest(payload, "failed to get schedule")
|
||||
return b.makeEventStreamRequest(payload)
|
||||
}
|
||||
|
||||
// SetCalendarMode toggles calendar mode.
|
||||
// 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.
|
||||
func (b *Basestation) SetCalendarMode(active bool) (response *EventStreamResponse, err error) {
|
||||
payload := EventStreamPayload{
|
||||
Action: "set",
|
||||
Resource: "schedule",
|
||||
PublishResponse: true,
|
||||
Properties: BasestationScheduleProperties{
|
||||
Active: active,
|
||||
},
|
||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
||||
To: b.DeviceId,
|
||||
func (b *Basestation) SetCalendarMode(active bool) error {
|
||||
resp := make(map[string]bool)
|
||||
err := b.makeRequest("set", "schedule", true, struct {
|
||||
Active bool `json:"active"`
|
||||
}{
|
||||
Active: active,
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting calendar mode %t: %v", active, err)
|
||||
}
|
||||
|
||||
return b.makeEventStreamRequest(payload, "failed to set schedule")
|
||||
activemode, ok := resp["active"]
|
||||
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) {
|
||||
payload := EventStreamPayload{
|
||||
Action: "get",
|
||||
Resource: "modes",
|
||||
PublishResponse: false,
|
||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
||||
To: b.DeviceId,
|
||||
func (b *Basestation) GetModes() (*GetModesResponse, error) {
|
||||
var resp GetModesResponse
|
||||
err := b.makeRequest("get", "modes", false, nil, &resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting modes: %v", err)
|
||||
}
|
||||
|
||||
return b.makeEventStreamRequest(payload, "failed to get modes")
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (b *Basestation) SetCustomMode(mode string) (response *EventStreamResponse, err error) {
|
||||
payload := EventStreamPayload{
|
||||
Action: "set",
|
||||
Resource: "modes",
|
||||
PublishResponse: true,
|
||||
Properties: BasestationModeProperties{
|
||||
Active: mode,
|
||||
},
|
||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
||||
To: b.DeviceId,
|
||||
func (b *Basestation) SetCustomMode(mode string) error {
|
||||
resp := make(map[string]string)
|
||||
err := b.makeRequest("set", "modes", true, struct {
|
||||
Active string `json:"active"`
|
||||
}{
|
||||
Active: mode,
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting custom mode %s: %v", mode, err)
|
||||
}
|
||||
|
||||
return b.makeEventStreamRequest(payload, "failed to set mode")
|
||||
activemode, ok := resp["active"]
|
||||
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) {
|
||||
@ -283,15 +394,23 @@ func (b *Basestation) DeleteMode(mode string) (response *EventStreamResponse, er
|
||||
To: b.DeviceId,
|
||||
}
|
||||
|
||||
return b.makeEventStreamRequest(payload, "failed to set mode")
|
||||
return b.makeEventStreamRequest(payload)
|
||||
}
|
||||
|
||||
func (b *Basestation) Arm() (response *EventStreamResponse, err error) {
|
||||
return b.SetCustomMode("mode1")
|
||||
func (b *Basestation) Arm() error {
|
||||
err := b.SetCustomMode("mode1")
|
||||
if err != nil {
|
||||
return fmt.Errorf("arming (mode1): %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Basestation) Disarm() (response *EventStreamResponse, err error) {
|
||||
return b.SetCustomMode("mode0")
|
||||
func (b *Basestation) Disarm() error {
|
||||
err := b.SetCustomMode("mode0")
|
||||
if err != nil {
|
||||
return fmt.Errorf("disarming (mode0): %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Basestation) SirenOn() (response *EventStreamResponse, err error) {
|
||||
@ -309,7 +428,7 @@ func (b *Basestation) SirenOn() (response *EventStreamResponse, err error) {
|
||||
To: b.DeviceId,
|
||||
}
|
||||
|
||||
return b.makeEventStreamRequest(payload, "failed to get modes")
|
||||
return b.makeEventStreamRequest(payload)
|
||||
}
|
||||
|
||||
func (b *Basestation) SirenOff() (response *EventStreamResponse, err error) {
|
||||
@ -327,5 +446,5 @@ func (b *Basestation) SirenOff() (response *EventStreamResponse, err error) {
|
||||
To: b.DeviceId,
|
||||
}
|
||||
|
||||
return b.makeEventStreamRequest(payload, "failed to get modes")
|
||||
return b.makeEventStreamRequest(payload)
|
||||
}
|
||||
|
478
camera.go
478
camera.go
@ -18,24 +18,88 @@ package arlo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// 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.
|
||||
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.
|
||||
type Cameras []Camera
|
||||
type Cameras []*Camera
|
||||
|
||||
// Find returns a camera with the device id passed in.
|
||||
func (cs *Cameras) Find(deviceId string) *Camera {
|
||||
for _, c := range *cs {
|
||||
if c.DeviceId == deviceId {
|
||||
return &c
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,14 +119,11 @@ func (c *Camera) On() (response *EventStreamResponse, err error) {
|
||||
To: c.ParentId,
|
||||
}
|
||||
|
||||
msg := "failed to turn camera on"
|
||||
|
||||
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 nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||
}
|
||||
return b.makeEventStreamRequest(payload, msg)
|
||||
return b.makeEventStreamRequest(payload)
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
msg := "failed to turn camera 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 nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||
}
|
||||
return b.makeEventStreamRequest(payload, msg)
|
||||
return b.makeEventStreamRequest(payload)
|
||||
}
|
||||
|
||||
// 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),
|
||||
To: c.ParentId,
|
||||
}
|
||||
|
||||
msg := "failed to set camera 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 nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||
}
|
||||
return b.makeEventStreamRequest(payload, msg)
|
||||
return b.makeEventStreamRequest(payload)
|
||||
}
|
||||
|
||||
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),
|
||||
To: c.ParentId,
|
||||
}
|
||||
|
||||
msg := "failed to enable motion alerts"
|
||||
|
||||
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 nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||
}
|
||||
return b.makeEventStreamRequest(payload, msg)
|
||||
return b.makeEventStreamRequest(payload)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
msg := "failed to enable motion alerts"
|
||||
|
||||
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 nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||
}
|
||||
return b.makeEventStreamRequest(payload, msg)
|
||||
return b.makeEventStreamRequest(payload)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
msg := "failed to enable audio alerts"
|
||||
|
||||
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 nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||
}
|
||||
return b.makeEventStreamRequest(payload, msg)
|
||||
return b.makeEventStreamRequest(payload)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
msg := "failed to disable audio alerts"
|
||||
|
||||
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 nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||
}
|
||||
return b.makeEventStreamRequest(payload, msg)
|
||||
}
|
||||
|
||||
// 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")
|
||||
return b.makeEventStreamRequest(payload)
|
||||
}
|
||||
|
||||
// action: disabled OR recordSnapshot OR recordVideo
|
||||
@ -422,178 +285,9 @@ func (c *Camera) SetAlertNotificationMethods(action string, email, push bool) (r
|
||||
To: c.ParentId,
|
||||
}
|
||||
|
||||
msg := "failed to set alert notification methods"
|
||||
|
||||
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 nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||
}
|
||||
return b.makeEventStreamRequest(payload, msg)
|
||||
}
|
||||
|
||||
// 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
|
||||
return b.makeEventStreamRequest(payload)
|
||||
}
|
||||
|
30
cmd/main.go
Normal file
30
cmd/main.go
Normal 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 {}
|
||||
|
||||
}
|
61
devices.go
61
devices.go
@ -46,7 +46,7 @@ type Device struct {
|
||||
PresignedFullFrameSnapshotUrl string `json:"presignedFullFrameSnapshotUrl"`
|
||||
PresignedLastImageUrl string `json:"presignedLastImageUrl"`
|
||||
PresignedSnapshotUrl string `json:"presignedSnapshotUrl"`
|
||||
MediaObjectCount uint8 `json:"mediaObjectCount"`
|
||||
MediaObjectCount uint32 `json:"mediaObjectCount"`
|
||||
ModelId string `json:"modelId"`
|
||||
Owner Owner `json:"owner"`
|
||||
ParentId string `json:"parentId"`
|
||||
@ -59,7 +59,7 @@ type Device struct {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
func (ds *Devices) Find(deviceId string) *Device {
|
||||
for _, d := range *ds {
|
||||
func (ds Devices) Find(deviceId string) *Device {
|
||||
for _, d := range ds {
|
||||
if d.DeviceId == deviceId {
|
||||
return &d
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,28 +87,28 @@ func (ds *Devices) Find(deviceId string) *Device {
|
||||
}
|
||||
|
||||
func (ds Devices) FindCameras(basestationId string) Cameras {
|
||||
cs := new(Cameras)
|
||||
cs := Cameras{}
|
||||
for _, d := range ds {
|
||||
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 {
|
||||
return d.DeviceType == DeviceTypeBasestation || d.DeviceId == d.ParentId
|
||||
return d.DeviceType == DeviceTypeBasestation
|
||||
}
|
||||
|
||||
func (d Device) IsCamera() bool {
|
||||
switch(d.DeviceType) {
|
||||
case
|
||||
DeviceTypeCamera,
|
||||
DeviceTypeArloQ:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
switch d.DeviceType {
|
||||
case
|
||||
DeviceTypeCamera,
|
||||
DeviceTypeArloQ:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d Device) IsArloQ() bool {
|
||||
@ -127,11 +127,11 @@ func (d Device) IsSiren() bool {
|
||||
// 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.
|
||||
// Cameras also includes devices of this type, so you can get the same data there or cast.
|
||||
func (ds Devices) GetBasestations() *Basestations {
|
||||
basestations := new(Basestations)
|
||||
func (ds Devices) GetBasestations() Basestations {
|
||||
var basestations Basestations
|
||||
for _, d := range ds {
|
||||
if d.IsBasestation() || !d.IsCamera() {
|
||||
*basestations = append(*basestations, Basestation{Device: d})
|
||||
if d.IsBasestation() {
|
||||
basestations = append(basestations, &Basestation{Device: *d})
|
||||
}
|
||||
}
|
||||
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.
|
||||
// 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.
|
||||
func (ds Devices) GetCameras() *Cameras {
|
||||
cameras := new(Cameras)
|
||||
func (ds Devices) GetCameras() Cameras {
|
||||
var cameras Cameras
|
||||
for _, d := range ds {
|
||||
if d.IsCamera() || !d.IsBasestation() {
|
||||
*cameras = append(*cameras, Camera(d))
|
||||
if d.IsCamera() {
|
||||
cam := Camera(*d)
|
||||
cameras = append(cameras, &cam)
|
||||
}
|
||||
}
|
||||
return cameras
|
||||
}
|
||||
|
||||
// UpdateDeviceName sets the name of the given device to the name argument.
|
||||
func (d *Device) UpdateDeviceName(name string) error {
|
||||
body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId}
|
||||
resp, err := d.arlo.put(RenameDeviceUri, d.XCloudId, body, nil)
|
||||
return checkRequest(resp, err, "failed to update device name")
|
||||
}
|
||||
//func (d *Device) UpdateDeviceName(name string) error {
|
||||
// body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId}
|
||||
// resp, err := d.arlo.put(RenameDeviceUri, d.XCloudId, body, nil)
|
||||
// return checkRequest(resp, err, "failed to update device name")
|
||||
//}
|
||||
|
196
events_stream.go
196
events_stream.go
@ -18,132 +18,148 @@ package arlo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"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 {
|
||||
SSEClient *sse.Client
|
||||
Events chan *sse.Event
|
||||
Error chan error
|
||||
Verbose bool
|
||||
Disconnected chan interface{}
|
||||
once *sync.Once
|
||||
SSEClient *sse.Client
|
||||
Events chan *sse.Event
|
||||
Error chan error
|
||||
Verbose bool
|
||||
DisconnectedChan chan interface{}
|
||||
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 {
|
||||
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.Connection = client
|
||||
|
||||
return &eventStream{
|
||||
SSEClient: SSEClient,
|
||||
Events: make(chan *sse.Event),
|
||||
subscriptions: subscriptions{make(map[string]subscriber), sync.RWMutex{}},
|
||||
Error: make(chan error),
|
||||
Disconnected: make(chan interface{}),
|
||||
once: new(sync.Once),
|
||||
}
|
||||
SSEClient.OnDisconnect(func(c *sse.Client) {
|
||||
e.disconnect()
|
||||
})
|
||||
e.SSEClient = SSEClient
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *eventStream) disconnect() {
|
||||
e.once.Do(func() {
|
||||
close(e.Disconnected)
|
||||
close(e.DisconnectedChan)
|
||||
})
|
||||
}
|
||||
|
||||
func (e *eventStream) listen() (connected chan bool) {
|
||||
connected = make(chan bool)
|
||||
func (e *eventStream) listen(ctx context.Context) (chan bool, error) {
|
||||
connectedChan := make(chan bool)
|
||||
err := e.SSEClient.SubscribeChanRaw(e.Events)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to subscribe to seeclient")
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
e.Error <- FAILED_TO_SUBSCRIBE
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-e.Events:
|
||||
//fmt.Println("Got event message.")
|
||||
/*
|
||||
fmt.Print(".")
|
||||
fmt.Printf("EVENT: %s\n", event.Event)
|
||||
fmt.Printf("DATA: %s\n", event.Data)
|
||||
*/
|
||||
|
||||
if event != nil && event.Data != nil {
|
||||
notifyResponse := &EventStreamResponse{}
|
||||
b := bytes.NewBuffer(event.Data)
|
||||
err := json.NewDecoder(b).Decode(notifyResponse)
|
||||
if err != nil {
|
||||
e.Error <- FAILED_TO_DECODE_JSON
|
||||
break
|
||||
}
|
||||
|
||||
// FIXME: This is a shitty way to handle this. It's potentially leaking a chan.
|
||||
if notifyResponse.Status == "connected" {
|
||||
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 <-ctx.Done():
|
||||
e.disconnect()
|
||||
return
|
||||
case event, ok := <-e.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
case <-e.Disconnected:
|
||||
connected <- false
|
||||
|
||||
if event == nil || event.Data == nil {
|
||||
log.Warn("EventStream > nil event or nil data in event")
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("DATA : %s\n", event.Data)
|
||||
|
||||
var notifyResponse EventStreamResponse
|
||||
err := json.NewDecoder(bytes.NewBuffer(event.Data)).Decode(¬ifyResponse)
|
||||
if err != nil {
|
||||
log.Warnf("EventStream > failed to decode event: %s", event.Data)
|
||||
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 <- ¬ifyResponse
|
||||
}
|
||||
|
||||
case <-e.DisconnectedChan:
|
||||
connectedChan <- false
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return connected
|
||||
return connectedChan, nil
|
||||
}
|
||||
|
||||
func (s *subscriptions) unsubscribe(transId string) {
|
||||
s.rwmutex.Lock()
|
||||
defer s.rwmutex.Unlock()
|
||||
if _, ok := s.subscribers[transId]; ok {
|
||||
close(s.subscribers[transId])
|
||||
delete(s.subscribers, transId)
|
||||
func (e *eventStream) unsubscribe(transId string) {
|
||||
e.subscribersMutex.Lock()
|
||||
if c, ok := e.subscribers[transId]; ok {
|
||||
close(c)
|
||||
delete(e.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) {
|
||||
s.rwmutex.Lock()
|
||||
s.subscribers[transId] = subscriber
|
||||
s.rwmutex.Unlock()
|
||||
func (e *eventStream) subscribe(transId string, subscriber chan *EventStreamResponse, errorChan chan error) {
|
||||
e.subscribersMutex.Lock()
|
||||
e.subscribers[transId] = subscriber
|
||||
e.subscribersMutex.Unlock()
|
||||
|
||||
e.errorMutex.Lock()
|
||||
e.errorsubsribers[transId] = errorChan
|
||||
e.errorMutex.Unlock()
|
||||
}
|
||||
|
8
go.mod
8
go.mod
@ -1,9 +1,9 @@
|
||||
module github.com/jeffreydwalter/arlo-go
|
||||
module git.lehouerou.net/laurent/arlo-go
|
||||
|
||||
go 1.13
|
||||
go 1.14
|
||||
|
||||
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
|
||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
)
|
||||
|
23
go.sum
23
go.sum
@ -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.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/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/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/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/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=
|
||||
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-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-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-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=
|
||||
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/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/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
BIN
gopher-arlo.png
BIN
gopher-arlo.png
Binary file not shown.
Before Width: | Height: | Size: 46 KiB |
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
256
library.go
256
library.go
@ -16,12 +16,6 @@
|
||||
|
||||
package arlo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// LibraryMetaData is the library meta data.
|
||||
type LibraryMetaData struct {
|
||||
DateTo string `json:"dateTo"`
|
||||
@ -53,169 +47,87 @@ type Recording struct {
|
||||
|
||||
type Library []Recording
|
||||
|
||||
func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (libraryMetaData *LibraryMetaData, err error) {
|
||||
msg := "failed to get library metadata"
|
||||
|
||||
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
|
||||
resp, err := a.post(MetadataUri, "", body, nil)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, msg)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
response := new(LibraryMetaDataResponse)
|
||||
if err := resp.Decode(&response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
return &response.Data, nil
|
||||
}
|
||||
|
||||
func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (library *Library, err error) {
|
||||
msg := "failed to get library"
|
||||
|
||||
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
|
||||
resp, err := a.post(RecordingsUri, "", body, nil)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, msg)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
response := new(LibraryResponse)
|
||||
if err := resp.Decode(&response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
return &response.Data, nil
|
||||
}
|
||||
|
||||
/*
|
||||
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.
|
||||
|
||||
NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
|
||||
*/
|
||||
func (a *Arlo) DeleteRecording(r *Recording) error {
|
||||
body := map[string]Library{"data": {*r}}
|
||||
resp, err := a.post(RecycleUri, "", body, nil)
|
||||
return checkRequest(resp, err, "failed to delete recording")
|
||||
}
|
||||
|
||||
/*
|
||||
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.
|
||||
|
||||
NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
|
||||
*/
|
||||
func (a *Arlo) BatchDeleteRecordings(l *Library) error {
|
||||
body := map[string]Library{"data": *l}
|
||||
resp, err := a.post(RecycleUri, "", body, nil)
|
||||
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
|
||||
// was tagged by it's AI in your recording was tagged correctly.
|
||||
func (a *Arlo) SendAnalyticFeedback(r *Recording) error {
|
||||
category := "Person" // Other
|
||||
body := 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")
|
||||
}
|
||||
|
||||
// GetActiveAutomationDefinitions gets the mode metadata (this API replaces the older GetModes(), which still works).
|
||||
func (a *Arlo) GetActiveAutomationDefinitions() error {
|
||||
resp, err := a.get(ActiveAutomationUri, "", nil)
|
||||
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
|
||||
})
|
||||
},
|
||||
*/
|
||||
//func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (libraryMetaData *LibraryMetaData, err error) {
|
||||
// msg := "failed to get library metadata"
|
||||
//
|
||||
// body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
|
||||
// resp, err := a.post(MetadataUri, "", body, nil)
|
||||
// if err != nil {
|
||||
// return nil, errors.WithMessage(err, msg)
|
||||
// }
|
||||
// defer resp.Body.Close()
|
||||
//
|
||||
// response := new(LibraryMetaDataResponse)
|
||||
// if err := resp.Decode(&response); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// if !response.Success {
|
||||
// return nil, errors.New(msg)
|
||||
// }
|
||||
//
|
||||
// return &response.Data, nil
|
||||
//}
|
||||
//
|
||||
//func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (library *Library, err error) {
|
||||
// msg := "failed to get library"
|
||||
//
|
||||
// body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
|
||||
// resp, err := a.post(RecordingsUri, "", body, nil)
|
||||
// if err != nil {
|
||||
// return nil, errors.WithMessage(err, msg)
|
||||
// }
|
||||
// defer resp.Body.Close()
|
||||
//
|
||||
// response := new(LibraryResponse)
|
||||
// if err := resp.Decode(&response); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// if !response.Success {
|
||||
// return nil, errors.New(msg)
|
||||
// }
|
||||
//
|
||||
// return &response.Data, nil
|
||||
//}
|
||||
//
|
||||
///*
|
||||
// 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.
|
||||
//
|
||||
// NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
|
||||
//*/
|
||||
//func (a *Arlo) DeleteRecording(r *Recording) error {
|
||||
// body := map[string]Library{"data": {*r}}
|
||||
// resp, err := a.post(RecycleUri, "", body, nil)
|
||||
// return checkRequest(resp, err, "failed to delete recording")
|
||||
//}
|
||||
//
|
||||
///*
|
||||
// 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.
|
||||
//
|
||||
// NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
|
||||
//*/
|
||||
//func (a *Arlo) BatchDeleteRecordings(l *Library) error {
|
||||
// body := map[string]Library{"data": *l}
|
||||
// resp, err := a.post(RecycleUri, "", body, nil)
|
||||
// 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
|
||||
//// was tagged by it's AI in your recording was tagged correctly.
|
||||
//func (a *Arlo) SendAnalyticFeedback(r *Recording) error {
|
||||
// category := "Person" // Other
|
||||
// body := 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")
|
||||
//}
|
||||
//
|
||||
//// GetActiveAutomationDefinitions gets the mode metadata (this API replaces the older GetModes(), which still works).
|
||||
//func (a *Arlo) GetActiveAutomationDefinitions() error {
|
||||
// resp, err := a.get(ActiveAutomationUri, "", nil)
|
||||
// return checkRequest(resp, err, "failed to get active automation definitions")
|
||||
//}
|
||||
|
@ -84,5 +84,6 @@ type RecordingResponse struct {
|
||||
|
||||
type EventStreamResponse struct {
|
||||
EventStreamPayload
|
||||
Status string `json:"status,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
RawProperties []byte
|
||||
}
|
||||
|
8
types.go
8
types.go
@ -251,14 +251,6 @@ type SirenProperties struct {
|
||||
Pattern string `json:"pattern"`
|
||||
}
|
||||
|
||||
type BasestationModeProperties struct {
|
||||
Active string `json:"active"`
|
||||
}
|
||||
|
||||
type BasestationScheduleProperties struct {
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
type CameraProperties struct {
|
||||
PrivacyActive bool `json:"privacyActive"`
|
||||
Brightness int `json:"brightness,omitempty"`
|
||||
|
111
util.go
111
util.go
@ -25,100 +25,69 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"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 {
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, msg)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
func FloatToHex(x float64) string {
|
||||
var result []byte
|
||||
quotient := int(x)
|
||||
fraction := x - float64(quotient)
|
||||
|
||||
var status Status
|
||||
if err := resp.Decode(&status); err != nil {
|
||||
return err
|
||||
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 status.Success == false {
|
||||
return errors.WithMessage(errors.New(status.Reason), msg)
|
||||
if fraction == 0 {
|
||||
return string(result)
|
||||
}
|
||||
|
||||
return nil
|
||||
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 {
|
||||
|
||||
source := rand.NewSource(time.Now().UnixNano())
|
||||
random := rand.New(source)
|
||||
|
||||
random := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
e := random.Float64() * math.Pow(2, 32)
|
||||
|
||||
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 {
|
||||
msg := fmt.Sprintf("failed to download file (%s)", url)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, msg)
|
||||
return fmt.Errorf("getting %s: %v", url, err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, msg)
|
||||
return fmt.Errorf("copying body to writer: %v", err)
|
||||
}
|
||||
|
||||
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) }
|
||||
|
Loading…
Reference in New Issue
Block a user