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)**
|
|
218
arlo.go
218
arlo.go
@ -17,121 +17,106 @@
|
|||||||
package arlo
|
package arlo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jeffreydwalter/arlo-go/internal/request"
|
"github.com/go-resty/resty/v2"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Arlo struct {
|
type Arlo struct {
|
||||||
user string
|
user string
|
||||||
pass string
|
pass string
|
||||||
client *request.Client
|
client *resty.Client
|
||||||
Account Account
|
Account Account
|
||||||
Basestations Basestations
|
Basestations Basestations
|
||||||
Cameras Cameras
|
Cameras Cameras
|
||||||
rwmutex sync.RWMutex
|
rwmutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newArlo(user string, pass string) (arlo *Arlo) {
|
func NewArlo() (arlo *Arlo) {
|
||||||
|
|
||||||
// Add important headers.
|
c := resty.New().
|
||||||
baseHeaders := make(http.Header)
|
SetHostURL(BaseUrl).
|
||||||
baseHeaders.Add("DNT", "1")
|
SetTimeout(30 * time.Second)
|
||||||
baseHeaders.Add("schemaVersion", "1")
|
|
||||||
baseHeaders.Add("Host", "my.arlo.com")
|
|
||||||
baseHeaders.Add("Referer", "https://my.arlo.com/")
|
|
||||||
|
|
||||||
c, _ := request.NewClient(BaseUrl, baseHeaders)
|
|
||||||
|
|
||||||
return &Arlo{
|
return &Arlo{
|
||||||
user: user,
|
|
||||||
pass: pass,
|
|
||||||
client: c,
|
client: c,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Login(user string, pass string) (arlo *Arlo, err error) {
|
func (a *Arlo) Login(ctx context.Context, user string, pass string) error {
|
||||||
arlo = newArlo(user, pass)
|
|
||||||
|
|
||||||
body := map[string]string{"email": arlo.user, "password": arlo.pass}
|
|
||||||
resp, err := arlo.post(LoginV2Uri, "", body, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithMessage(err, "failed to login")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var loginResponse LoginResponse
|
var loginResponse LoginResponse
|
||||||
if err := resp.Decode(&loginResponse); err != nil {
|
_, err := a.client.R().
|
||||||
return nil, err
|
SetBody(map[string]string{
|
||||||
|
"email": user,
|
||||||
|
"password": pass,
|
||||||
|
}).
|
||||||
|
SetResult(&loginResponse). // or SetResult(AuthSuccess{}).
|
||||||
|
Post(LoginV2Uri)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to login: %v", err)
|
||||||
|
}
|
||||||
|
if !loginResponse.Success {
|
||||||
|
return fmt.Errorf("failed to login")
|
||||||
}
|
}
|
||||||
|
|
||||||
if loginResponse.Success {
|
|
||||||
// Cache the auth token.
|
// Cache the auth token.
|
||||||
arlo.client.AddHeader("Authorization", loginResponse.Data.Token)
|
a.client.SetHeader("Authorization", loginResponse.Data.Token)
|
||||||
|
|
||||||
// Save the account info with the arlo struct.
|
// Save the account info with the arlo struct.
|
||||||
arlo.Account = loginResponse.Data
|
a.Account = loginResponse.Data
|
||||||
|
|
||||||
// Get the devices, which also caches them on the arlo object.
|
// Get the devices, which also caches them on the arlo object.
|
||||||
if _, err := arlo.GetDevices(); err != nil {
|
if _, err := a.GetDevices(ctx); err != nil {
|
||||||
return nil, errors.WithMessage(err, "failed to login")
|
return fmt.Errorf("getting devices: %v", err)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("failed to login")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return arlo, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Arlo) Logout() error {
|
func (a *Arlo) Logout() error {
|
||||||
resp, err := a.put(LogoutUri, "", nil, nil)
|
|
||||||
return checkRequest(resp, err, "failed to logout")
|
var response Status
|
||||||
|
_, err := a.client.R().
|
||||||
|
SetResult(&response).
|
||||||
|
Put(LogoutUri)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("logging out: %v", err)
|
||||||
|
}
|
||||||
|
if response.Success == false {
|
||||||
|
return fmt.Errorf("logging out: %s", response.Reason)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Arlo) CheckSession() (session *Session, err error) {
|
func (a *Arlo) GetSession() (*Session, error) {
|
||||||
msg := "failed to get session"
|
|
||||||
resp, err := a.get(SessionUri, "", nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var response SessionResponse
|
var response SessionResponse
|
||||||
if err := resp.Decode(&response); err != nil {
|
_, err := a.client.R().
|
||||||
return nil, err
|
SetResult(&response).
|
||||||
|
Get(SessionUri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting session: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.Success == false {
|
if response.Success == false {
|
||||||
return nil, errors.WithMessage(errors.New(response.Reason), msg)
|
return nil, fmt.Errorf("getting session: %s", response.Reason)
|
||||||
}
|
}
|
||||||
return &response.Data, nil
|
return &response.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDevices returns an array of all devices.
|
func (a *Arlo) GetDevices(ctx context.Context) (*Devices, error) {
|
||||||
// When you call Login, this method is called and all devices are cached in the arlo object.
|
|
||||||
func (a *Arlo) GetDevices() (devices *Devices, err error) {
|
|
||||||
resp, err := a.get(fmt.Sprintf(DevicesUri, time.Now().Format("20060102")), "", nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithMessage(err, "failed to get devices")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var response DeviceResponse
|
var response DeviceResponse
|
||||||
if err := resp.Decode(&response); err != nil {
|
_, err := a.client.R().
|
||||||
return nil, err
|
SetResult(&response).
|
||||||
|
Get(fmt.Sprintf(DevicesUri, time.Now().Format("20060102")))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting devices: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !response.Success {
|
if !response.Success {
|
||||||
return nil, errors.New("failed to get devices")
|
return nil, fmt.Errorf("failed to get devices")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(response.Data) == 0 {
|
if len(response.Data) == 0 {
|
||||||
return nil, errors.New("no devices found")
|
return nil, fmt.Errorf("no device found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache a pointer to the arlo object with each device.
|
// Cache a pointer to the arlo object with each device.
|
||||||
@ -140,74 +125,69 @@ func (a *Arlo) GetDevices() (devices *Devices, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect all of the basestations from the EventStream.
|
// Disconnect all of the basestations from the EventStream.
|
||||||
for i := range a.Basestations {
|
for _, basestation := range a.Basestations {
|
||||||
if err := a.Basestations[i].Disconnect(); err != nil {
|
if err := basestation.Disconnect(); err != nil {
|
||||||
return nil, errors.WithMessage(err, "failed to get devices")
|
return nil, fmt.Errorf("disconnecting device %s: %v", basestation.DeviceName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.rwmutex.Lock()
|
a.rwmutex.Lock()
|
||||||
// Cache the devices as their respective types.
|
// Cache the devices as their respective types.
|
||||||
a.Cameras = *response.Data.GetCameras()
|
a.Cameras = response.Data.GetCameras()
|
||||||
a.Basestations = *response.Data.GetBasestations()
|
a.Basestations = response.Data.GetBasestations()
|
||||||
a.rwmutex.Unlock()
|
a.rwmutex.Unlock()
|
||||||
|
|
||||||
// subscribe each basestation to the EventStream.
|
// subscribe each basestation to the EventStream.
|
||||||
for i := range a.Basestations {
|
for _, basestation := range a.Basestations {
|
||||||
if err := a.Basestations[i].Subscribe(); err != nil {
|
if err := basestation.Subscribe(ctx); err != nil {
|
||||||
return nil, errors.WithMessage(err, "failed to get devices")
|
return nil, fmt.Errorf("subscribing device %s: %v", basestation.DeviceName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &response.Data, nil
|
return &response.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProfile returns the user profile for the currently logged in user.
|
// GetProfile returns the user profile for the currently logged in user.
|
||||||
func (a *Arlo) GetProfile() (profile *UserProfile, err error) {
|
func (a *Arlo) GetProfile() (*UserProfile, error) {
|
||||||
resp, err := a.get(ProfileUri, "", nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithMessage(err, "failed to get user profile")
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var response UserProfileResponse
|
var response UserProfileResponse
|
||||||
if err := resp.Decode(&response); err != nil {
|
_, err := a.client.R().
|
||||||
return nil, err
|
SetResult(&response).
|
||||||
}
|
Get(ProfileUri)
|
||||||
|
|
||||||
if !response.Success {
|
if err != nil {
|
||||||
return nil, errors.New("failed to get user profile")
|
return nil, fmt.Errorf("getting user profile: %v", err)
|
||||||
|
}
|
||||||
|
if response.Success == false {
|
||||||
|
return nil, fmt.Errorf("getting user profile: %s", response.Reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &response.Data, nil
|
return &response.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given.
|
//// UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given.
|
||||||
func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) error {
|
//func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) error {
|
||||||
resp, err := a.post(CameraOrderUri, "", d, nil)
|
// resp, err := a.post(CameraOrderUri, "", d, nil)
|
||||||
return checkRequest(resp, err, "failed to display order")
|
// return checkRequest(resp, err, "failed to display order")
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
// UpdateProfile takes a first and last name, and updates the user profile with that information.
|
//// UpdateProfile takes a first and last name, and updates the user profile with that information.
|
||||||
func (a *Arlo) UpdateProfile(firstName, lastName string) error {
|
//func (a *Arlo) UpdateProfile(firstName, lastName string) error {
|
||||||
body := map[string]string{"firstName": firstName, "lastName": lastName}
|
// body := map[string]string{"firstName": firstName, "lastName": lastName}
|
||||||
resp, err := a.put(ProfileUri, "", body, nil)
|
// resp, err := a.put(ProfileUri, "", body, nil)
|
||||||
return checkRequest(resp, err, "failed to update profile")
|
// return checkRequest(resp, err, "failed to update profile")
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
func (a *Arlo) UpdatePassword(pass string) error {
|
//func (a *Arlo) UpdatePassword(pass string) error {
|
||||||
body := map[string]string{"currentPassword": a.pass, "newPassword": pass}
|
// body := map[string]string{"currentPassword": a.pass, "newPassword": pass}
|
||||||
resp, err := a.post(UpdatePasswordUri, "", body, nil)
|
// resp, err := a.post(UpdatePasswordUri, "", body, nil)
|
||||||
if err := checkRequest(resp, err, "failed to update password"); err != nil {
|
// if err := checkRequest(resp, err, "failed to update password"); err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
a.pass = pass
|
// a.pass = pass
|
||||||
|
//
|
||||||
return nil
|
// return nil
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
func (a *Arlo) UpdateFriends(f Friend) error {
|
//func (a *Arlo) UpdateFriends(f Friend) error {
|
||||||
resp, err := a.put(FriendsUri, "", f, nil)
|
// resp, err := a.put(FriendsUri, "", f, nil)
|
||||||
return checkRequest(resp, err, "failed to update friends")
|
// return checkRequest(resp, err, "failed to update friends")
|
||||||
}
|
//}
|
||||||
|
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)
|
|
||||||
}
|
|
351
basestation.go
351
basestation.go
@ -17,14 +17,16 @@
|
|||||||
package arlo
|
package arlo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const eventStreamTimeout = 10 * time.Second
|
const eventStreamTimeout = 30 * time.Second
|
||||||
const pingTime = 30 * time.Second
|
const pingTime = 30 * time.Second
|
||||||
|
|
||||||
// A Basestation is a Device that's not type "camera" (basestation, arloq, arloqs, etc.).
|
// A Basestation is a Device that's not type "camera" (basestation, arloq, arloqs, etc.).
|
||||||
@ -34,41 +36,108 @@ type Basestation struct {
|
|||||||
eventStream *eventStream
|
eventStream *eventStream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BaseStationState struct {
|
||||||
|
InterfaceVersion int `json:"interfaceVersion"`
|
||||||
|
APIVersion int `json:"apiVersion"`
|
||||||
|
State string `json:"state"`
|
||||||
|
SwVersion string `json:"swVersion"`
|
||||||
|
HwVersion string `json:"hwVersion"`
|
||||||
|
ModelID string `json:"modelId"`
|
||||||
|
Capabilities []string `json:"capabilities"`
|
||||||
|
McsEnabled bool `json:"mcsEnabled"`
|
||||||
|
AutoUpdateEnabled bool `json:"autoUpdateEnabled"`
|
||||||
|
UpdateAvailable interface{} `json:"updateAvailable"`
|
||||||
|
TimeZone string `json:"timeZone"`
|
||||||
|
OlsonTimeZone string `json:"olsonTimeZone"`
|
||||||
|
UploadBandwidthSaturated bool `json:"uploadBandwidthSaturated"`
|
||||||
|
AntiFlicker struct {
|
||||||
|
Mode int `json:"mode"`
|
||||||
|
AutoDefault int `json:"autoDefault"`
|
||||||
|
} `json:"antiFlicker"`
|
||||||
|
LowBatteryAlert struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
} `json:"lowBatteryAlert"`
|
||||||
|
LowSignalAlert struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
} `json:"lowSignalAlert"`
|
||||||
|
Claimed bool `json:"claimed"`
|
||||||
|
TimeSyncState string `json:"timeSyncState"`
|
||||||
|
Connectivity []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Connected bool `json:"connected"`
|
||||||
|
} `json:"connectivity"`
|
||||||
|
Groups []interface{} `json:"groups"`
|
||||||
|
LocalCert struct {
|
||||||
|
OwnCert string `json:"ownCert"`
|
||||||
|
PeerCerts []string `json:"peerCerts"`
|
||||||
|
} `json:"localCert"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetModesResponse struct {
|
||||||
|
Active string `json:"active"`
|
||||||
|
Modes []*Mode `json:"modes"`
|
||||||
|
}
|
||||||
|
type Mode struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
RulesIds []string `json:"rules"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetRulesResponse struct {
|
||||||
|
Rules []Rule `json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rule struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Protected bool `json:"protected"`
|
||||||
|
Triggers []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
DeviceID string `json:"deviceId"`
|
||||||
|
Sensitivity int `json:"sensitivity"`
|
||||||
|
} `json:"triggers"`
|
||||||
|
Actions []struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Recipients []string `json:"recipients,omitempty"`
|
||||||
|
DeviceID string `json:"deviceId,omitempty"`
|
||||||
|
StopCondition struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
DeviceID string `json:"deviceId"`
|
||||||
|
} `json:"stopCondition,omitempty"`
|
||||||
|
} `json:"actions"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
// Basestations is a slice of Basestation objects.
|
// Basestations is a slice of Basestation objects.
|
||||||
type Basestations []Basestation
|
type Basestations []*Basestation
|
||||||
|
|
||||||
// Find returns a basestation with the device id passed in.
|
// Find returns a basestation with the device id passed in.
|
||||||
func (bs *Basestations) Find(deviceId string) *Basestation {
|
func (bs *Basestations) Find(deviceId string) *Basestation {
|
||||||
for _, b := range *bs {
|
for _, b := range *bs {
|
||||||
if b.DeviceId == deviceId {
|
if b.DeviceId == deviceId {
|
||||||
return &b
|
return b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeEventStreamRequest is a helper function sets up a response channel, sends a message to the event stream, and blocks waiting for the response.
|
// makeEventStreamRequest is a helper function sets up a response channel, sends a message to the event stream, and blocks waiting for the response.
|
||||||
func (b *Basestation) makeEventStreamRequest(payload EventStreamPayload, msg string) (response *EventStreamResponse, err error) {
|
func (b *Basestation) makeEventStreamRequest(payload EventStreamPayload) (*EventStreamResponse, error) {
|
||||||
transId := genTransId()
|
transId := genTransId()
|
||||||
payload.TransId = transId
|
payload.TransId = transId
|
||||||
|
|
||||||
if err := b.IsConnected(); err != nil {
|
if err := b.IsConnected(); err != nil {
|
||||||
//if err := b.Subscribe(); err != nil {
|
return nil, fmt.Errorf("event stream not connected")
|
||||||
return nil, errors.WithMessage(errors.WithMessage(err, msg), "failed to reconnect to event stream")
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriber := make(subscriber)
|
responseChan := make(chan *EventStreamResponse)
|
||||||
|
errorChan := make(chan error)
|
||||||
// Add the response channel to the event stream queue so the response can be written to it.
|
b.eventStream.subscribe(transId, responseChan, errorChan)
|
||||||
b.eventStream.subscribe(transId, subscriber)
|
|
||||||
// Make sure we close and remove the response channel before returning.
|
|
||||||
defer b.eventStream.unsubscribe(transId)
|
defer b.eventStream.unsubscribe(transId)
|
||||||
|
|
||||||
// Send the payload to the event stream.
|
// Send the payload to the event stream.
|
||||||
if err := b.NotifyEventStream(payload, msg); err != nil {
|
if err := b.NotifyEventStream(payload); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("notifying event stream: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
timer := time.NewTimer(eventStreamTimeout)
|
timer := time.NewTimer(eventStreamTimeout)
|
||||||
@ -77,72 +146,89 @@ func (b *Basestation) makeEventStreamRequest(payload EventStreamPayload, msg str
|
|||||||
// Wait for the response to come back from the event stream on the response channel.
|
// Wait for the response to come back from the event stream on the response channel.
|
||||||
select {
|
select {
|
||||||
// If we get a response, return it to the caller.
|
// If we get a response, return it to the caller.
|
||||||
case response := <-subscriber:
|
case response := <-responseChan:
|
||||||
return response, nil
|
return response, nil
|
||||||
case err = <-b.eventStream.Error:
|
case err := <-b.eventStream.Error:
|
||||||
return nil, errors.Wrap(err, msg)
|
return nil, fmt.Errorf("event stream error: %v", err)
|
||||||
// If the event stream is closed, return an error about it.
|
// If the event stream is closed, return an error about it.
|
||||||
case <-b.eventStream.Disconnected:
|
case <-b.eventStream.DisconnectedChan:
|
||||||
err = errors.New("event stream was closed before response was read")
|
return nil, fmt.Errorf("event stream was closed before response was read")
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
// If we timeout, return an error about it.
|
// If we timeout, return an error about it.
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
err = fmt.Errorf("event stream response timed out after %.0f second", eventStreamTimeout.Seconds())
|
return nil, fmt.Errorf("event stream response timed out after %.0f second", eventStreamTimeout.Seconds())
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) IsConnected() error {
|
func (b *Basestation) IsConnected() error {
|
||||||
// If the event stream is closed, return an error about it.
|
// If the event stream is closed, return an error about it.
|
||||||
select {
|
select {
|
||||||
case <-b.eventStream.Disconnected:
|
case <-b.eventStream.DisconnectedChan:
|
||||||
return errors.New("basestation not connected to event stream")
|
return fmt.Errorf("basestation not connected to event stream")
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) Subscribe() error {
|
func (b *Basestation) Subscribe(ctx context.Context) error {
|
||||||
b.eventStream = newEventStream(BaseUrl+fmt.Sprintf(NotifyResponsesPushServiceUri, b.arlo.Account.Token), &http.Client{Jar: b.arlo.client.HttpClient.Jar})
|
b.eventStream = newEventStream(BaseUrl+fmt.Sprintf(NotifyResponsesPushServiceUri, b.arlo.Account.Token), &http.Client{Jar: b.arlo.client.GetClient().Jar})
|
||||||
|
|
||||||
|
connectedChan, err := b.eventStream.listen(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting up event stream: %v", err)
|
||||||
|
}
|
||||||
forLoop:
|
forLoop:
|
||||||
for {
|
for {
|
||||||
// We blocking here because we can't really do anything with the event stream until we're connected.
|
|
||||||
// Once we have confirmation that we're connected to the event stream, we will "subscribe" to events.
|
|
||||||
select {
|
select {
|
||||||
case connected := <-b.eventStream.listen():
|
case <-ctx.Done():
|
||||||
if connected {
|
return fmt.Errorf("failed to subscribe to the event stream: requesting shutdown")
|
||||||
break forLoop
|
case connected := <-connectedChan:
|
||||||
} else {
|
if !connected {
|
||||||
return errors.New("failed to subscribe to the event stream")
|
return fmt.Errorf("failed to subscribe to the event stream")
|
||||||
}
|
}
|
||||||
case <-b.eventStream.Disconnected:
|
break forLoop
|
||||||
err := errors.New("event stream was closed")
|
case <-b.eventStream.DisconnectedChan:
|
||||||
return errors.WithMessage(err, "failed to subscribe to the event stream")
|
return fmt.Errorf("failed to subscribe to the event stream: event stream was closed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := b.Ping(); err != nil {
|
if err := b.Ping(); err != nil {
|
||||||
return errors.WithMessage(err, "failed to subscribe to the event stream")
|
_ = b.Disconnect()
|
||||||
|
return fmt.Errorf("Pingloop > error while pinging: %v > disconnect event stream", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Arlo event stream requires a "ping" every 30s.
|
// The Arlo event stream requires a "ping" every 30s.
|
||||||
go func() {
|
go func(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(pingTime)
|
||||||
for {
|
for {
|
||||||
time.Sleep(pingTime)
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case _ = <-ticker.C:
|
||||||
if err := b.Ping(); err != nil {
|
if err := b.Ping(); err != nil {
|
||||||
b.Disconnect()
|
log.Errorf("Pingloop > error while pinging: %v > disconnect event stream", err)
|
||||||
break
|
_ = b.Disconnect()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) Unsubscribe() error {
|
func (b *Basestation) Unsubscribe() error {
|
||||||
resp, err := b.arlo.get(UnsubscribeUri, b.XCloudId, nil)
|
var response Status
|
||||||
return checkRequest(resp, err, "failed to unsubscribe from event stream")
|
_, err := b.arlo.client.R().
|
||||||
|
SetResult(&response).
|
||||||
|
SetHeader("xcloudId", b.XCloudId).
|
||||||
|
Put(UnsubscribeUri)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unsubscribing from event stream: %v", err)
|
||||||
|
}
|
||||||
|
if response.Success == false {
|
||||||
|
return fmt.Errorf("unsubscribing from event stream: %s", response.Reason)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) Disconnect() error {
|
func (b *Basestation) Disconnect() error {
|
||||||
@ -164,57 +250,75 @@ func (b *Basestation) Ping() error {
|
|||||||
To: b.DeviceId,
|
To: b.DeviceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := b.makeEventStreamRequest(payload, "failed to ping the event stream"); err != nil {
|
if _, err := b.makeEventStreamRequest(payload); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) NotifyEventStream(payload EventStreamPayload, msg string) error {
|
func (b *Basestation) NotifyEventStream(payload EventStreamPayload) error {
|
||||||
resp, err := b.arlo.post(fmt.Sprintf(NotifyUri, b.DeviceId), b.XCloudId, payload, nil)
|
var response Status
|
||||||
if err := checkRequest(resp, err, msg); err != nil {
|
_, err := b.arlo.client.R().
|
||||||
return errors.WithMessage(err, "failed to notify event stream")
|
SetBody(payload).
|
||||||
|
SetResult(&response).
|
||||||
|
SetHeader("xcloudId", b.XCloudId).
|
||||||
|
Post(fmt.Sprintf(NotifyUri, b.DeviceId))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("notifying event stream: %v", err)
|
||||||
|
}
|
||||||
|
if response.Success == false {
|
||||||
|
return fmt.Errorf("notifying event stream: %s", response.Reason)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) GetState() (response *EventStreamResponse, err error) {
|
func (b *Basestation) makeRequest(action string, resource string, publishResponse bool, properties interface{}, result interface{}) error {
|
||||||
|
|
||||||
payload := EventStreamPayload{
|
payload := EventStreamPayload{
|
||||||
Action: "get",
|
Action: action,
|
||||||
Resource: "basestation",
|
Resource: resource,
|
||||||
PublishResponse: false,
|
PublishResponse: publishResponse,
|
||||||
|
Properties: properties,
|
||||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
||||||
To: b.DeviceId,
|
To: b.DeviceId,
|
||||||
}
|
}
|
||||||
|
resp, err := b.makeEventStreamRequest(payload)
|
||||||
return b.makeEventStreamRequest(payload, "failed to get basestation state")
|
if err != nil {
|
||||||
|
return fmt.Errorf("making event stream request: %v", err)
|
||||||
|
}
|
||||||
|
if result != nil {
|
||||||
|
err = json.Unmarshal(resp.RawProperties, result)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshalling properties: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) GetAssociatedCamerasState() (response *EventStreamResponse, err error) {
|
func (b *Basestation) GetState() (*BaseStationState, error) {
|
||||||
payload := EventStreamPayload{
|
var state BaseStationState
|
||||||
Action: "get",
|
err := b.makeRequest("get", "basestation", false, nil, &state)
|
||||||
Resource: "cameras",
|
if err != nil {
|
||||||
PublishResponse: false,
|
return nil, fmt.Errorf("getting basestation %s state: %v", b.DeviceName, err)
|
||||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
|
||||||
To: b.DeviceId,
|
|
||||||
}
|
}
|
||||||
|
return &state, nil
|
||||||
return b.makeEventStreamRequest(payload, "failed to get associated cameras state")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) GetRules() (response *EventStreamResponse, err error) {
|
func (b *Basestation) GetAllCameraState() ([]CameraState, error) {
|
||||||
payload := EventStreamPayload{
|
var states []CameraState
|
||||||
Action: "get",
|
err := b.makeRequest("get", "cameras", false, nil, &states)
|
||||||
Resource: "rules",
|
if err != nil {
|
||||||
PublishResponse: false,
|
return nil, fmt.Errorf("getting associated cameras state: %v", err)
|
||||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
|
||||||
To: b.DeviceId,
|
|
||||||
}
|
}
|
||||||
|
return states, nil
|
||||||
|
}
|
||||||
|
|
||||||
return b.makeEventStreamRequest(payload, "failed to get rules")
|
func (b *Basestation) GetRules() ([]Rule, error) {
|
||||||
|
var resp GetRulesResponse
|
||||||
|
err := b.makeRequest("get", "rules", false, nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting rules: %v", err)
|
||||||
|
}
|
||||||
|
return resp.Rules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) GetCalendarMode() (response *EventStreamResponse, err error) {
|
func (b *Basestation) GetCalendarMode() (response *EventStreamResponse, err error) {
|
||||||
@ -226,52 +330,59 @@ func (b *Basestation) GetCalendarMode() (response *EventStreamResponse, err erro
|
|||||||
To: b.DeviceId,
|
To: b.DeviceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.makeEventStreamRequest(payload, "failed to get schedule")
|
return b.makeEventStreamRequest(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCalendarMode toggles calendar mode.
|
// SetCalendarMode toggles calendar mode.
|
||||||
// NOTE: The Arlo API seems to disable calendar mode when switching to other modes, if it's enabled.
|
// NOTE: The Arlo API seems to disable calendar mode when switching to other modes, if it's enabled.
|
||||||
// You should probably do the same, although, the UI reflects the switch from calendar mode to say armed mode without explicitly setting calendar mode to inactive.
|
// You should probably do the same, although, the UI reflects the switch from calendar mode to say armed mode without explicitly setting calendar mode to inactive.
|
||||||
func (b *Basestation) SetCalendarMode(active bool) (response *EventStreamResponse, err error) {
|
func (b *Basestation) SetCalendarMode(active bool) error {
|
||||||
payload := EventStreamPayload{
|
resp := make(map[string]bool)
|
||||||
Action: "set",
|
err := b.makeRequest("set", "schedule", true, struct {
|
||||||
Resource: "schedule",
|
Active bool `json:"active"`
|
||||||
PublishResponse: true,
|
}{
|
||||||
Properties: BasestationScheduleProperties{
|
|
||||||
Active: active,
|
Active: active,
|
||||||
},
|
}, &resp)
|
||||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
if err != nil {
|
||||||
To: b.DeviceId,
|
return fmt.Errorf("setting calendar mode %t: %v", active, err)
|
||||||
}
|
}
|
||||||
|
activemode, ok := resp["active"]
|
||||||
return b.makeEventStreamRequest(payload, "failed to set schedule")
|
if !ok {
|
||||||
|
return fmt.Errorf("active mode not present in response")
|
||||||
|
}
|
||||||
|
if activemode != active {
|
||||||
|
return fmt.Errorf("active mode is not the mode requested: requested %t, set %t", active, activemode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) GetModes() (response *EventStreamResponse, err error) {
|
func (b *Basestation) GetModes() (*GetModesResponse, error) {
|
||||||
payload := EventStreamPayload{
|
var resp GetModesResponse
|
||||||
Action: "get",
|
err := b.makeRequest("get", "modes", false, nil, &resp)
|
||||||
Resource: "modes",
|
if err != nil {
|
||||||
PublishResponse: false,
|
return nil, fmt.Errorf("getting modes: %v", err)
|
||||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
|
||||||
To: b.DeviceId,
|
|
||||||
}
|
}
|
||||||
|
return &resp, nil
|
||||||
return b.makeEventStreamRequest(payload, "failed to get modes")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) SetCustomMode(mode string) (response *EventStreamResponse, err error) {
|
func (b *Basestation) SetCustomMode(mode string) error {
|
||||||
payload := EventStreamPayload{
|
resp := make(map[string]string)
|
||||||
Action: "set",
|
err := b.makeRequest("set", "modes", true, struct {
|
||||||
Resource: "modes",
|
Active string `json:"active"`
|
||||||
PublishResponse: true,
|
}{
|
||||||
Properties: BasestationModeProperties{
|
|
||||||
Active: mode,
|
Active: mode,
|
||||||
},
|
}, &resp)
|
||||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
if err != nil {
|
||||||
To: b.DeviceId,
|
return fmt.Errorf("setting custom mode %s: %v", mode, err)
|
||||||
}
|
}
|
||||||
|
activemode, ok := resp["active"]
|
||||||
return b.makeEventStreamRequest(payload, "failed to set mode")
|
if !ok {
|
||||||
|
return fmt.Errorf("active mode not present in response")
|
||||||
|
}
|
||||||
|
if activemode != mode {
|
||||||
|
return fmt.Errorf("active mode is not the mode requested: requested %s, set %s", mode, activemode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) DeleteMode(mode string) (response *EventStreamResponse, err error) {
|
func (b *Basestation) DeleteMode(mode string) (response *EventStreamResponse, err error) {
|
||||||
@ -283,15 +394,23 @@ func (b *Basestation) DeleteMode(mode string) (response *EventStreamResponse, er
|
|||||||
To: b.DeviceId,
|
To: b.DeviceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.makeEventStreamRequest(payload, "failed to set mode")
|
return b.makeEventStreamRequest(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) Arm() (response *EventStreamResponse, err error) {
|
func (b *Basestation) Arm() error {
|
||||||
return b.SetCustomMode("mode1")
|
err := b.SetCustomMode("mode1")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("arming (mode1): %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) Disarm() (response *EventStreamResponse, err error) {
|
func (b *Basestation) Disarm() error {
|
||||||
return b.SetCustomMode("mode0")
|
err := b.SetCustomMode("mode0")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("disarming (mode0): %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) SirenOn() (response *EventStreamResponse, err error) {
|
func (b *Basestation) SirenOn() (response *EventStreamResponse, err error) {
|
||||||
@ -309,7 +428,7 @@ func (b *Basestation) SirenOn() (response *EventStreamResponse, err error) {
|
|||||||
To: b.DeviceId,
|
To: b.DeviceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.makeEventStreamRequest(payload, "failed to get modes")
|
return b.makeEventStreamRequest(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) SirenOff() (response *EventStreamResponse, err error) {
|
func (b *Basestation) SirenOff() (response *EventStreamResponse, err error) {
|
||||||
@ -327,5 +446,5 @@ func (b *Basestation) SirenOff() (response *EventStreamResponse, err error) {
|
|||||||
To: b.DeviceId,
|
To: b.DeviceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.makeEventStreamRequest(payload, "failed to get modes")
|
return b.makeEventStreamRequest(payload)
|
||||||
}
|
}
|
||||||
|
478
camera.go
478
camera.go
@ -18,24 +18,88 @@ package arlo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// A Camera is a Device of type "camera".
|
// A Camera is a Device of type "camera".
|
||||||
// This type is here just for semantics. Some methods explicitly require a device of a certain type.
|
// This type is here just for semantics. Some methods explicitly require a device of a certain type.
|
||||||
type Camera Device
|
type Camera Device
|
||||||
|
|
||||||
|
type CameraState struct {
|
||||||
|
InterfaceVersion int `json:"interfaceVersion"`
|
||||||
|
SerialNumber string `json:"serialNumber"`
|
||||||
|
BatteryLevel int `json:"batteryLevel"`
|
||||||
|
BatteryTech string `json:"batteryTech"`
|
||||||
|
ChargerTech string `json:"chargerTech"`
|
||||||
|
ChargingState string `json:"chargingState"`
|
||||||
|
ChargeOnly bool `json:"chargeOnly"`
|
||||||
|
ChargeNotificationLedEnable bool `json:"chargeNotificationLedEnable"`
|
||||||
|
AudioMicAGC int `json:"audioMicAGC"`
|
||||||
|
SignalStrength int `json:"signalStrength"`
|
||||||
|
Brightness int `json:"brightness"`
|
||||||
|
Mirror bool `json:"mirror"`
|
||||||
|
Flip bool `json:"flip"`
|
||||||
|
PowerSaveMode int `json:"powerSaveMode"`
|
||||||
|
Zoom struct {
|
||||||
|
Topleftx int `json:"topleftx"`
|
||||||
|
Toplefty int `json:"toplefty"`
|
||||||
|
Bottomrightx int `json:"bottomrightx"`
|
||||||
|
Bottomrighty int `json:"bottomrighty"`
|
||||||
|
} `json:"zoom"`
|
||||||
|
Mic struct {
|
||||||
|
Mute bool `json:"mute"`
|
||||||
|
Volume int `json:"volume"`
|
||||||
|
} `json:"mic"`
|
||||||
|
Speaker struct {
|
||||||
|
Mute bool `json:"mute"`
|
||||||
|
Volume int `json:"volume"`
|
||||||
|
} `json:"speaker"`
|
||||||
|
StreamingMode string `json:"streamingMode"`
|
||||||
|
ContinuousStreamState string `json:"continuousStreamState"`
|
||||||
|
Motion struct {
|
||||||
|
Sensitivity int `json:"sensitivity"`
|
||||||
|
Zones []interface{} `json:"zones"`
|
||||||
|
} `json:"motion"`
|
||||||
|
Resolution struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
} `json:"resolution"`
|
||||||
|
IdleLedEnable bool `json:"idleLedEnable"`
|
||||||
|
PrivacyActive bool `json:"privacyActive"`
|
||||||
|
StandbyActive bool `json:"standbyActive"`
|
||||||
|
SetupActive bool `json:"setupActive"`
|
||||||
|
ConnectionState string `json:"connectionState"`
|
||||||
|
ActivityState string `json:"activityState"`
|
||||||
|
SwVersion string `json:"swVersion"`
|
||||||
|
HwVersion string `json:"hwVersion"`
|
||||||
|
ModelID string `json:"modelId"`
|
||||||
|
MotionSetupModeEnabled bool `json:"motionSetupModeEnabled"`
|
||||||
|
MotionSetupModeSensitivity int `json:"motionSetupModeSensitivity"`
|
||||||
|
MotionDetected bool `json:"motionDetected"`
|
||||||
|
AudioDetected bool `json:"audioDetected"`
|
||||||
|
HasStreamed bool `json:"hasStreamed"`
|
||||||
|
LocalRecordingActive bool `json:"localRecordingActive"`
|
||||||
|
OlsonTimeZone string `json:"olsonTimeZone"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
NightVisionMode int `json:"nightVisionMode"`
|
||||||
|
VideoMode string `json:"videoMode"`
|
||||||
|
Hdr string `json:"hdr"`
|
||||||
|
UpdateAvailable interface{} `json:"updateAvailable"`
|
||||||
|
BlockNotifications struct {
|
||||||
|
Block bool `json:"block"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
EndTime int `json:"endTime"`
|
||||||
|
} `json:"blockNotifications"`
|
||||||
|
BestLocalLiveStreaming string `json:"bestLocalLiveStreaming"`
|
||||||
|
}
|
||||||
|
|
||||||
// Cameras is a slice of Camera objects.
|
// Cameras is a slice of Camera objects.
|
||||||
type Cameras []Camera
|
type Cameras []*Camera
|
||||||
|
|
||||||
// Find returns a camera with the device id passed in.
|
// Find returns a camera with the device id passed in.
|
||||||
func (cs *Cameras) Find(deviceId string) *Camera {
|
func (cs *Cameras) Find(deviceId string) *Camera {
|
||||||
for _, c := range *cs {
|
for _, c := range *cs {
|
||||||
if c.DeviceId == deviceId {
|
if c.DeviceId == deviceId {
|
||||||
return &c
|
return c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,14 +119,11 @@ func (c *Camera) On() (response *EventStreamResponse, err error) {
|
|||||||
To: c.ParentId,
|
To: c.ParentId,
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := "failed to turn camera on"
|
|
||||||
|
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
b := c.arlo.Basestations.Find(c.ParentId)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
}
|
}
|
||||||
return b.makeEventStreamRequest(payload, msg)
|
return b.makeEventStreamRequest(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// On turns a camera off; meaning it won't detect and record events.
|
// On turns a camera off; meaning it won't detect and record events.
|
||||||
@ -78,14 +139,11 @@ func (c *Camera) Off() (response *EventStreamResponse, err error) {
|
|||||||
To: c.ParentId,
|
To: c.ParentId,
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := "failed to turn camera off"
|
|
||||||
|
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
b := c.arlo.Basestations.Find(c.ParentId)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
}
|
}
|
||||||
return b.makeEventStreamRequest(payload, msg)
|
return b.makeEventStreamRequest(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBrightness sets the camera brightness.
|
// SetBrightness sets the camera brightness.
|
||||||
@ -109,15 +167,11 @@ func (c *Camera) SetBrightness(brightness int) (response *EventStreamResponse, e
|
|||||||
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
||||||
To: c.ParentId,
|
To: c.ParentId,
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := "failed to set camera brightness"
|
|
||||||
|
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
b := c.arlo.Basestations.Find(c.ParentId)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
}
|
}
|
||||||
return b.makeEventStreamRequest(payload, msg)
|
return b.makeEventStreamRequest(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Camera) EnableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) {
|
func (c *Camera) EnableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) {
|
||||||
@ -135,15 +189,11 @@ func (c *Camera) EnableMotionAlerts(sensitivity int, zones []string) (response *
|
|||||||
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
||||||
To: c.ParentId,
|
To: c.ParentId,
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := "failed to enable motion alerts"
|
|
||||||
|
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
b := c.arlo.Basestations.Find(c.ParentId)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
}
|
}
|
||||||
return b.makeEventStreamRequest(payload, msg)
|
return b.makeEventStreamRequest(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Camera) DisableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) {
|
func (c *Camera) DisableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) {
|
||||||
@ -162,14 +212,11 @@ func (c *Camera) DisableMotionAlerts(sensitivity int, zones []string) (response
|
|||||||
To: c.ParentId,
|
To: c.ParentId,
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := "failed to enable motion alerts"
|
|
||||||
|
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
b := c.arlo.Basestations.Find(c.ParentId)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
}
|
}
|
||||||
return b.makeEventStreamRequest(payload, msg)
|
return b.makeEventStreamRequest(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Camera) EnableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) {
|
func (c *Camera) EnableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) {
|
||||||
@ -187,14 +234,11 @@ func (c *Camera) EnableAudioAlerts(sensitivity int) (response *EventStreamRespon
|
|||||||
To: c.ParentId,
|
To: c.ParentId,
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := "failed to enable audio alerts"
|
|
||||||
|
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
b := c.arlo.Basestations.Find(c.ParentId)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
}
|
}
|
||||||
return b.makeEventStreamRequest(payload, msg)
|
return b.makeEventStreamRequest(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Camera) DisableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) {
|
func (c *Camera) DisableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) {
|
||||||
@ -212,192 +256,11 @@ func (c *Camera) DisableAudioAlerts(sensitivity int) (response *EventStreamRespo
|
|||||||
To: c.ParentId,
|
To: c.ParentId,
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := "failed to disable audio alerts"
|
|
||||||
|
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
b := c.arlo.Basestations.Find(c.ParentId)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
}
|
}
|
||||||
return b.makeEventStreamRequest(payload, msg)
|
return b.makeEventStreamRequest(payload)
|
||||||
}
|
|
||||||
|
|
||||||
// PushToTalk starts a push-to-talk session.
|
|
||||||
// FIXME: This feature requires more API calls to make it actually work, and I haven't figure out how to fully implement it.
|
|
||||||
// It appears that the audio stream is Real-Time Transport Protocol (RTP), which requires a player (ffmpeg?) to consume the stream.
|
|
||||||
func (c *Camera) PushToTalk() error {
|
|
||||||
/*
|
|
||||||
processResponse: function(e) {
|
|
||||||
if (g.pc)
|
|
||||||
if (e.properties && "answerSdp" == e.properties.type) {
|
|
||||||
var t = e.properties.data
|
|
||||||
, i = {
|
|
||||||
type: "answer",
|
|
||||||
sdp: t
|
|
||||||
};
|
|
||||||
r.debug(i),
|
|
||||||
g.pc.setRemoteDescription(new g.SessionDescription(i), u, d)
|
|
||||||
} else if (e.properties && "answerCandidate" == e.properties.type)
|
|
||||||
if (g.candidateCache)
|
|
||||||
g.candidateCache.push(e.properties.data);
|
|
||||||
else {
|
|
||||||
var n = e.properties.data
|
|
||||||
, a = window.mozRTCIceCandidate || window.RTCIceCandidate
|
|
||||||
, o = new a({
|
|
||||||
candidate: n,
|
|
||||||
sdpMLineIndex: 0
|
|
||||||
});
|
|
||||||
r.debug(o),
|
|
||||||
g.pc.addIceCandidate(o)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
startConnection: function(t) {
|
|
||||||
g.loading = !0,
|
|
||||||
g.error = !1,
|
|
||||||
g.candidateCache = [];
|
|
||||||
var i = t.deviceId
|
|
||||||
, o = t.parentId
|
|
||||||
, u = t.uniqueId;
|
|
||||||
g.device = t;
|
|
||||||
var p = {
|
|
||||||
method: "GET",
|
|
||||||
url: l.getPttUrl(u),
|
|
||||||
data: "",
|
|
||||||
headers: {
|
|
||||||
Authorization: s.ssoToken,
|
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
|
||||||
"Data-Type": "json"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
r.debug("getting ptt data: " + JSON.stringify(p));
|
|
||||||
n(p).then(function(u) {
|
|
||||||
if (!u.data.success)
|
|
||||||
return e.$broadcast("show_error", u.data),
|
|
||||||
void (g.error = u.data.data.message || !0);
|
|
||||||
var m = u.data.data.data;
|
|
||||||
g.uSessionId = u.data.data.uSessionId,
|
|
||||||
_.each(m, function(e) {
|
|
||||||
e.url && (e.urls = e.url,
|
|
||||||
delete e.url)
|
|
||||||
});
|
|
||||||
var f = new g.PeerConnection({
|
|
||||||
iceServers: m,
|
|
||||||
iceCandidatePoolSize: 0
|
|
||||||
});
|
|
||||||
f.onicecandidate = function(e) {
|
|
||||||
if (null != e.candidate) {
|
|
||||||
r.debug(e.candidate);
|
|
||||||
var a = {
|
|
||||||
action: "pushToTalk",
|
|
||||||
from: t.userId,
|
|
||||||
publishResponse: !1,
|
|
||||||
resource: "cameras/" + i,
|
|
||||||
responseUrl: "",
|
|
||||||
to: o,
|
|
||||||
transId: "web!98b0c88b!1429756137177",
|
|
||||||
properties: {
|
|
||||||
uSessionId: g.uSessionId,
|
|
||||||
type: "offerCandidate",
|
|
||||||
data: e.candidate.candidate
|
|
||||||
}
|
|
||||||
};
|
|
||||||
p = {
|
|
||||||
method: "POST",
|
|
||||||
url: l.getPttNotifyUrl(o),
|
|
||||||
data: a,
|
|
||||||
headers: {
|
|
||||||
xcloudId: t.xCloudId,
|
|
||||||
Authorization: s.ssoToken
|
|
||||||
}
|
|
||||||
},
|
|
||||||
n(p)
|
|
||||||
} else
|
|
||||||
r.debug("Failed to get any more candidate")
|
|
||||||
}
|
|
||||||
,
|
|
||||||
f.oniceconnectionstatechange = function(e) {
|
|
||||||
r.debug("ICE Connection State Change:" + f.iceConnectionState),
|
|
||||||
"connected" == f.iceConnectionState || "completed" == f.iceConnectionState ? g.loading = !1 : "disconnected" != f.iceConnectionState && "failed" != f.iceConnectionState || (g.stopConnection(),
|
|
||||||
g.error = a("i18n")("camera_label_ptt_failed_to_connect"))
|
|
||||||
}
|
|
||||||
,
|
|
||||||
g.pc = f,
|
|
||||||
(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia).call(navigator, {
|
|
||||||
audio: !0,
|
|
||||||
video: !1
|
|
||||||
}, function(e) {
|
|
||||||
g.stream = e,
|
|
||||||
g.stream.getAudioTracks()[0].enabled = !1,
|
|
||||||
f.addStream(e),
|
|
||||||
f.createOffer(function(e) {
|
|
||||||
f.setLocalDescription(e, c, d),
|
|
||||||
r.debug(e.sdp);
|
|
||||||
var a = {
|
|
||||||
action: "pushToTalk",
|
|
||||||
from: t.userId,
|
|
||||||
publishResponse: !0,
|
|
||||||
resource: "cameras/" + i,
|
|
||||||
responseUrl: "",
|
|
||||||
to: o,
|
|
||||||
transId: "web!98b0c88b!1429756137177",
|
|
||||||
properties: {
|
|
||||||
uSessionId: g.uSessionId,
|
|
||||||
type: "offerSdp",
|
|
||||||
data: e.sdp
|
|
||||||
}
|
|
||||||
};
|
|
||||||
p = {
|
|
||||||
method: "POST",
|
|
||||||
url: l.getPttNotifyUrl(o),
|
|
||||||
data: a,
|
|
||||||
headers: {
|
|
||||||
xcloudId: t.xCloudId,
|
|
||||||
Authorization: s.ssoToken
|
|
||||||
}
|
|
||||||
},
|
|
||||||
n(p)
|
|
||||||
}, d)
|
|
||||||
}, d)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
stopConnection: function() {
|
|
||||||
if (g.pc) {
|
|
||||||
var e = {
|
|
||||||
action: "pushToTalk",
|
|
||||||
from: g.device.userId,
|
|
||||||
publishResponse: !1,
|
|
||||||
resource: "cameras/" + g.device.deviceId,
|
|
||||||
responseUrl: "",
|
|
||||||
to: g.device.deviceId,
|
|
||||||
transId: "web!98b0c88b!1429756137177",
|
|
||||||
properties: {
|
|
||||||
uSessionId: g.uSessionId,
|
|
||||||
type: "endSession"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, t = {
|
|
||||||
method: "POST",
|
|
||||||
url: l.getPttNotifyUrl(g.device.deviceId),
|
|
||||||
data: e,
|
|
||||||
headers: {
|
|
||||||
xcloudId: g.device.xCloudId,
|
|
||||||
Authorization: s.ssoToken
|
|
||||||
}
|
|
||||||
};
|
|
||||||
n(t);
|
|
||||||
try {
|
|
||||||
g.stream.getAudioTracks()[0].stop(),
|
|
||||||
g.stream = null
|
|
||||||
} catch (e) {}
|
|
||||||
g.pc.close(),
|
|
||||||
g.pc = null,
|
|
||||||
g.loading = !0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
resp, err := c.arlo.get(fmt.Sprintf(PttUri, c.UniqueId), c.XCloudId, nil)
|
|
||||||
return checkRequest(resp, err, "failed to enable push to talk")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// action: disabled OR recordSnapshot OR recordVideo
|
// action: disabled OR recordSnapshot OR recordVideo
|
||||||
@ -422,178 +285,9 @@ func (c *Camera) SetAlertNotificationMethods(action string, email, push bool) (r
|
|||||||
To: c.ParentId,
|
To: c.ParentId,
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := "failed to set alert notification methods"
|
|
||||||
|
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
b := c.arlo.Basestations.Find(c.ParentId)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
}
|
}
|
||||||
return b.makeEventStreamRequest(payload, msg)
|
return b.makeEventStreamRequest(payload)
|
||||||
}
|
|
||||||
|
|
||||||
// StartStream returns a json object containing the rtmps url to the requested video stream.
|
|
||||||
// You will need something like ffmpeg to read the rtmps stream.
|
|
||||||
|
|
||||||
// If you call StartStream(), you have to start reading data from the stream, or streaming will be cancelled
|
|
||||||
// and taking a snapshot may fail (since it requires the stream to be active).
|
|
||||||
func (c *Camera) StartStream() (url string, err error) {
|
|
||||||
payload := EventStreamPayload{
|
|
||||||
Action: "set",
|
|
||||||
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
|
|
||||||
PublishResponse: true,
|
|
||||||
Properties: map[string]string{
|
|
||||||
"activityState": "startUserStream",
|
|
||||||
"cameraId": c.DeviceId,
|
|
||||||
},
|
|
||||||
TransId: genTransId(),
|
|
||||||
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
|
||||||
To: c.ParentId,
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := "failed to start stream"
|
|
||||||
|
|
||||||
resp, err := c.arlo.post(StartStreamUri, c.XCloudId, payload, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.WithMessage(err, msg)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
response := new(StreamResponse)
|
|
||||||
if err := resp.Decode(response); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !response.Success {
|
|
||||||
return "", errors.WithMessage(errors.New("status was false"), msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Data.URL = strings.Replace(response.Data.URL, "rtsp://", "rtsps://", 1)
|
|
||||||
|
|
||||||
return response.Data.URL, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TakeSnapshot causes the camera to snapshot while recording.
|
|
||||||
// NOTE: You MUST call StartStream() before calling this function.
|
|
||||||
// If you call StartStream(), you have to start reading data from the stream, or streaming will be cancelled
|
|
||||||
// and taking a snapshot may fail (since it requires the stream to be active).
|
|
||||||
|
|
||||||
// NOTE: You should not use this function is you just want a snapshot and aren't intending to stream.
|
|
||||||
// Use TriggerFullFrameSnapshot() instead.
|
|
||||||
//
|
|
||||||
// NOTE: Use DownloadSnapshot() to download the actual image file.
|
|
||||||
// TODO: Need to refactor the even stream code to allow handling of events whose transIds don't correlate. :/
|
|
||||||
func (c *Camera) TakeSnapshot() (response *EventStreamResponse, err error) {
|
|
||||||
|
|
||||||
return nil, errors.New("TakeSnapshot not implemented")
|
|
||||||
/*
|
|
||||||
msg := "failed to take snapshot"
|
|
||||||
|
|
||||||
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
|
|
||||||
resp, err := c.arlo.post(TakeSnapshotUri, c.XCloudId, body, nil)
|
|
||||||
if err := checkRequest(resp, err, msg); err != nil {
|
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO: Need to write the code to handle the event stream message.
|
|
||||||
/*
|
|
||||||
def callback(self, event):
|
|
||||||
if event.get("deviceId") == camera.get("deviceId") and event.get("resource") == "mediaUploadNotification":
|
|
||||||
presigned_content_url = event.get("presignedContentUrl")
|
|
||||||
if presigned_content_url is not None:
|
|
||||||
r return presigned_content_url
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
// TriggerFullFrameSnapshot causes the camera to record a full-frame snapshot.
|
|
||||||
// The presignedFullFrameSnapshotUrl url is returned.
|
|
||||||
// Use DownloadSnapshot() to download the actual image file.
|
|
||||||
// TODO: Need to refactor the even stream code to allow handling of events whose transIds don't correlate. :/
|
|
||||||
func (c *Camera) TriggerFullFrameSnapshot() (response *EventStreamResponse, err error) {
|
|
||||||
|
|
||||||
return nil, errors.New("TriggerFullFrameSnapshot not implemented")
|
|
||||||
/*
|
|
||||||
payload := EventStreamPayload{
|
|
||||||
Action: "set",
|
|
||||||
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
|
|
||||||
PublishResponse: true,
|
|
||||||
Properties: map[string]string{
|
|
||||||
"activityState": "fullFrameSnapshot",
|
|
||||||
},
|
|
||||||
TransId: genTransId(),
|
|
||||||
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
|
||||||
To: c.ParentId,
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := "failed to trigger full-frame snapshot"
|
|
||||||
|
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
|
||||||
if b == nil {
|
|
||||||
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
}
|
|
||||||
return b.makeEventStreamRequest(payload, msg)
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
def callback(self, event):
|
|
||||||
if event.get("from") == basestation.get("deviceId") and event.get("resource") == "cameras/"+camera.get("deviceId") and event.get("action") == "fullFrameSnapshotAvailable":
|
|
||||||
return event.get("properties", {}).get("presignedFullFrameSnapshotUrl")
|
|
||||||
return None
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartRecording causes the camera to start recording and returns a url that you must start reading from using ffmpeg
|
|
||||||
// or something similar.
|
|
||||||
func (c *Camera) StartRecording() (url string, err error) {
|
|
||||||
msg := "failed to start recording"
|
|
||||||
|
|
||||||
url, err = c.StartStream()
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.WithMessage(err, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
|
|
||||||
resp, err := c.arlo.post(StartRecordUri, c.XCloudId, body, nil)
|
|
||||||
if err := checkRequest(resp, err, msg); err != nil {
|
|
||||||
return "", errors.WithMessage(err, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return url, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopRecording causes the camera to stop recording.
|
|
||||||
func (c *Camera) StopRecording() error {
|
|
||||||
msg := "failed to stop recording"
|
|
||||||
|
|
||||||
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
|
|
||||||
resp, err := c.arlo.post(StopRecordUri, c.XCloudId, body, nil)
|
|
||||||
if err := checkRequest(resp, err, msg); err != nil {
|
|
||||||
return errors.WithMessage(err, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function downloads a Cvr Playlist file for the period fromDate to toDate.
|
|
||||||
func (c *Camera) GetCvrPlaylist(fromDate, toDate time.Time) (playlist *CvrPlaylist, err error) {
|
|
||||||
msg := "failed to get cvr playlist"
|
|
||||||
|
|
||||||
resp, err := c.arlo.get(fmt.Sprintf(PlaylistUri, c.UniqueId, fromDate.Format("20060102"), toDate.Format("20060102")), c.XCloudId, nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithMessage(err, msg)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
response := new(CvrPlaylistResponse)
|
|
||||||
if err := resp.Decode(&response); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !response.Success {
|
|
||||||
return nil, errors.New(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &response.Data, nil
|
|
||||||
}
|
}
|
||||||
|
30
cmd/main.go
Normal file
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 {}
|
||||||
|
|
||||||
|
}
|
49
devices.go
49
devices.go
@ -46,7 +46,7 @@ type Device struct {
|
|||||||
PresignedFullFrameSnapshotUrl string `json:"presignedFullFrameSnapshotUrl"`
|
PresignedFullFrameSnapshotUrl string `json:"presignedFullFrameSnapshotUrl"`
|
||||||
PresignedLastImageUrl string `json:"presignedLastImageUrl"`
|
PresignedLastImageUrl string `json:"presignedLastImageUrl"`
|
||||||
PresignedSnapshotUrl string `json:"presignedSnapshotUrl"`
|
PresignedSnapshotUrl string `json:"presignedSnapshotUrl"`
|
||||||
MediaObjectCount uint8 `json:"mediaObjectCount"`
|
MediaObjectCount uint32 `json:"mediaObjectCount"`
|
||||||
ModelId string `json:"modelId"`
|
ModelId string `json:"modelId"`
|
||||||
Owner Owner `json:"owner"`
|
Owner Owner `json:"owner"`
|
||||||
ParentId string `json:"parentId"`
|
ParentId string `json:"parentId"`
|
||||||
@ -59,7 +59,7 @@ type Device struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Devices is a slice of Device objects.
|
// Devices is a slice of Device objects.
|
||||||
type Devices []Device
|
type Devices []*Device
|
||||||
|
|
||||||
// A DeviceOrder holds a map of device ids and a numeric index. The numeric index is the device order.
|
// A DeviceOrder holds a map of device ids and a numeric index. The numeric index is the device order.
|
||||||
// Device order is mainly used by the UI to determine which order to show the devices.
|
// Device order is mainly used by the UI to determine which order to show the devices.
|
||||||
@ -76,10 +76,10 @@ type DeviceOrder struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find returns a device with the device id passed in.
|
// Find returns a device with the device id passed in.
|
||||||
func (ds *Devices) Find(deviceId string) *Device {
|
func (ds Devices) Find(deviceId string) *Device {
|
||||||
for _, d := range *ds {
|
for _, d := range ds {
|
||||||
if d.DeviceId == deviceId {
|
if d.DeviceId == deviceId {
|
||||||
return &d
|
return d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,22 +87,22 @@ func (ds *Devices) Find(deviceId string) *Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ds Devices) FindCameras(basestationId string) Cameras {
|
func (ds Devices) FindCameras(basestationId string) Cameras {
|
||||||
cs := new(Cameras)
|
cs := Cameras{}
|
||||||
for _, d := range ds {
|
for _, d := range ds {
|
||||||
if d.ParentId == basestationId {
|
if d.ParentId == basestationId {
|
||||||
*cs = append(*cs, Camera(d))
|
cam := Camera(*d)
|
||||||
|
cs = append(cs, &cam)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return cs
|
||||||
return *cs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Device) IsBasestation() bool {
|
func (d Device) IsBasestation() bool {
|
||||||
return d.DeviceType == DeviceTypeBasestation || d.DeviceId == d.ParentId
|
return d.DeviceType == DeviceTypeBasestation
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Device) IsCamera() bool {
|
func (d Device) IsCamera() bool {
|
||||||
switch(d.DeviceType) {
|
switch d.DeviceType {
|
||||||
case
|
case
|
||||||
DeviceTypeCamera,
|
DeviceTypeCamera,
|
||||||
DeviceTypeArloQ:
|
DeviceTypeArloQ:
|
||||||
@ -127,11 +127,11 @@ func (d Device) IsSiren() bool {
|
|||||||
// I did this because some device types, like arloq, don't have a basestation.
|
// I did this because some device types, like arloq, don't have a basestation.
|
||||||
// So, when interacting with them you must treat them like a basestation and a camera.
|
// So, when interacting with them you must treat them like a basestation and a camera.
|
||||||
// Cameras also includes devices of this type, so you can get the same data there or cast.
|
// Cameras also includes devices of this type, so you can get the same data there or cast.
|
||||||
func (ds Devices) GetBasestations() *Basestations {
|
func (ds Devices) GetBasestations() Basestations {
|
||||||
basestations := new(Basestations)
|
var basestations Basestations
|
||||||
for _, d := range ds {
|
for _, d := range ds {
|
||||||
if d.IsBasestation() || !d.IsCamera() {
|
if d.IsBasestation() {
|
||||||
*basestations = append(*basestations, Basestation{Device: d})
|
basestations = append(basestations, &Basestation{Device: *d})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return basestations
|
return basestations
|
||||||
@ -141,19 +141,20 @@ func (ds Devices) GetBasestations() *Basestations {
|
|||||||
// I did this because some device types, like arloq, don't have a basestation.
|
// I did this because some device types, like arloq, don't have a basestation.
|
||||||
// So, when interacting with them you must treat them like a basestation and a camera.
|
// So, when interacting with them you must treat them like a basestation and a camera.
|
||||||
// Basestations also includes devices of this type, so you can get the same data there or cast.
|
// Basestations also includes devices of this type, so you can get the same data there or cast.
|
||||||
func (ds Devices) GetCameras() *Cameras {
|
func (ds Devices) GetCameras() Cameras {
|
||||||
cameras := new(Cameras)
|
var cameras Cameras
|
||||||
for _, d := range ds {
|
for _, d := range ds {
|
||||||
if d.IsCamera() || !d.IsBasestation() {
|
if d.IsCamera() {
|
||||||
*cameras = append(*cameras, Camera(d))
|
cam := Camera(*d)
|
||||||
|
cameras = append(cameras, &cam)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cameras
|
return cameras
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateDeviceName sets the name of the given device to the name argument.
|
// UpdateDeviceName sets the name of the given device to the name argument.
|
||||||
func (d *Device) UpdateDeviceName(name string) error {
|
//func (d *Device) UpdateDeviceName(name string) error {
|
||||||
body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId}
|
// body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId}
|
||||||
resp, err := d.arlo.put(RenameDeviceUri, d.XCloudId, body, nil)
|
// resp, err := d.arlo.put(RenameDeviceUri, d.XCloudId, body, nil)
|
||||||
return checkRequest(resp, err, "failed to update device name")
|
// return checkRequest(resp, err, "failed to update device name")
|
||||||
}
|
//}
|
||||||
|
176
events_stream.go
176
events_stream.go
@ -18,132 +18,148 @@ package arlo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/r3labs/sse"
|
"github.com/r3labs/sse"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
FAILED_TO_PUBLISH = errors.New("failed to publish")
|
|
||||||
FAILED_TO_DECODE_JSON = errors.New("failed to decode json")
|
|
||||||
FAILED_TO_SUBSCRIBE = errors.New("failed to subscribe to seeclient")
|
|
||||||
)
|
|
||||||
|
|
||||||
type subscriber chan *EventStreamResponse
|
|
||||||
|
|
||||||
type subscribers map[string]subscriber
|
|
||||||
|
|
||||||
type subscriptions struct {
|
|
||||||
subscribers
|
|
||||||
rwmutex sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
type eventStream struct {
|
type eventStream struct {
|
||||||
SSEClient *sse.Client
|
SSEClient *sse.Client
|
||||||
Events chan *sse.Event
|
Events chan *sse.Event
|
||||||
Error chan error
|
Error chan error
|
||||||
Verbose bool
|
Verbose bool
|
||||||
Disconnected chan interface{}
|
DisconnectedChan chan interface{}
|
||||||
once *sync.Once
|
once *sync.Once
|
||||||
|
|
||||||
subscriptions
|
subscribers map[string]chan *EventStreamResponse
|
||||||
|
subscribersMutex sync.RWMutex
|
||||||
|
|
||||||
|
errorsubsribers map[string]chan error
|
||||||
|
errorMutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEventStream(url string, client *http.Client) *eventStream {
|
func newEventStream(url string, client *http.Client) *eventStream {
|
||||||
|
e := &eventStream{
|
||||||
|
Events: make(chan *sse.Event),
|
||||||
|
subscribers: make(map[string]chan *EventStreamResponse),
|
||||||
|
subscribersMutex: sync.RWMutex{},
|
||||||
|
errorsubsribers: make(map[string]chan error),
|
||||||
|
errorMutex: sync.RWMutex{},
|
||||||
|
DisconnectedChan: make(chan interface{}),
|
||||||
|
once: new(sync.Once),
|
||||||
|
}
|
||||||
|
|
||||||
SSEClient := sse.NewClient(url)
|
SSEClient := sse.NewClient(url)
|
||||||
SSEClient.Connection = client
|
SSEClient.Connection = client
|
||||||
|
SSEClient.OnDisconnect(func(c *sse.Client) {
|
||||||
return &eventStream{
|
e.disconnect()
|
||||||
SSEClient: SSEClient,
|
})
|
||||||
Events: make(chan *sse.Event),
|
e.SSEClient = SSEClient
|
||||||
subscriptions: subscriptions{make(map[string]subscriber), sync.RWMutex{}},
|
return e
|
||||||
Error: make(chan error),
|
|
||||||
Disconnected: make(chan interface{}),
|
|
||||||
once: new(sync.Once),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *eventStream) disconnect() {
|
func (e *eventStream) disconnect() {
|
||||||
e.once.Do(func() {
|
e.once.Do(func() {
|
||||||
close(e.Disconnected)
|
close(e.DisconnectedChan)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *eventStream) listen() (connected chan bool) {
|
func (e *eventStream) listen(ctx context.Context) (chan bool, error) {
|
||||||
connected = make(chan bool)
|
connectedChan := make(chan bool)
|
||||||
|
|
||||||
go func() {
|
|
||||||
e.SSEClient.OnDisconnect(func(c *sse.Client) {
|
|
||||||
e.disconnect()
|
|
||||||
// fmt.Printf("\n\n\n\nClIENT DISCONNECTED!!!!!\n\n\n\n")
|
|
||||||
})
|
|
||||||
err := e.SSEClient.SubscribeChanRaw(e.Events)
|
err := e.SSEClient.SubscribeChanRaw(e.Events)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.Error <- FAILED_TO_SUBSCRIBE
|
return nil, fmt.Errorf("failed to subscribe to seeclient")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event := <-e.Events:
|
case <-ctx.Done():
|
||||||
//fmt.Println("Got event message.")
|
e.disconnect()
|
||||||
/*
|
return
|
||||||
fmt.Print(".")
|
case event, ok := <-e.Events:
|
||||||
fmt.Printf("EVENT: %s\n", event.Event)
|
if !ok {
|
||||||
fmt.Printf("DATA: %s\n", event.Data)
|
return
|
||||||
*/
|
}
|
||||||
|
|
||||||
if event != nil && event.Data != nil {
|
if event == nil || event.Data == nil {
|
||||||
notifyResponse := &EventStreamResponse{}
|
log.Warn("EventStream > nil event or nil data in event")
|
||||||
b := bytes.NewBuffer(event.Data)
|
continue
|
||||||
err := json.NewDecoder(b).Decode(notifyResponse)
|
}
|
||||||
|
|
||||||
|
fmt.Printf("DATA : %s\n", event.Data)
|
||||||
|
|
||||||
|
var notifyResponse EventStreamResponse
|
||||||
|
err := json.NewDecoder(bytes.NewBuffer(event.Data)).Decode(¬ifyResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.Error <- FAILED_TO_DECODE_JSON
|
log.Warnf("EventStream > failed to decode event: %s", event.Data)
|
||||||
break
|
continue
|
||||||
|
}
|
||||||
|
bytesProperties, err := json.Marshal(notifyResponse.EventStreamPayload.Properties)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("EventStream > failed to marshal raw properties: %s", event.Data)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
notifyResponse.RawProperties = bytesProperties
|
||||||
|
|
||||||
|
if notifyResponse.Status == "connected" {
|
||||||
|
connectedChan <- true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if notifyResponse.Status == "disconnected" {
|
||||||
|
e.disconnect()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if notifyResponse.Action == "logout" {
|
||||||
|
log.Warn("EventStream > logged out")
|
||||||
|
e.disconnect()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.subscribersMutex.RLock()
|
||||||
|
subscriber, ok := e.subscribers[notifyResponse.TransId]
|
||||||
|
e.subscribersMutex.RUnlock()
|
||||||
|
if ok {
|
||||||
|
subscriber <- ¬ifyResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: This is a shitty way to handle this. It's potentially leaking a chan.
|
case <-e.DisconnectedChan:
|
||||||
if notifyResponse.Status == "connected" {
|
connectedChan <- false
|
||||||
connected <- true
|
|
||||||
} else if notifyResponse.Status == "disconnected" {
|
|
||||||
e.disconnect()
|
|
||||||
} else if notifyResponse.Action == "logout" {
|
|
||||||
e.disconnect()
|
|
||||||
} else {
|
|
||||||
e.subscriptions.rwmutex.RLock()
|
|
||||||
subscriber, ok := e.subscribers[notifyResponse.TransId]
|
|
||||||
e.subscriptions.rwmutex.RUnlock()
|
|
||||||
if ok {
|
|
||||||
subscriber <- notifyResponse
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <-e.Disconnected:
|
|
||||||
connected <- false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return connected
|
return connectedChan, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *subscriptions) unsubscribe(transId string) {
|
func (e *eventStream) unsubscribe(transId string) {
|
||||||
s.rwmutex.Lock()
|
e.subscribersMutex.Lock()
|
||||||
defer s.rwmutex.Unlock()
|
if c, ok := e.subscribers[transId]; ok {
|
||||||
if _, ok := s.subscribers[transId]; ok {
|
close(c)
|
||||||
close(s.subscribers[transId])
|
delete(e.subscribers, transId)
|
||||||
delete(s.subscribers, transId)
|
|
||||||
}
|
}
|
||||||
|
e.subscribersMutex.Unlock()
|
||||||
|
|
||||||
|
e.errorMutex.Lock()
|
||||||
|
if c, ok := e.errorsubsribers[transId]; ok {
|
||||||
|
close(c)
|
||||||
|
delete(e.errorsubsribers, transId)
|
||||||
|
}
|
||||||
|
e.errorMutex.Unlock()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *subscriptions) subscribe(transId string, subscriber subscriber) {
|
func (e *eventStream) subscribe(transId string, subscriber chan *EventStreamResponse, errorChan chan error) {
|
||||||
s.rwmutex.Lock()
|
e.subscribersMutex.Lock()
|
||||||
s.subscribers[transId] = subscriber
|
e.subscribers[transId] = subscriber
|
||||||
s.rwmutex.Unlock()
|
e.subscribersMutex.Unlock()
|
||||||
|
|
||||||
|
e.errorMutex.Lock()
|
||||||
|
e.errorsubsribers[transId] = errorChan
|
||||||
|
e.errorMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
8
go.mod
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 (
|
require (
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/go-resty/resty/v2 v2.3.0
|
||||||
github.com/r3labs/sse v0.0.0-20191120111931-24eacf438413
|
github.com/r3labs/sse v0.0.0-20191120111931-24eacf438413
|
||||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933
|
github.com/sirupsen/logrus v1.6.0
|
||||||
)
|
)
|
||||||
|
23
go.sum
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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-resty/resty v1.12.0 h1:L1P5qymrXL5H/doXe2pKUr1wxovAI5ilm2LdVLbwThc=
|
||||||
|
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
|
||||||
|
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
|
||||||
|
github.com/jeffreydwalter/arlo-go v0.0.0-20200420231349-d68ae1fb4cb7 h1:vFCcYqhV2UgUB2ROxwdW1fVoOVE33jr4snGsRKKIBQ8=
|
||||||
|
github.com/jeffreydwalter/arlo-go v0.0.0-20200420231349-d68ae1fb4cb7/go.mod h1:eNo//rDcum+KKc3Itlb1YzEzX+7dBuRzdD93eS5pd2Q=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/r3labs/sse v0.0.0-20191120111931-24eacf438413 h1:bF3heZD0lrJF16uVKKJsorZjBS7ET5Y9QKYtgno7X4Q=
|
github.com/r3labs/sse v0.0.0-20191120111931-24eacf438413 h1:bF3heZD0lrJF16uVKKJsorZjBS7ET5Y9QKYtgno7X4Q=
|
||||||
github.com/r3labs/sse v0.0.0-20191120111931-24eacf438413/go.mod h1:S8xSOnV3CgpNrWd0GQ/OoQfMtlg2uPRSuTzcSGrzwK8=
|
github.com/r3labs/sse v0.0.0-20191120111931-24eacf438413/go.mod h1:S8xSOnV3CgpNrWd0GQ/OoQfMtlg2uPRSuTzcSGrzwK8=
|
||||||
|
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||||
|
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk=
|
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk=
|
||||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY=
|
||||||
|
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
|
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
|
||||||
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
|
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
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
|
package arlo
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LibraryMetaData is the library meta data.
|
// LibraryMetaData is the library meta data.
|
||||||
type LibraryMetaData struct {
|
type LibraryMetaData struct {
|
||||||
DateTo string `json:"dateTo"`
|
DateTo string `json:"dateTo"`
|
||||||
@ -53,169 +47,87 @@ type Recording struct {
|
|||||||
|
|
||||||
type Library []Recording
|
type Library []Recording
|
||||||
|
|
||||||
func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (libraryMetaData *LibraryMetaData, err error) {
|
//func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (libraryMetaData *LibraryMetaData, err error) {
|
||||||
msg := "failed to get library metadata"
|
// msg := "failed to get library metadata"
|
||||||
|
//
|
||||||
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
|
// body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
|
||||||
resp, err := a.post(MetadataUri, "", body, nil)
|
// resp, err := a.post(MetadataUri, "", body, nil)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, errors.WithMessage(err, msg)
|
// return nil, errors.WithMessage(err, msg)
|
||||||
}
|
// }
|
||||||
defer resp.Body.Close()
|
// defer resp.Body.Close()
|
||||||
|
//
|
||||||
response := new(LibraryMetaDataResponse)
|
// response := new(LibraryMetaDataResponse)
|
||||||
if err := resp.Decode(&response); err != nil {
|
// if err := resp.Decode(&response); err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if !response.Success {
|
// if !response.Success {
|
||||||
return nil, errors.New(msg)
|
// return nil, errors.New(msg)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return &response.Data, nil
|
// return &response.Data, nil
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (library *Library, err error) {
|
//func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (library *Library, err error) {
|
||||||
msg := "failed to get library"
|
// msg := "failed to get library"
|
||||||
|
//
|
||||||
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
|
// body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
|
||||||
resp, err := a.post(RecordingsUri, "", body, nil)
|
// resp, err := a.post(RecordingsUri, "", body, nil)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, errors.WithMessage(err, msg)
|
// return nil, errors.WithMessage(err, msg)
|
||||||
}
|
// }
|
||||||
defer resp.Body.Close()
|
// defer resp.Body.Close()
|
||||||
|
//
|
||||||
response := new(LibraryResponse)
|
// response := new(LibraryResponse)
|
||||||
if err := resp.Decode(&response); err != nil {
|
// if err := resp.Decode(&response); err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if !response.Success {
|
// if !response.Success {
|
||||||
return nil, errors.New(msg)
|
// return nil, errors.New(msg)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return &response.Data, nil
|
// return &response.Data, nil
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
/*
|
///*
|
||||||
Delete a single video recording from arlo.
|
// Delete a single video recording from arlo.
|
||||||
|
//
|
||||||
All of the date info and device id you need to pass into this method are given in the results of the GetLibrary() call.
|
// All of the date info and device id you need to pass into this method are given in the results of the GetLibrary() call.
|
||||||
|
//
|
||||||
NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
|
// NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
|
||||||
*/
|
//*/
|
||||||
func (a *Arlo) DeleteRecording(r *Recording) error {
|
//func (a *Arlo) DeleteRecording(r *Recording) error {
|
||||||
body := map[string]Library{"data": {*r}}
|
// body := map[string]Library{"data": {*r}}
|
||||||
resp, err := a.post(RecycleUri, "", body, nil)
|
// resp, err := a.post(RecycleUri, "", body, nil)
|
||||||
return checkRequest(resp, err, "failed to delete recording")
|
// return checkRequest(resp, err, "failed to delete recording")
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
/*
|
///*
|
||||||
Delete a batch of video recordings from arlo.
|
// Delete a batch of video recordings from arlo.
|
||||||
|
//
|
||||||
The GetLibrary() call response json can be passed directly to this method if you'd like to delete the same list of videos you queried for.
|
// The GetLibrary() call response json can be passed directly to this method if you'd like to delete the same list of videos you queried for.
|
||||||
|
//
|
||||||
NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
|
// NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
|
||||||
*/
|
//*/
|
||||||
func (a *Arlo) BatchDeleteRecordings(l *Library) error {
|
//func (a *Arlo) BatchDeleteRecordings(l *Library) error {
|
||||||
body := map[string]Library{"data": *l}
|
// body := map[string]Library{"data": *l}
|
||||||
resp, err := a.post(RecycleUri, "", body, nil)
|
// resp, err := a.post(RecycleUri, "", body, nil)
|
||||||
return checkRequest(resp, err, "failed to delete recordings")
|
// return checkRequest(resp, err, "failed to delete recordings")
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
// SendAnalyticFeedback is only really used by the GUI. It is a response to a prompt asking you whether an object which
|
//// SendAnalyticFeedback is only really used by the GUI. It is a response to a prompt asking you whether an object which
|
||||||
// was tagged by it's AI in your recording was tagged correctly.
|
//// was tagged by it's AI in your recording was tagged correctly.
|
||||||
func (a *Arlo) SendAnalyticFeedback(r *Recording) error {
|
//func (a *Arlo) SendAnalyticFeedback(r *Recording) error {
|
||||||
category := "Person" // Other
|
// category := "Person" // Other
|
||||||
body := map[string]map[string]interface{}{"data": {"utcCreatedDate": r.UtcCreatedDate, "category": category, "createdDate": r.CreatedDate}}
|
// body := map[string]map[string]interface{}{"data": {"utcCreatedDate": r.UtcCreatedDate, "category": category, "createdDate": r.CreatedDate}}
|
||||||
resp, err := a.put(AnalyticFeedbackUri, "", body, nil)
|
// resp, err := a.put(AnalyticFeedbackUri, "", body, nil)
|
||||||
return checkRequest(resp, err, "failed to send analytic feedback about recording")
|
// return checkRequest(resp, err, "failed to send analytic feedback about recording")
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
// GetActiveAutomationDefinitions gets the mode metadata (this API replaces the older GetModes(), which still works).
|
//// GetActiveAutomationDefinitions gets the mode metadata (this API replaces the older GetModes(), which still works).
|
||||||
func (a *Arlo) GetActiveAutomationDefinitions() error {
|
//func (a *Arlo) GetActiveAutomationDefinitions() error {
|
||||||
resp, err := a.get(ActiveAutomationUri, "", nil)
|
// resp, err := a.get(ActiveAutomationUri, "", nil)
|
||||||
return checkRequest(resp, err, "failed to get active automation definitions")
|
// return checkRequest(resp, err, "failed to get active automation definitions")
|
||||||
}
|
//}
|
||||||
|
|
||||||
/*
|
|
||||||
func (a *Arlo) SetActiveAutomationMode() error {
|
|
||||||
|
|
||||||
body := struct{}{} //map[string]map[string]interface{}{"data": {"utcCreatedDate": r.UtcCreatedDate, "category": category, "createdDate": r.CreatedDate}}
|
|
||||||
resp, err := a.put(AnalyticFeedbackUri, "", body, nil)
|
|
||||||
return checkRequest(resp, err, "failed to send analytic feedback about recording")
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"activeModes": [
|
|
||||||
"mode1"
|
|
||||||
],
|
|
||||||
"activeSchedules": [],
|
|
||||||
"gatewayId": "48935B7SA9847",
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"timestamp": 1536781758034,
|
|
||||||
"type": "activeAutomations",
|
|
||||||
"uniqueId": "336-4764296_48935B7SA9847"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
setActiveAutomationMode: function(r, a) {
|
|
||||||
var s = {
|
|
||||||
activeAutomations: [{
|
|
||||||
deviceId: a.gatewayId,
|
|
||||||
timestamp: _.now(),
|
|
||||||
activeModes: [r],
|
|
||||||
activeSchedules: []
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
, l = {
|
|
||||||
method: "POST",
|
|
||||||
data: s,
|
|
||||||
url: d.getActiveAutomationUrl(a.gatewayId),
|
|
||||||
headers: {
|
|
||||||
Authorization: o.ssoToken,
|
|
||||||
schemaVersion: 1
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return n.debug("calling set active automation mode with config:" + JSON.stringify(l)),
|
|
||||||
i(l).then(function(i) {
|
|
||||||
if (n.debug("got set active automation mode result:" + JSON.stringify(i)),
|
|
||||||
i && i.data && !i.data.success)
|
|
||||||
return e.$broadcast(c.appEvents.SHOW_ERROR, i.data),
|
|
||||||
t.reject(i.data)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
setActiveAutomationSchedule: function(r) {
|
|
||||||
var r = {
|
|
||||||
activeAutomations: [{
|
|
||||||
deviceId: r.deviceId,
|
|
||||||
timestamp: _.now(),
|
|
||||||
activeModes: [],
|
|
||||||
activeSchedules: [r.scheduleId]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
, a = {
|
|
||||||
method: "POST",
|
|
||||||
data: r,
|
|
||||||
url: d.getActiveAutomationUrl(r.deviceId),
|
|
||||||
headers: {
|
|
||||||
Authorization: o.ssoToken,
|
|
||||||
schemaVersion: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, s = this;
|
|
||||||
return n.debug("calling set active automation schedule with config:" + JSON.stringify(a)),
|
|
||||||
i(a).then(function(i) {
|
|
||||||
return n.debug("got set active automation schedule result:" + JSON.stringify(i)),
|
|
||||||
i && i.data && !i.data.success ? (e.$broadcast(c.appEvents.SHOW_ERROR, i.data),
|
|
||||||
t.reject(i.data)) : i && i.data && i.data.success ? (_.filter(s.activeAutomationDefinitions, function(e) {
|
|
||||||
e.gatewayId == i.config.data.activeAutomations[0].deviceId && (e.activeModes = i.config.data.activeAutomations[0].activeModes,
|
|
||||||
e.activeSchedules = i.config.data.activeAutomations[0].activeSchedules)
|
|
||||||
}),
|
|
||||||
i.config.data.activeAutomations[0]) : void 0
|
|
||||||
})
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
|
@ -85,4 +85,5 @@ type RecordingResponse struct {
|
|||||||
type EventStreamResponse struct {
|
type EventStreamResponse struct {
|
||||||
EventStreamPayload
|
EventStreamPayload
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
|
RawProperties []byte
|
||||||
}
|
}
|
||||||
|
8
types.go
8
types.go
@ -251,14 +251,6 @@ type SirenProperties struct {
|
|||||||
Pattern string `json:"pattern"`
|
Pattern string `json:"pattern"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BasestationModeProperties struct {
|
|
||||||
Active string `json:"active"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BasestationScheduleProperties struct {
|
|
||||||
Active bool `json:"active"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CameraProperties struct {
|
type CameraProperties struct {
|
||||||
PrivacyActive bool `json:"privacyActive"`
|
PrivacyActive bool `json:"privacyActive"`
|
||||||
Brightness int `json:"brightness,omitempty"`
|
Brightness int `json:"brightness,omitempty"`
|
||||||
|
111
util.go
111
util.go
@ -25,100 +25,69 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jeffreydwalter/arlo-go/internal/request"
|
|
||||||
"github.com/jeffreydwalter/arlo-go/internal/util"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkRequest(resp *request.Response, err error, msg string) error {
|
func FloatToHex(x float64) string {
|
||||||
if err != nil {
|
var result []byte
|
||||||
return errors.WithMessage(err, msg)
|
quotient := int(x)
|
||||||
}
|
fraction := x - float64(quotient)
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var status Status
|
for quotient > 0 {
|
||||||
if err := resp.Decode(&status); err != nil {
|
quotient = int(x / 16)
|
||||||
return err
|
remainder := int(x - (float64(quotient) * 16))
|
||||||
|
|
||||||
|
if remainder > 9 {
|
||||||
|
result = append([]byte{byte(remainder + 55)}, result...)
|
||||||
|
} else {
|
||||||
|
for _, c := range strconv.Itoa(int(remainder)) {
|
||||||
|
result = append([]byte{byte(c)}, result...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.Success == false {
|
x = float64(quotient)
|
||||||
return errors.WithMessage(errors.New(status.Reason), msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if fraction == 0 {
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, '.')
|
||||||
|
|
||||||
|
for fraction > 0 {
|
||||||
|
fraction = fraction * 16
|
||||||
|
integer := int(fraction)
|
||||||
|
fraction = fraction - float64(integer)
|
||||||
|
|
||||||
|
if integer > 9 {
|
||||||
|
result = append(result, byte(integer+55))
|
||||||
|
} else {
|
||||||
|
for _, c := range strconv.Itoa(int(integer)) {
|
||||||
|
result = append(result, byte(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func genTransId() string {
|
func genTransId() string {
|
||||||
|
|
||||||
source := rand.NewSource(time.Now().UnixNano())
|
random := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
random := rand.New(source)
|
|
||||||
|
|
||||||
e := random.Float64() * math.Pow(2, 32)
|
e := random.Float64() * math.Pow(2, 32)
|
||||||
|
|
||||||
ms := time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
|
ms := time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
|
||||||
|
|
||||||
return fmt.Sprintf("%s!%s!%s", TransIdPrefix, strings.ToLower(util.FloatToHex(e)), strconv.Itoa(int(ms)))
|
return fmt.Sprintf("%s!%s!%s", TransIdPrefix, strings.ToLower(FloatToHex(e)), strconv.Itoa(int(ms)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Arlo) get(uri, xCloudId string, header http.Header) (*request.Response, error) {
|
|
||||||
a.client.AddHeader("xcloudId", xCloudId)
|
|
||||||
return a.client.Get(uri, header)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Arlo) put(uri, xCloudId string, body interface{}, header http.Header) (*request.Response, error) {
|
|
||||||
a.client.AddHeader("xcloudId", xCloudId)
|
|
||||||
return a.client.Put(uri, body, header)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Arlo) post(uri, xCloudId string, body interface{}, header http.Header) (*request.Response, error) {
|
|
||||||
a.client.AddHeader("xcloudId", xCloudId)
|
|
||||||
return a.client.Post(uri, body, header)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
func (a *Arlo) DownloadFile(url, to string) error {
|
|
||||||
msg := fmt.Sprintf("failed to download file (%s) => (%s)", url, to)
|
|
||||||
resp, err := a.get(url, "", nil)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithMessage(err, msg)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
f, err := os.Create(to)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithMessage(err, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.Copy(f, resp.Body)
|
|
||||||
defer f.Close()
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithMessage(err, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
func (a *Arlo) DownloadFile(url string, w io.Writer) error {
|
func (a *Arlo) DownloadFile(url string, w io.Writer) error {
|
||||||
msg := fmt.Sprintf("failed to download file (%s)", url)
|
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithMessage(err, msg)
|
return fmt.Errorf("getting %s: %v", url, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
_, err = io.Copy(w, resp.Body)
|
_, err = io.Copy(w, resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithMessage(err, msg)
|
return fmt.Errorf("copying body to writer: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromUnixMicro(µs int64) time.Time { return time.Unix(0, 1000*µs) }
|
|
||||||
|
|
||||||
func FromUnixMilli(ms int64) time.Time { return time.Unix(0, 1000000*ms) }
|
|
||||||
|
Loading…
Reference in New Issue
Block a user