grosse refacto avec gestion de la reconnexion
This commit is contained in:
parent
24bac70957
commit
21aec5f516
313
arlo.go
313
arlo.go
@ -2,21 +2,23 @@ package arlo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Arlo struct {
|
type Arlo struct {
|
||||||
user string
|
|
||||||
pass string
|
|
||||||
client *resty.Client
|
client *resty.Client
|
||||||
|
eventStream *eventStream
|
||||||
|
|
||||||
Account Account
|
Account Account
|
||||||
Basestations Basestations
|
Basestations []*Basestation
|
||||||
Cameras Cameras
|
Cameras []*Camera
|
||||||
rwmutex sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewArlo() (arlo *Arlo) {
|
func NewArlo() (arlo *Arlo) {
|
||||||
@ -30,11 +32,20 @@ func NewArlo() (arlo *Arlo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Arlo) getBaseRequest(body interface{}, result interface{}, errResult interface{}, xcloudId string) *resty.Request {
|
||||||
|
r := a.client.R().
|
||||||
|
SetBody(body).
|
||||||
|
SetResult(result).
|
||||||
|
SetError(errResult)
|
||||||
|
if xcloudId != "" {
|
||||||
|
r.SetHeader("xcloudId", xcloudId)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Arlo) get(url string, result interface{}) error {
|
func (a *Arlo) get(url string, result interface{}) error {
|
||||||
var errorResponse ErrorResponse
|
var errorResponse ErrorResponse
|
||||||
resp, err := a.client.R().
|
resp, err := a.getBaseRequest(nil, result, &errorResponse, "").
|
||||||
SetResult(result).
|
|
||||||
SetError(&errorResponse).
|
|
||||||
Get(url)
|
Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -47,14 +58,9 @@ func (a *Arlo) get(url string, result interface{}) error {
|
|||||||
|
|
||||||
func (a *Arlo) post(url string, body interface{}, result interface{}, xcloudId string) error {
|
func (a *Arlo) post(url string, body interface{}, result interface{}, xcloudId string) error {
|
||||||
var errorResponse ErrorResponse
|
var errorResponse ErrorResponse
|
||||||
request := a.client.R().
|
request := a.getBaseRequest(body, result, &errorResponse, xcloudId)
|
||||||
SetBody(body).
|
resp, err := request.
|
||||||
SetResult(result).
|
Post(url)
|
||||||
SetError(&errorResponse)
|
|
||||||
if xcloudId != "" {
|
|
||||||
request.SetHeader("xcloudId", xcloudId)
|
|
||||||
}
|
|
||||||
resp, err := request.Post(url)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -66,13 +72,8 @@ func (a *Arlo) post(url string, body interface{}, result interface{}, xcloudId s
|
|||||||
|
|
||||||
func (a *Arlo) put(url string, result interface{}, xcloudId string) error {
|
func (a *Arlo) put(url string, result interface{}, xcloudId string) error {
|
||||||
var errorResponse ErrorResponse
|
var errorResponse ErrorResponse
|
||||||
request := a.client.R().
|
resp, err := a.getBaseRequest(nil, result, &errorResponse, xcloudId).
|
||||||
SetResult(result).
|
Put(url)
|
||||||
SetError(&errorResponse)
|
|
||||||
if xcloudId != "" {
|
|
||||||
request.SetHeader("xcloudId", xcloudId)
|
|
||||||
}
|
|
||||||
resp, err := request.Put(url)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -94,11 +95,53 @@ func (a *Arlo) Login(ctx context.Context, user string, pass string) error {
|
|||||||
if !loginResponse.Success {
|
if !loginResponse.Success {
|
||||||
return fmt.Errorf("no success but no error")
|
return fmt.Errorf("no success but no error")
|
||||||
}
|
}
|
||||||
a.client.SetHeader("Authorization", loginResponse.Data.Token)
|
|
||||||
a.Account = loginResponse.Data
|
a.Account = loginResponse.Data
|
||||||
if _, err := a.GetDevices(ctx); err != nil {
|
a.client.SetHeader("Authorization", a.Account.Token)
|
||||||
return fmt.Errorf("getting devices: %v", err)
|
|
||||||
|
var response DeviceResponse
|
||||||
|
err = a.get(fmt.Sprintf(DevicesUri, time.Now().Format("20060102")), &response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
if !response.Success {
|
||||||
|
return fmt.Errorf("no success but no error")
|
||||||
|
}
|
||||||
|
if len(response.Data) == 0 {
|
||||||
|
return fmt.Errorf("no device found")
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Basestations = nil
|
||||||
|
a.Cameras = nil
|
||||||
|
for _, device := range response.Data {
|
||||||
|
if device.IsBasestation() {
|
||||||
|
a.Basestations = append(a.Basestations, NewBaseStation(a, device))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if device.IsCamera() {
|
||||||
|
a.Cameras = append(a.Cameras, NewCamera(a, device))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.Subscribe(ctx); err != nil {
|
||||||
|
return fmt.Errorf("subscribing to event stream: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(pingTime)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case _ = <-ticker.C:
|
||||||
|
if err := a.Ping(ctx); err != nil {
|
||||||
|
log.Errorf("Pingloop > error while pinging: %v > disconnect event stream", err)
|
||||||
|
_ = a.Disconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,56 +158,16 @@ func (a *Arlo) Logout() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Arlo) GetSession() (*Session, error) {
|
func (a *Arlo) GetSession() (Session, error) {
|
||||||
var response SessionResponse
|
var response SessionResponse
|
||||||
err := a.get(SessionUri, &response)
|
err := a.get(SessionUri, &response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return Session{}, err
|
||||||
}
|
}
|
||||||
if !response.Success {
|
if !response.Success {
|
||||||
return nil, fmt.Errorf("no success but no error")
|
return Session{}, fmt.Errorf("no success but no error")
|
||||||
}
|
}
|
||||||
return &response.Data, nil
|
return response.Data, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Arlo) GetDevices(ctx context.Context) (*Devices, error) {
|
|
||||||
var response DeviceResponse
|
|
||||||
err := a.get(fmt.Sprintf(DevicesUri, time.Now().Format("20060102")), &response)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !response.Success {
|
|
||||||
return nil, fmt.Errorf("no success but no error")
|
|
||||||
}
|
|
||||||
if len(response.Data) == 0 {
|
|
||||||
return nil, fmt.Errorf("no device found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache a pointer to the arlo object with each device.
|
|
||||||
for i := range response.Data {
|
|
||||||
response.Data[i].arlo = a
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disconnect all of the basestations from the EventStream.
|
|
||||||
for _, basestation := range a.Basestations {
|
|
||||||
if err := basestation.Disconnect(); err != nil {
|
|
||||||
return nil, fmt.Errorf("disconnecting device %s: %v", basestation.DeviceName, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a.rwmutex.Lock()
|
|
||||||
// Cache the devices as their respective types.
|
|
||||||
a.Cameras = response.Data.GetCameras()
|
|
||||||
a.Basestations = response.Data.GetBasestations()
|
|
||||||
a.rwmutex.Unlock()
|
|
||||||
|
|
||||||
// subscribe each basestation to the EventStream.
|
|
||||||
for _, basestation := range a.Basestations {
|
|
||||||
if err := basestation.Subscribe(ctx); err != nil {
|
|
||||||
return nil, fmt.Errorf("subscribing device %s: %v", basestation.DeviceName, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &response.Data, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProfile returns the user profile for the currently logged in user.
|
// GetProfile returns the user profile for the currently logged in user.
|
||||||
@ -179,3 +182,171 @@ func (a *Arlo) GetProfile() (*UserProfile, error) {
|
|||||||
}
|
}
|
||||||
return &response.Data, nil
|
return &response.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Arlo) GetBaseStation(deviceId string) *Basestation {
|
||||||
|
for _, basestation := range a.Basestations {
|
||||||
|
if basestation.DeviceId == deviceId {
|
||||||
|
return basestation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Arlo) makeEventStreamRequest(ctx context.Context, payload EventStreamPayload, xcloudid string) (*EventStreamResponse, error) {
|
||||||
|
|
||||||
|
if !a.IsConnected() {
|
||||||
|
log.Infof("event stream not connected: reconnecting")
|
||||||
|
err := a.Subscribe(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reconnecting to event stream: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transId := genTransId()
|
||||||
|
payload.TransId = transId
|
||||||
|
|
||||||
|
responseChan := a.eventStream.subscribeTransaction(transId)
|
||||||
|
|
||||||
|
// Send the payload to the event stream.
|
||||||
|
if err := a.NotifyEventStream(payload, xcloudid); err != nil {
|
||||||
|
return nil, fmt.Errorf("notifying event stream: %v", err)
|
||||||
|
}
|
||||||
|
timer := time.NewTimer(eventStreamTimeout)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case response := <-responseChan:
|
||||||
|
return response, nil
|
||||||
|
case err := <-a.eventStream.Error:
|
||||||
|
return nil, fmt.Errorf("event stream error: %v", err)
|
||||||
|
case <-a.eventStream.disconnectedChan:
|
||||||
|
log.Warn("event stream was closed before response was read")
|
||||||
|
return a.makeEventStreamRequest(ctx, payload, xcloudid)
|
||||||
|
case <-timer.C:
|
||||||
|
return nil, fmt.Errorf("event stream response timed out after %.0f second", eventStreamTimeout.Seconds())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Arlo) IsConnected() bool {
|
||||||
|
select {
|
||||||
|
case <-a.eventStream.disconnectedChan:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Arlo) Subscribe(ctx context.Context) error {
|
||||||
|
a.eventStream = newEventStream(
|
||||||
|
BaseUrl+fmt.Sprintf(NotifyResponsesPushServiceUri),
|
||||||
|
&http.Client{Jar: a.client.GetClient().Jar},
|
||||||
|
a.client.Header.Get("Authorization"),
|
||||||
|
)
|
||||||
|
|
||||||
|
err := a.eventStream.listen(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting up event stream: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedloop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fmt.Errorf("failed to subscribe to the event stream: requesting shutdown")
|
||||||
|
case <-a.eventStream.disconnectedChan:
|
||||||
|
return fmt.Errorf("failed to subscribe to the event stream: disconnected")
|
||||||
|
default:
|
||||||
|
if a.eventStream.GetIsConnected() {
|
||||||
|
break connectedloop
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.Ping(ctx); err != nil {
|
||||||
|
_ = a.Disconnect()
|
||||||
|
return fmt.Errorf("pingloop > error while pinging: %v > disconnect event stream", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, camera := range a.Cameras {
|
||||||
|
camera.subscribeToStateUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Arlo) Unsubscribe(xcloudid string) error {
|
||||||
|
var response BaseResponse
|
||||||
|
err := a.put(UnsubscribeUri, &response, xcloudid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !response.Success {
|
||||||
|
return fmt.Errorf("no success but no error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Arlo) Disconnect() error {
|
||||||
|
// disconnect channel to stop event stream.
|
||||||
|
if a.eventStream != nil {
|
||||||
|
a.eventStream.disconnect()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping makes a call to the subscriptions endpoint. The Arlo event stream requires this message to be sent every 30s.
|
||||||
|
|
||||||
|
func (a *Arlo) NotifyEventStream(payload EventStreamPayload, xcloudId string) error {
|
||||||
|
var response ErrorResponse
|
||||||
|
err := a.post(fmt.Sprintf(NotifyUri, payload.To), payload, &response, xcloudId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !response.Success {
|
||||||
|
if response.Reason != "" {
|
||||||
|
return fmt.Errorf(response.Reason)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("no success but no error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Arlo) makeRequest(ctx context.Context, deviceId string, xcloudid string, action string, resource string, publishResponse bool, properties interface{}, result interface{}) error {
|
||||||
|
payload := EventStreamPayload{
|
||||||
|
Action: action,
|
||||||
|
Resource: resource,
|
||||||
|
PublishResponse: publishResponse,
|
||||||
|
Properties: properties,
|
||||||
|
From: fmt.Sprintf("%s_%s", a.Account.UserId, TransIdPrefix),
|
||||||
|
To: deviceId,
|
||||||
|
}
|
||||||
|
resp, err := a.makeEventStreamRequest(ctx, payload, xcloudid)
|
||||||
|
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 (a *Arlo) Ping(ctx context.Context) error {
|
||||||
|
var devices []string
|
||||||
|
var xcloudid string
|
||||||
|
for _, basestation := range a.Basestations {
|
||||||
|
devices = append(devices, basestation.DeviceId)
|
||||||
|
xcloudid = basestation.XCloudId
|
||||||
|
}
|
||||||
|
if len(devices) == 0 {
|
||||||
|
return fmt.Errorf("no basestation registered")
|
||||||
|
}
|
||||||
|
err := a.makeRequest(ctx, devices[0], xcloudid, "set", fmt.Sprintf("subscriptions/%s_%s", a.Account.UserId, TransIdPrefix), false, map[string][]string{"devices": devices}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
230
basestation.go
230
basestation.go
@ -2,12 +2,8 @@ package arlo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const eventStreamTimeout = 30 * time.Second
|
const eventStreamTimeout = 30 * time.Second
|
||||||
@ -16,45 +12,8 @@ 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.).
|
||||||
// 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 Basestation struct {
|
type Basestation struct {
|
||||||
|
arlo *Arlo
|
||||||
Device
|
Device
|
||||||
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 {
|
type GetModesResponse struct {
|
||||||
@ -100,177 +59,18 @@ type CalendarMode struct {
|
|||||||
} `json:"schedule"`
|
} `json:"schedule"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basestations is a slice of Basestation objects.
|
func NewBaseStation(arlo *Arlo, device Device) *Basestation {
|
||||||
type Basestations []*Basestation
|
return &Basestation{
|
||||||
|
arlo: arlo,
|
||||||
// Find returns a basestation with the device id passed in.
|
Device: device,
|
||||||
func (bs *Basestations) Find(deviceId string) *Basestation {
|
|
||||||
for _, b := range *bs {
|
|
||||||
if b.DeviceId == deviceId {
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeEventStreamRequest is a helper function sets up a response channel, sends a message to the event stream, and blocks waiting for the response.
|
// 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(ctx context.Context, payload EventStreamPayload) (*EventStreamResponse, error) {
|
|
||||||
|
|
||||||
if !b.IsConnected() {
|
|
||||||
log.Infof("event stream not connected: reconnecting")
|
|
||||||
err := b.Subscribe(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("reconnecting to event stream: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transId := genTransId()
|
|
||||||
payload.TransId = transId
|
|
||||||
|
|
||||||
responseChan := b.eventStream.subscribeTransaction(transId)
|
|
||||||
|
|
||||||
// Send the payload to the event stream.
|
|
||||||
if err := b.NotifyEventStream(payload); err != nil {
|
|
||||||
return nil, fmt.Errorf("notifying event stream: %v", err)
|
|
||||||
}
|
|
||||||
timer := time.NewTimer(eventStreamTimeout)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case response := <-responseChan:
|
|
||||||
return response, nil
|
|
||||||
case err := <-b.eventStream.Error:
|
|
||||||
return nil, fmt.Errorf("event stream error: %v", err)
|
|
||||||
case <-b.eventStream.disconnectedChan:
|
|
||||||
log.Warn("event stream was closed before response was read")
|
|
||||||
return b.makeEventStreamRequest(ctx, payload)
|
|
||||||
case <-timer.C:
|
|
||||||
return nil, fmt.Errorf("event stream response timed out after %.0f second", eventStreamTimeout.Seconds())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Basestation) IsConnected() bool {
|
|
||||||
select {
|
|
||||||
case <-b.eventStream.disconnectedChan:
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Basestation) Subscribe(ctx context.Context) error {
|
|
||||||
b.eventStream = newEventStream(
|
|
||||||
BaseUrl+fmt.Sprintf(NotifyResponsesPushServiceUri, b.arlo.Account.Token),
|
|
||||||
&http.Client{Jar: b.arlo.client.GetClient().Jar})
|
|
||||||
|
|
||||||
connectedChan, err := b.eventStream.listen(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("setting up event stream: %v", err)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return fmt.Errorf("failed to subscribe to the event stream: requesting shutdown")
|
|
||||||
case connected := <-connectedChan:
|
|
||||||
if !connected {
|
|
||||||
return fmt.Errorf("failed to subscribe to the event stream")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.Ping(ctx); err != nil {
|
|
||||||
_ = b.Disconnect()
|
|
||||||
return fmt.Errorf("Pingloop > error while pinging: %v > disconnect event stream", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Arlo event stream requires a "ping" every 30s.
|
|
||||||
go func(ctx context.Context) {
|
|
||||||
ticker := time.NewTicker(pingTime)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case _ = <-ticker.C:
|
|
||||||
if err := b.Ping(ctx); err != nil {
|
|
||||||
log.Errorf("Pingloop > error while pinging: %v > disconnect event stream", err)
|
|
||||||
_ = b.Disconnect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(ctx)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Basestation) Unsubscribe() error {
|
|
||||||
var response BaseResponse
|
|
||||||
err := b.arlo.put(UnsubscribeUri, &response, b.XCloudId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !response.Success {
|
|
||||||
return fmt.Errorf("no success but no error")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Basestation) Disconnect() error {
|
|
||||||
// disconnect channel to stop event stream.
|
|
||||||
if b.eventStream != nil {
|
|
||||||
b.eventStream.disconnect()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping makes a call to the subscriptions endpoint. The Arlo event stream requires this message to be sent every 30s.
|
|
||||||
|
|
||||||
func (b *Basestation) NotifyEventStream(payload EventStreamPayload) error {
|
|
||||||
var response ErrorResponse
|
|
||||||
err := b.arlo.post(fmt.Sprintf(NotifyUri, b.DeviceId), payload, &response, b.XCloudId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !response.Success {
|
|
||||||
if response.Reason != "" {
|
|
||||||
return fmt.Errorf(response.Reason)
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("no success but no error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Basestation) makeRequest(ctx context.Context, action string, resource string, publishResponse bool, properties interface{}, result interface{}) error {
|
|
||||||
payload := EventStreamPayload{
|
|
||||||
Action: action,
|
|
||||||
Resource: resource,
|
|
||||||
PublishResponse: publishResponse,
|
|
||||||
Properties: properties,
|
|
||||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
|
||||||
To: b.DeviceId,
|
|
||||||
}
|
|
||||||
resp, err := b.makeEventStreamRequest(ctx, payload)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("making event stream request: %v", err)
|
|
||||||
}
|
|
||||||
if result != nil {
|
|
||||||
err = json.Unmarshal(resp.RawProperties, result)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unmarshalling properties: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Basestation) Ping(ctx context.Context) error {
|
|
||||||
err := b.makeRequest(ctx, "set", fmt.Sprintf("subscriptions/%s_%s", b.UserId, TransIdPrefix), false, map[string][1]string{"devices": {b.DeviceId}}, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Basestation) GetState(ctx context.Context) (*BaseStationState, error) {
|
func (b *Basestation) GetState(ctx context.Context) (*BaseStationState, error) {
|
||||||
var state BaseStationState
|
var state BaseStationState
|
||||||
err := b.makeRequest(ctx, "get", "basestation", false, nil, &state)
|
err := b.arlo.makeRequest(ctx, b.DeviceId, b.XCloudId, "get", "basestation", false, nil, &state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting basestation %s state: %v", b.DeviceName, err)
|
return nil, fmt.Errorf("getting basestation %s state: %v", b.DeviceName, err)
|
||||||
}
|
}
|
||||||
@ -279,7 +79,7 @@ func (b *Basestation) GetState(ctx context.Context) (*BaseStationState, error) {
|
|||||||
|
|
||||||
func (b *Basestation) GetAllCameraState(ctx context.Context) ([]CameraState, error) {
|
func (b *Basestation) GetAllCameraState(ctx context.Context) ([]CameraState, error) {
|
||||||
var states []CameraState
|
var states []CameraState
|
||||||
err := b.makeRequest(ctx, "get", "cameras", false, nil, &states)
|
err := b.arlo.makeRequest(ctx, b.DeviceId, b.XCloudId, "get", "cameras", false, nil, &states)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting associated cameras state: %v", err)
|
return nil, fmt.Errorf("getting associated cameras state: %v", err)
|
||||||
}
|
}
|
||||||
@ -288,7 +88,7 @@ func (b *Basestation) GetAllCameraState(ctx context.Context) ([]CameraState, err
|
|||||||
|
|
||||||
func (b *Basestation) GetRules(ctx context.Context) ([]Rule, error) {
|
func (b *Basestation) GetRules(ctx context.Context) ([]Rule, error) {
|
||||||
var resp GetRulesResponse
|
var resp GetRulesResponse
|
||||||
err := b.makeRequest(ctx, "get", "rules", false, nil, &resp)
|
err := b.arlo.makeRequest(ctx, b.DeviceId, b.XCloudId, "get", "rules", false, nil, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting rules: %v", err)
|
return nil, fmt.Errorf("getting rules: %v", err)
|
||||||
}
|
}
|
||||||
@ -297,7 +97,7 @@ func (b *Basestation) GetRules(ctx context.Context) ([]Rule, error) {
|
|||||||
|
|
||||||
func (b *Basestation) GetCalendarMode(ctx context.Context) (*CalendarMode, error) {
|
func (b *Basestation) GetCalendarMode(ctx context.Context) (*CalendarMode, error) {
|
||||||
var calendarMode CalendarMode
|
var calendarMode CalendarMode
|
||||||
err := b.makeRequest(ctx, "get", "schedule", false, nil, &calendarMode)
|
err := b.arlo.makeRequest(ctx, b.DeviceId, b.XCloudId, "get", "schedule", false, nil, &calendarMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting calendar mode: %v", err)
|
return nil, fmt.Errorf("getting calendar mode: %v", err)
|
||||||
}
|
}
|
||||||
@ -309,7 +109,7 @@ func (b *Basestation) GetCalendarMode(ctx context.Context) (*CalendarMode, error
|
|||||||
// 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(ctx context.Context, active bool) error {
|
func (b *Basestation) SetCalendarMode(ctx context.Context, active bool) error {
|
||||||
resp := make(map[string]bool)
|
resp := make(map[string]bool)
|
||||||
err := b.makeRequest(ctx, "set", "schedule", true, struct {
|
err := b.arlo.makeRequest(ctx, b.DeviceId, b.XCloudId, "set", "schedule", true, struct {
|
||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
}{
|
}{
|
||||||
Active: active,
|
Active: active,
|
||||||
@ -329,7 +129,7 @@ func (b *Basestation) SetCalendarMode(ctx context.Context, active bool) error {
|
|||||||
|
|
||||||
func (b *Basestation) GetModes(ctx context.Context) (*GetModesResponse, error) {
|
func (b *Basestation) GetModes(ctx context.Context) (*GetModesResponse, error) {
|
||||||
var resp GetModesResponse
|
var resp GetModesResponse
|
||||||
err := b.makeRequest(ctx, "get", "modes", false, nil, &resp)
|
err := b.arlo.makeRequest(ctx, b.DeviceId, b.XCloudId, "get", "modes", false, nil, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting modes: %v", err)
|
return nil, fmt.Errorf("getting modes: %v", err)
|
||||||
}
|
}
|
||||||
@ -338,7 +138,7 @@ func (b *Basestation) GetModes(ctx context.Context) (*GetModesResponse, error) {
|
|||||||
|
|
||||||
func (b *Basestation) SetCustomMode(ctx context.Context, mode string) error {
|
func (b *Basestation) SetCustomMode(ctx context.Context, mode string) error {
|
||||||
resp := make(map[string]string)
|
resp := make(map[string]string)
|
||||||
err := b.makeRequest(ctx, "set", "modes", true, struct {
|
err := b.arlo.makeRequest(ctx, b.DeviceId, b.XCloudId, "set", "modes", true, struct {
|
||||||
Active string `json:"active"`
|
Active string `json:"active"`
|
||||||
}{
|
}{
|
||||||
Active: mode,
|
Active: mode,
|
||||||
@ -357,7 +157,7 @@ func (b *Basestation) SetCustomMode(ctx context.Context, mode string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) DeleteMode(ctx context.Context, mode string) error {
|
func (b *Basestation) DeleteMode(ctx context.Context, mode string) error {
|
||||||
err := b.makeRequest(ctx, "delete", fmt.Sprintf("modes/%s", mode), true, nil, nil)
|
err := b.arlo.makeRequest(ctx, b.DeviceId, b.XCloudId, "delete", fmt.Sprintf("modes/%s", mode), true, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("deleting mode %s: %v", mode, err)
|
return fmt.Errorf("deleting mode %s: %v", mode, err)
|
||||||
}
|
}
|
||||||
@ -389,7 +189,7 @@ type SetSirenResponse struct {
|
|||||||
|
|
||||||
func (b *Basestation) SirenOn(ctx context.Context) error {
|
func (b *Basestation) SirenOn(ctx context.Context) error {
|
||||||
var response SetSirenResponse
|
var response SetSirenResponse
|
||||||
err := b.makeRequest(ctx, "set", "siren", true, SirenProperties{
|
err := b.arlo.makeRequest(ctx, b.DeviceId, b.XCloudId, "set", "siren", true, SirenProperties{
|
||||||
SirenState: "on",
|
SirenState: "on",
|
||||||
Duration: 300,
|
Duration: 300,
|
||||||
Volume: 8,
|
Volume: 8,
|
||||||
@ -406,7 +206,7 @@ func (b *Basestation) SirenOn(ctx context.Context) error {
|
|||||||
|
|
||||||
func (b *Basestation) SirenOff(ctx context.Context) error {
|
func (b *Basestation) SirenOff(ctx context.Context) error {
|
||||||
var response SetSirenResponse
|
var response SetSirenResponse
|
||||||
err := b.makeRequest(ctx, "set", "siren", true, SirenProperties{
|
err := b.arlo.makeRequest(ctx, b.DeviceId, b.XCloudId, "set", "siren", true, SirenProperties{
|
||||||
SirenState: "off",
|
SirenState: "off",
|
||||||
Duration: 300,
|
Duration: 300,
|
||||||
Volume: 8,
|
Volume: 8,
|
||||||
|
38
basestation_state.go
Normal file
38
basestation_state.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package arlo
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
137
camera.go
137
camera.go
@ -4,13 +4,22 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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 struct {
|
||||||
|
arlo *Arlo
|
||||||
|
Device
|
||||||
|
CameraState
|
||||||
|
stateMutex sync.RWMutex
|
||||||
|
|
||||||
|
motionSubscribers []chan bool
|
||||||
|
motionSubscribersMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
type CameraState struct {
|
type CameraState struct {
|
||||||
InterfaceVersion int `json:"interfaceVersion"`
|
InterfaceVersion int `json:"interfaceVersion"`
|
||||||
@ -80,27 +89,15 @@ type CameraState struct {
|
|||||||
BestLocalLiveStreaming string `json:"bestLocalLiveStreaming"`
|
BestLocalLiveStreaming string `json:"bestLocalLiveStreaming"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cameras is a slice of Camera objects.
|
func NewCamera(arlo *Arlo, device Device) *Camera {
|
||||||
type Cameras []*Camera
|
return &Camera{
|
||||||
|
arlo: arlo,
|
||||||
// Find returns a camera with the device id passed in.
|
Device: device,
|
||||||
func (cs *Cameras) Find(deviceId string) *Camera {
|
|
||||||
for _, c := range *cs {
|
|
||||||
if c.DeviceId == deviceId {
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Camera) On(ctx context.Context) error {
|
func (c *Camera) On(ctx context.Context) error {
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
err := c.arlo.makeRequest(ctx, c.DeviceId, c.XCloudId, "set", fmt.Sprintf("cameras/%s", c.DeviceId), true, CameraProperties{
|
||||||
if b == nil {
|
|
||||||
return fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := b.makeRequest(ctx, "set", fmt.Sprintf("cameras/%s", c.DeviceId), true, CameraProperties{
|
|
||||||
PrivacyActive: false,
|
PrivacyActive: false,
|
||||||
}, nil)
|
}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -119,14 +116,9 @@ func (c *Camera) Off(ctx context.Context) (response *EventStreamResponse, err er
|
|||||||
PrivacyActive: true,
|
PrivacyActive: true,
|
||||||
},
|
},
|
||||||
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
||||||
To: c.ParentId,
|
To: c.DeviceId,
|
||||||
}
|
}
|
||||||
|
return c.arlo.makeEventStreamRequest(ctx, payload, c.XCloudId)
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
|
||||||
if b == nil {
|
|
||||||
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
|
||||||
}
|
|
||||||
return b.makeEventStreamRequest(ctx, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBrightness sets the camera brightness.
|
// SetBrightness sets the camera brightness.
|
||||||
@ -148,21 +140,14 @@ func (c *Camera) SetBrightness(ctx context.Context, brightness int) (response *E
|
|||||||
Brightness: brightness,
|
Brightness: brightness,
|
||||||
},
|
},
|
||||||
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
||||||
To: c.ParentId,
|
To: c.DeviceId,
|
||||||
}
|
}
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
|
||||||
if b == nil {
|
return c.arlo.makeEventStreamRequest(ctx, payload, c.XCloudId)
|
||||||
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
|
||||||
}
|
|
||||||
return b.makeEventStreamRequest(ctx, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Camera) EnableMotionAlerts(ctx context.Context, sensitivity int, zones []string) error {
|
func (c *Camera) EnableMotionAlerts(ctx context.Context, sensitivity int, zones []string) error {
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
err := c.arlo.makeRequest(ctx, c.DeviceId, c.XCloudId, "set", fmt.Sprintf("cameras/%s", c.DeviceId), true, MotionDetectionProperties{
|
||||||
if b == nil {
|
|
||||||
return fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
|
||||||
}
|
|
||||||
err := b.makeRequest(ctx, "set", fmt.Sprintf("cameras/%s", c.DeviceId), true, MotionDetectionProperties{
|
|
||||||
BaseDetectionProperties: BaseDetectionProperties{
|
BaseDetectionProperties: BaseDetectionProperties{
|
||||||
Armed: true,
|
Armed: true,
|
||||||
Sensitivity: sensitivity,
|
Sensitivity: sensitivity,
|
||||||
@ -189,14 +174,10 @@ func (c *Camera) DisableMotionAlerts(ctx context.Context, sensitivity int, zones
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
||||||
To: c.ParentId,
|
To: c.DeviceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
return c.arlo.makeEventStreamRequest(ctx, payload, c.XCloudId)
|
||||||
if b == nil {
|
|
||||||
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
|
||||||
}
|
|
||||||
return b.makeEventStreamRequest(ctx, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Camera) EnableAudioAlerts(ctx context.Context, sensitivity int) (response *EventStreamResponse, err error) {
|
func (c *Camera) EnableAudioAlerts(ctx context.Context, sensitivity int) (response *EventStreamResponse, err error) {
|
||||||
@ -213,12 +194,7 @@ func (c *Camera) EnableAudioAlerts(ctx context.Context, sensitivity int) (respon
|
|||||||
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
||||||
To: c.ParentId,
|
To: c.ParentId,
|
||||||
}
|
}
|
||||||
|
return c.arlo.makeEventStreamRequest(ctx, payload, c.XCloudId)
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
|
||||||
if b == nil {
|
|
||||||
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
|
||||||
}
|
|
||||||
return b.makeEventStreamRequest(ctx, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Camera) DisableAudioAlerts(ctx context.Context, sensitivity int) (response *EventStreamResponse, err error) {
|
func (c *Camera) DisableAudioAlerts(ctx context.Context, sensitivity int) (response *EventStreamResponse, err error) {
|
||||||
@ -233,14 +209,10 @@ func (c *Camera) DisableAudioAlerts(ctx context.Context, sensitivity int) (respo
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
||||||
To: c.ParentId,
|
To: c.DeviceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
return c.arlo.makeEventStreamRequest(ctx, payload, c.XCloudId)
|
||||||
if b == nil {
|
|
||||||
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
|
||||||
}
|
|
||||||
return b.makeEventStreamRequest(ctx, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// action: disabled OR recordSnapshot OR recordVideo
|
// action: disabled OR recordSnapshot OR recordVideo
|
||||||
@ -262,34 +234,53 @@ func (c *Camera) SetAlertNotificationMethods(ctx context.Context, action string,
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
||||||
To: c.ParentId,
|
To: c.DeviceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
return c.arlo.makeEventStreamRequest(ctx, payload, c.XCloudId)
|
||||||
if b == nil {
|
|
||||||
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
|
||||||
}
|
|
||||||
return b.makeEventStreamRequest(ctx, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Camera) SubscribeToMotionDetection(ctx context.Context) (chan bool, error) {
|
func (c *Camera) subscribeToStateUpdate() {
|
||||||
b := c.arlo.Basestations.Find(c.ParentId)
|
|
||||||
if b == nil {
|
|
||||||
return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
|
||||||
}
|
|
||||||
out := make(chan bool)
|
|
||||||
respChan := b.eventStream.subscribeResource(fmt.Sprintf("cameras/%s", c.DeviceId))
|
|
||||||
go func() {
|
go func() {
|
||||||
|
respChan := c.arlo.eventStream.subscribeResource(fmt.Sprintf("cameras/%s", c.DeviceId))
|
||||||
for msg := range respChan {
|
for msg := range respChan {
|
||||||
var state CameraState
|
c.stateMutex.Lock()
|
||||||
err := json.Unmarshal(msg.RawProperties, &state)
|
old := *c
|
||||||
|
err := json.Unmarshal(msg.RawProperties, &c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("unmarshalling properties: %v", err)
|
log.Errorf("camera state update > unmarshalling properties: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out <- state.MotionDetected
|
c.stateMutex.Unlock()
|
||||||
|
c.processStateChanges(old, *c)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
}
|
||||||
return out, nil
|
|
||||||
|
func (c *Camera) RefreshState(ctx context.Context) error {
|
||||||
|
old := *c
|
||||||
|
err := c.arlo.makeRequest(ctx, c.DeviceId, c.XCloudId, "get", fmt.Sprintf("cameras/%s", c.DeviceId), false, nil, &c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.processStateChanges(old, *c)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Camera) processStateChanges(old Camera, new Camera) {
|
||||||
|
if old.MotionDetected != new.MotionDetected {
|
||||||
|
c.motionSubscribersMutex.RLock()
|
||||||
|
for _, subscriber := range c.motionSubscribers {
|
||||||
|
subscriber <- new.MotionDetected
|
||||||
|
}
|
||||||
|
c.motionSubscribersMutex.RUnlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Camera) SubscribeToMotion() chan bool {
|
||||||
|
c.motionSubscribersMutex.Lock()
|
||||||
|
defer c.motionSubscribersMutex.Unlock()
|
||||||
|
out := make(chan bool)
|
||||||
|
c.motionSubscribers = append(c.motionSubscribers, out)
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
14
cmd/main.go
14
cmd/main.go
@ -15,14 +15,16 @@ func main() {
|
|||||||
log.Errorf("login: %v", err)
|
log.Errorf("login: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
for _, device := range a.Cameras {
|
||||||
|
err := device.RefreshState(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, device := range a.Cameras {
|
for _, device := range a.Cameras {
|
||||||
if device.DeviceName == "Salon" {
|
if device.DeviceName == "Salon" {
|
||||||
motionChan, err := device.SubscribeToMotionDetection(ctx)
|
motionChan := device.SubscribeToMotion()
|
||||||
if err != nil {
|
|
||||||
log.Errorf("subscribing to motion: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for b := range motionChan {
|
for b := range motionChan {
|
||||||
log.Infof("motion salon %t", b)
|
log.Infof("motion salon %t", b)
|
||||||
|
|
||||||
@ -30,6 +32,4 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
select {}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
2
const.go
2
const.go
@ -75,7 +75,7 @@ const (
|
|||||||
MigrateZonesUri = "/users/devices/%uniqueId/activityzones/migrate"
|
MigrateZonesUri = "/users/devices/%uniqueId/activityzones/migrate"
|
||||||
MobileOffersUri = "/users/payment/offers/dataplans/v5"
|
MobileOffersUri = "/users/payment/offers/dataplans/v5"
|
||||||
ModifyBillingUri = "/users/payment/billing/%paymentId"
|
ModifyBillingUri = "/users/payment/billing/%paymentId"
|
||||||
NotifyResponsesPushServiceUri = "/client/subscribe?token=%s"
|
NotifyResponsesPushServiceUri = "/client/subscribe"
|
||||||
NotifyUri = "/users/devices/notify/%s"
|
NotifyUri = "/users/devices/notify/%s"
|
||||||
OffersDetailsUri = "/users/payment/offersdetail"
|
OffersDetailsUri = "/users/payment/offersdetail"
|
||||||
OffersDvrChangeUri = "/users/payment/offers/arloq/html/v5/change"
|
OffersDvrChangeUri = "/users/payment/offers/arloq/html/v5/change"
|
||||||
|
76
devices.go
76
devices.go
@ -2,7 +2,6 @@ package arlo
|
|||||||
|
|
||||||
// A Device is the device data, this can be a camera, basestation, arloq, etc.
|
// A Device is the device data, this can be a camera, basestation, arloq, etc.
|
||||||
type Device struct {
|
type Device struct {
|
||||||
arlo *Arlo // Let's hold a reference to the parent arlo object since it holds the http.Client object and references to all devices.
|
|
||||||
AnalyticsEnabled bool `json:"analyticsEnabled"`
|
AnalyticsEnabled bool `json:"analyticsEnabled"`
|
||||||
ArloMobilePlan bool `json:"arloMobilePlan"`
|
ArloMobilePlan bool `json:"arloMobilePlan"`
|
||||||
ArloMobilePlanId string `json:"arloMobilePlanId"`
|
ArloMobilePlanId string `json:"arloMobilePlanId"`
|
||||||
@ -42,45 +41,6 @@ type Device struct {
|
|||||||
XCloudId string `json:"xCloudId"`
|
XCloudId string `json:"xCloudId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Devices is a slice of Device objects.
|
|
||||||
type Devices []*Device
|
|
||||||
|
|
||||||
// A DeviceOrder holds a map of device ids and a numeric index. The numeric index is the device order.
|
|
||||||
// Device order is mainly used by the UI to determine which order to show the devices.
|
|
||||||
/*
|
|
||||||
{
|
|
||||||
"devices":{
|
|
||||||
"XXXXXXXXXXXXX":1,
|
|
||||||
"XXXXXXXXXXXXX":2,
|
|
||||||
"XXXXXXXXXXXXX":3
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
type DeviceOrder struct {
|
|
||||||
Devices map[string]int `json:"devices"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find returns a device with the device id passed in.
|
|
||||||
func (ds Devices) Find(deviceId string) *Device {
|
|
||||||
for _, d := range ds {
|
|
||||||
if d.DeviceId == deviceId {
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ds Devices) FindCameras(basestationId string) Cameras {
|
|
||||||
cs := Cameras{}
|
|
||||||
for _, d := range ds {
|
|
||||||
if d.ParentId == basestationId {
|
|
||||||
cam := Camera(*d)
|
|
||||||
cs = append(cs, &cam)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d Device) IsBasestation() bool {
|
func (d Device) IsBasestation() bool {
|
||||||
return d.DeviceType == DeviceTypeBasestation
|
return d.DeviceType == DeviceTypeBasestation
|
||||||
}
|
}
|
||||||
@ -106,39 +66,3 @@ func (d Device) IsLight() bool {
|
|||||||
func (d Device) IsSiren() bool {
|
func (d Device) IsSiren() bool {
|
||||||
return d.DeviceType == DeviceTypeSiren
|
return d.DeviceType == DeviceTypeSiren
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBasestations returns a Basestations object containing all devices that are NOT type "camera".
|
|
||||||
// I did this because some device types, like arloq, don't have a basestation.
|
|
||||||
// So, when interacting with them you must treat them like a basestation and a camera.
|
|
||||||
// Cameras also includes devices of this type, so you can get the same data there or cast.
|
|
||||||
func (ds Devices) GetBasestations() Basestations {
|
|
||||||
var basestations Basestations
|
|
||||||
for _, d := range ds {
|
|
||||||
if d.IsBasestation() {
|
|
||||||
basestations = append(basestations, &Basestation{Device: *d})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return basestations
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCameras returns a Cameras object containing all devices that are of type "camera".
|
|
||||||
// I did this because some device types, like arloq, don't have a basestation.
|
|
||||||
// So, when interacting with them you must treat them like a basestation and a camera.
|
|
||||||
// Basestations also includes devices of this type, so you can get the same data there or cast.
|
|
||||||
func (ds Devices) GetCameras() Cameras {
|
|
||||||
var cameras Cameras
|
|
||||||
for _, d := range ds {
|
|
||||||
if d.IsCamera() {
|
|
||||||
cam := Camera(*d)
|
|
||||||
cameras = append(cameras, &cam)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cameras
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateDeviceName sets the name of the given device to the name argument.
|
|
||||||
//func (d *Device) UpdateDeviceName(name string) error {
|
|
||||||
// body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId}
|
|
||||||
// resp, err := d.arlo.put(RenameDeviceUri, d.XCloudId, body, nil)
|
|
||||||
// return checkRequest(resp, err, "failed to update device name")
|
|
||||||
//}
|
|
||||||
|
@ -20,6 +20,9 @@ type eventStream struct {
|
|||||||
disconnectedChan chan interface{}
|
disconnectedChan chan interface{}
|
||||||
once *sync.Once
|
once *sync.Once
|
||||||
|
|
||||||
|
isConnected bool
|
||||||
|
isConnectedMutex sync.RWMutex
|
||||||
|
|
||||||
transactionSubscribers map[string]chan *EventStreamResponse
|
transactionSubscribers map[string]chan *EventStreamResponse
|
||||||
transactionSubscribersMutex sync.RWMutex
|
transactionSubscribersMutex sync.RWMutex
|
||||||
|
|
||||||
@ -27,7 +30,7 @@ type eventStream struct {
|
|||||||
resourceSubscribersMutex sync.RWMutex
|
resourceSubscribersMutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEventStream(url string, client *http.Client) *eventStream {
|
func newEventStream(url string, client *http.Client, authHeader string) *eventStream {
|
||||||
e := &eventStream{
|
e := &eventStream{
|
||||||
Events: make(chan *sse.Event),
|
Events: make(chan *sse.Event),
|
||||||
transactionSubscribers: make(map[string]chan *EventStreamResponse),
|
transactionSubscribers: make(map[string]chan *EventStreamResponse),
|
||||||
@ -36,10 +39,13 @@ func newEventStream(url string, client *http.Client) *eventStream {
|
|||||||
resourceSubscribersMutex: sync.RWMutex{},
|
resourceSubscribersMutex: sync.RWMutex{},
|
||||||
disconnectedChan: make(chan interface{}),
|
disconnectedChan: make(chan interface{}),
|
||||||
once: new(sync.Once),
|
once: new(sync.Once),
|
||||||
|
isConnected: false,
|
||||||
|
isConnectedMutex: sync.RWMutex{},
|
||||||
}
|
}
|
||||||
|
|
||||||
SSEClient := sse.NewClient(url)
|
SSEClient := sse.NewClient(url)
|
||||||
SSEClient.Connection = client
|
SSEClient.Connection = client
|
||||||
|
SSEClient.Headers["Authorization"] = authHeader
|
||||||
SSEClient.OnDisconnect(func(c *sse.Client) {
|
SSEClient.OnDisconnect(func(c *sse.Client) {
|
||||||
e.disconnect()
|
e.disconnect()
|
||||||
})
|
})
|
||||||
@ -49,23 +55,24 @@ func newEventStream(url string, client *http.Client) *eventStream {
|
|||||||
|
|
||||||
func (e *eventStream) disconnect() {
|
func (e *eventStream) disconnect() {
|
||||||
e.once.Do(func() {
|
e.once.Do(func() {
|
||||||
|
log.Info("disconnect > close disconnectedChan")
|
||||||
close(e.disconnectedChan)
|
close(e.disconnectedChan)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *eventStream) listen(ctx context.Context) (chan bool, error) {
|
func (e *eventStream) listen(ctx context.Context) error {
|
||||||
|
log.Info("eventStream.listen > start")
|
||||||
|
|
||||||
err := e.SSEClient.SubscribeChanRaw(e.Events)
|
err := e.SSEClient.SubscribeChanRaw(e.Events)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to subscribe to seeclient")
|
return fmt.Errorf("failed to subscribe to seeclient")
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedChan := make(chan bool)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
e.clearResourceSubscriptions()
|
e.clearResourceSubscriptions()
|
||||||
e.clearTransactionSubscriptions()
|
e.clearTransactionSubscriptions()
|
||||||
|
log.Info("eventStream.listen > stop")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@ -99,7 +106,7 @@ func (e *eventStream) listen(ctx context.Context) (chan bool, error) {
|
|||||||
notifyResponse.RawProperties = bytesProperties
|
notifyResponse.RawProperties = bytesProperties
|
||||||
|
|
||||||
if notifyResponse.Status == "connected" {
|
if notifyResponse.Status == "connected" {
|
||||||
connectedChan <- true
|
e.setIsConnected(true)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if notifyResponse.Status == "disconnected" {
|
if notifyResponse.Status == "disconnected" {
|
||||||
@ -129,13 +136,26 @@ func (e *eventStream) listen(ctx context.Context) (chan bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case <-e.disconnectedChan:
|
case <-e.disconnectedChan:
|
||||||
connectedChan <- false
|
log.Info("listen loop <- disconnectedchan")
|
||||||
|
e.setIsConnected(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return connectedChan, nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *eventStream) GetIsConnected() bool {
|
||||||
|
e.isConnectedMutex.RLock()
|
||||||
|
defer e.isConnectedMutex.RUnlock()
|
||||||
|
return e.isConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *eventStream) setIsConnected(isConnected bool) {
|
||||||
|
e.isConnectedMutex.Lock()
|
||||||
|
defer e.isConnectedMutex.Unlock()
|
||||||
|
e.isConnected = isConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *eventStream) subscribeTransaction(transId string) chan *EventStreamResponse {
|
func (e *eventStream) subscribeTransaction(transId string) chan *EventStreamResponse {
|
@ -32,7 +32,7 @@ type UserProfileResponse struct {
|
|||||||
|
|
||||||
type DeviceResponse struct {
|
type DeviceResponse struct {
|
||||||
BaseResponse
|
BaseResponse
|
||||||
Data Devices `json:"data"`
|
Data []Device `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LibraryMetaDataResponse is an intermediate struct used when parsing data from the GetLibraryMetaData() call.
|
// LibraryMetaDataResponse is an intermediate struct used when parsing data from the GetLibraryMetaData() call.
|
||||||
|
1
types.go
1
types.go
@ -70,7 +70,6 @@ type UserProfile struct {
|
|||||||
type Friend struct {
|
type Friend struct {
|
||||||
FirstName string `json:"firstName"`
|
FirstName string `json:"firstName"`
|
||||||
LastName string `json:"lastName"`
|
LastName string `json:"lastName"`
|
||||||
Devices DeviceOrder `json:"devices"`
|
|
||||||
LastModified int64 `json:"lastModified"`
|
LastModified int64 `json:"lastModified"`
|
||||||
AdminUser bool `json:"adminUser"`
|
AdminUser bool `json:"adminUser"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
Loading…
Reference in New Issue
Block a user