2018-09-19 07:07:32 +00:00
package arlo
2018-09-17 04:44:41 +00:00
import (
2020-05-27 08:49:40 +00:00
"context"
"encoding/json"
2018-09-17 04:44:41 +00:00
"fmt"
2018-12-11 23:48:51 +00:00
"net/http"
2018-09-20 22:38:01 +00:00
"time"
2018-09-19 21:35:05 +00:00
2020-05-27 08:49:40 +00:00
log "github.com/sirupsen/logrus"
2018-09-17 04:44:41 +00:00
)
2020-05-27 08:49:40 +00:00
const eventStreamTimeout = 30 * time . Second
2018-09-22 14:15:22 +00:00
const pingTime = 30 * time . Second
2018-09-17 04:44:41 +00:00
// 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.
type Basestation struct {
Device
2018-09-22 14:15:22 +00:00
eventStream * eventStream
2018-09-17 04:44:41 +00:00
}
2020-05-27 08:49:40 +00:00
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" `
}
2020-05-27 11:35:02 +00:00
type CalendarMode struct {
Active bool ` json:"active" `
Schedule [ ] struct {
ModeID string ` json:"modeId" `
StartTime int ` json:"startTime" `
} ` json:"schedule" `
}
2018-09-22 19:22:42 +00:00
// Basestations is a slice of Basestation objects.
2020-05-27 08:49:40 +00:00
type Basestations [ ] * Basestation
2018-09-17 04:44:41 +00:00
2018-09-22 14:15:22 +00:00
// Find returns a basestation with the device id passed in.
func ( bs * Basestations ) Find ( deviceId string ) * Basestation {
for _ , b := range * bs {
if b . DeviceId == deviceId {
2020-05-27 08:49:40 +00:00
return b
2018-09-22 14:15:22 +00:00
}
}
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.
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) makeEventStreamRequest ( ctx context . Context , payload EventStreamPayload ) ( * EventStreamResponse , error ) {
2018-09-20 22:38:01 +00:00
2020-05-27 19:03:26 +00:00
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 )
}
2018-09-20 22:38:01 +00:00
}
2020-05-27 19:03:26 +00:00
transId := genTransId ( )
payload . TransId = transId
2020-06-06 12:23:15 +00:00
responseChan := b . eventStream . subscribeTransaction ( transId )
2018-09-20 22:38:01 +00:00
2018-09-22 14:15:22 +00:00
// Send the payload to the event stream.
2020-05-27 08:49:40 +00:00
if err := b . NotifyEventStream ( payload ) ; err != nil {
return nil , fmt . Errorf ( "notifying event stream: %v" , err )
2018-09-20 22:38:01 +00:00
}
timer := time . NewTimer ( eventStreamTimeout )
select {
2020-05-27 08:49:40 +00:00
case response := <- responseChan :
2018-09-20 22:38:01 +00:00
return response , nil
2020-05-27 08:49:40 +00:00
case err := <- b . eventStream . Error :
return nil , fmt . Errorf ( "event stream error: %v" , err )
2020-05-27 19:03:26 +00:00
case <- b . eventStream . disconnectedChan :
log . Warn ( "event stream was closed before response was read" )
return b . makeEventStreamRequest ( ctx , payload )
2018-09-22 14:15:22 +00:00
case <- timer . C :
2020-05-27 08:49:40 +00:00
return nil , fmt . Errorf ( "event stream response timed out after %.0f second" , eventStreamTimeout . Seconds ( ) )
2018-09-20 22:38:01 +00:00
}
}
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) IsConnected ( ) bool {
2018-09-22 14:15:22 +00:00
select {
2020-05-27 19:03:26 +00:00
case <- b . eventStream . disconnectedChan :
return false
2018-09-22 14:15:22 +00:00
default :
2020-05-27 19:03:26 +00:00
return true
2018-09-20 22:38:01 +00:00
}
}
2020-05-27 08:49:40 +00:00
func ( b * Basestation ) Subscribe ( ctx context . Context ) error {
2020-06-08 16:19:08 +00:00
b . eventStream = newEventStream (
BaseUrl + fmt . Sprintf ( NotifyResponsesPushServiceUri , b . arlo . Account . Token ) ,
& http . Client { Jar : b . arlo . client . GetClient ( ) . Jar } )
2018-09-19 21:35:05 +00:00
2020-05-27 08:49:40 +00:00
connectedChan , err := b . eventStream . listen ( ctx )
if err != nil {
return fmt . Errorf ( "setting up event stream: %v" , err )
}
2020-06-06 12:23:15 +00:00
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" )
2018-09-19 21:35:05 +00:00
}
}
2018-09-18 18:02:21 +00:00
2020-05-27 19:03:26 +00:00
if err := b . Ping ( ctx ) ; err != nil {
2020-05-27 08:49:40 +00:00
_ = b . Disconnect ( )
return fmt . Errorf ( "Pingloop > error while pinging: %v > disconnect event stream" , err )
2018-09-20 22:38:01 +00:00
}
// The Arlo event stream requires a "ping" every 30s.
2020-05-27 08:49:40 +00:00
go func ( ctx context . Context ) {
ticker := time . NewTicker ( pingTime )
2018-09-19 21:35:05 +00:00
for {
2020-05-27 08:49:40 +00:00
select {
case <- ctx . Done ( ) :
return
case _ = <- ticker . C :
2020-05-27 19:03:26 +00:00
if err := b . Ping ( ctx ) ; err != nil {
2020-05-27 08:49:40 +00:00
log . Errorf ( "Pingloop > error while pinging: %v > disconnect event stream" , err )
_ = b . Disconnect ( )
return
}
2018-09-19 21:35:05 +00:00
}
}
2020-05-27 08:49:40 +00:00
} ( ctx )
2018-09-19 21:35:05 +00:00
2018-09-20 22:38:01 +00:00
return nil
}
2018-12-11 23:48:51 +00:00
func ( b * Basestation ) Unsubscribe ( ) error {
2020-05-27 11:35:02 +00:00
var response BaseResponse
err := b . arlo . put ( UnsubscribeUri , & response , b . XCloudId )
2020-05-27 08:49:40 +00:00
if err != nil {
2020-05-27 11:35:02 +00:00
return err
2020-05-27 08:49:40 +00:00
}
2020-05-27 11:35:02 +00:00
if ! response . Success {
return fmt . Errorf ( "no success but no error" )
2020-05-27 08:49:40 +00:00
}
return nil
2018-12-11 23:48:51 +00:00
}
2018-09-22 14:15:22 +00:00
func ( b * Basestation ) Disconnect ( ) error {
// disconnect channel to stop event stream.
2018-09-20 22:38:01 +00:00
if b . eventStream != nil {
2018-09-22 14:15:22 +00:00
b . eventStream . disconnect ( )
2018-09-20 22:38:01 +00:00
}
return nil
}
// Ping makes a call to the subscriptions endpoint. The Arlo event stream requires this message to be sent every 30s.
2018-09-18 18:02:21 +00:00
2020-05-27 08:49:40 +00:00
func ( b * Basestation ) NotifyEventStream ( payload EventStreamPayload ) error {
2020-05-27 11:35:02 +00:00
var response ErrorResponse
err := b . arlo . post ( fmt . Sprintf ( NotifyUri , b . DeviceId ) , payload , & response , b . XCloudId )
2020-05-27 08:49:40 +00:00
if err != nil {
2020-05-27 11:35:02 +00:00
return err
2020-05-27 08:49:40 +00:00
}
2020-05-27 11:35:02 +00:00
if ! response . Success {
if response . Reason != "" {
return fmt . Errorf ( response . Reason )
} else {
return fmt . Errorf ( "no success but no error" )
}
2018-09-19 21:35:05 +00:00
}
return nil
}
2018-09-17 04:44:41 +00:00
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) makeRequest ( ctx context . Context , action string , resource string , publishResponse bool , properties interface { } , result interface { } ) error {
2018-09-20 22:38:01 +00:00
payload := EventStreamPayload {
2020-05-27 08:49:40 +00:00
Action : action ,
Resource : resource ,
PublishResponse : publishResponse ,
Properties : properties ,
2018-09-17 04:44:41 +00:00
From : fmt . Sprintf ( "%s_%s" , b . UserId , TransIdPrefix ) ,
To : b . DeviceId ,
}
2020-05-27 19:03:26 +00:00
resp , err := b . makeEventStreamRequest ( ctx , payload )
2020-05-27 08:49:40 +00:00
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
2018-09-19 07:07:32 +00:00
}
2020-05-27 19:03:26 +00:00
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 )
2020-05-27 11:35:02 +00:00
if err != nil {
2020-05-27 19:03:26 +00:00
return err
2020-05-27 11:35:02 +00:00
}
return nil
}
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) GetState ( ctx context . Context ) ( * BaseStationState , error ) {
2020-05-27 08:49:40 +00:00
var state BaseStationState
2020-05-27 19:03:26 +00:00
err := b . makeRequest ( ctx , "get" , "basestation" , false , nil , & state )
2020-05-27 08:49:40 +00:00
if err != nil {
return nil , fmt . Errorf ( "getting basestation %s state: %v" , b . DeviceName , err )
2018-09-19 07:07:32 +00:00
}
2020-05-27 08:49:40 +00:00
return & state , nil
2018-09-19 21:35:05 +00:00
}
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) GetAllCameraState ( ctx context . Context ) ( [ ] CameraState , error ) {
2020-05-27 08:49:40 +00:00
var states [ ] CameraState
2020-05-27 19:03:26 +00:00
err := b . makeRequest ( ctx , "get" , "cameras" , false , nil , & states )
2020-05-27 08:49:40 +00:00
if err != nil {
return nil , fmt . Errorf ( "getting associated cameras state: %v" , err )
2018-09-20 22:38:01 +00:00
}
2020-05-27 08:49:40 +00:00
return states , nil
}
2018-09-19 21:35:05 +00:00
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) GetRules ( ctx context . Context ) ( [ ] Rule , error ) {
2020-05-27 08:49:40 +00:00
var resp GetRulesResponse
2020-05-27 19:03:26 +00:00
err := b . makeRequest ( ctx , "get" , "rules" , false , nil , & resp )
2020-05-27 08:49:40 +00:00
if err != nil {
return nil , fmt . Errorf ( "getting rules: %v" , err )
}
return resp . Rules , nil
2018-09-20 22:38:01 +00:00
}
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) GetCalendarMode ( ctx context . Context ) ( * CalendarMode , error ) {
2020-05-27 11:35:02 +00:00
var calendarMode CalendarMode
2020-05-27 19:03:26 +00:00
err := b . makeRequest ( ctx , "get" , "schedule" , false , nil , & calendarMode )
2020-05-27 11:35:02 +00:00
if err != nil {
return nil , fmt . Errorf ( "getting calendar mode: %v" , err )
2018-09-19 21:35:05 +00:00
}
2020-05-27 11:35:02 +00:00
return & calendarMode , nil
2018-09-20 22:38:01 +00:00
}
2018-09-19 21:35:05 +00:00
2018-09-20 22:38:01 +00:00
// SetCalendarMode toggles calendar mode.
// NOTE: The Arlo API seems to disable calendar mode when switching to other modes, if it's enabled.
// You should probably do the same, although, the UI reflects the switch from calendar mode to say armed mode without explicitly setting calendar mode to inactive.
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) SetCalendarMode ( ctx context . Context , active bool ) error {
2020-05-27 08:49:40 +00:00
resp := make ( map [ string ] bool )
2020-05-27 19:03:26 +00:00
err := b . makeRequest ( ctx , "set" , "schedule" , true , struct {
2020-05-27 08:49:40 +00:00
Active bool ` json:"active" `
} {
Active : active ,
} , & resp )
if err != nil {
return fmt . Errorf ( "setting calendar mode %t: %v" , active , err )
2018-09-17 04:44:41 +00:00
}
2020-05-27 08:49:40 +00:00
activemode , ok := resp [ "active" ]
if ! ok {
return fmt . Errorf ( "active mode not present in response" )
}
if activemode != active {
return fmt . Errorf ( "active mode is not the mode requested: requested %t, set %t" , active , activemode )
}
return nil
2018-09-20 22:38:01 +00:00
}
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) GetModes ( ctx context . Context ) ( * GetModesResponse , error ) {
2020-05-27 08:49:40 +00:00
var resp GetModesResponse
2020-05-27 19:03:26 +00:00
err := b . makeRequest ( ctx , "get" , "modes" , false , nil , & resp )
2020-05-27 08:49:40 +00:00
if err != nil {
return nil , fmt . Errorf ( "getting modes: %v" , err )
2018-09-20 22:38:01 +00:00
}
2020-05-27 08:49:40 +00:00
return & resp , nil
2018-09-20 22:38:01 +00:00
}
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) SetCustomMode ( ctx context . Context , mode string ) error {
2020-05-27 08:49:40 +00:00
resp := make ( map [ string ] string )
2020-05-27 19:03:26 +00:00
err := b . makeRequest ( ctx , "set" , "modes" , true , struct {
2020-05-27 08:49:40 +00:00
Active string ` json:"active" `
} {
Active : mode ,
} , & resp )
if err != nil {
return fmt . Errorf ( "setting custom mode %s: %v" , mode , err )
2018-09-19 21:35:05 +00:00
}
2020-05-27 08:49:40 +00:00
activemode , ok := resp [ "active" ]
if ! ok {
return fmt . Errorf ( "active mode not present in response" )
}
if activemode != mode {
return fmt . Errorf ( "active mode is not the mode requested: requested %s, set %s" , mode , activemode )
}
return nil
2018-09-20 22:38:01 +00:00
}
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) DeleteMode ( ctx context . Context , mode string ) error {
err := b . makeRequest ( ctx , "delete" , fmt . Sprintf ( "modes/%s" , mode ) , true , nil , nil )
2020-05-27 11:35:02 +00:00
if err != nil {
return fmt . Errorf ( "deleting mode %s: %v" , mode , err )
2018-09-20 22:38:01 +00:00
}
2020-05-27 11:35:02 +00:00
return nil
2018-09-20 22:38:01 +00:00
}
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) Arm ( ctx context . Context ) error {
err := b . SetCustomMode ( ctx , "mode1" )
2020-05-27 08:49:40 +00:00
if err != nil {
return fmt . Errorf ( "arming (mode1): %v" , err )
}
return nil
2018-09-20 22:38:01 +00:00
}
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) Disarm ( ctx context . Context ) error {
err := b . SetCustomMode ( ctx , "mode0" )
2020-05-27 08:49:40 +00:00
if err != nil {
return fmt . Errorf ( "disarming (mode0): %v" , err )
}
return nil
2018-09-20 22:38:01 +00:00
}
2020-05-27 19:03:26 +00:00
type SetSirenResponse struct {
SirenState string ` json:"sirenState" `
SirenTrigger string ` json:"sirenTrigger" `
Duration int ` json:"duration" `
Timestamp int64 ` json:"timestamp" `
2018-09-20 22:38:01 +00:00
}
2020-05-27 19:03:26 +00:00
func ( b * Basestation ) SirenOn ( ctx context . Context ) error {
var response SetSirenResponse
err := b . makeRequest ( ctx , "set" , "siren" , true , SirenProperties {
SirenState : "on" ,
Duration : 300 ,
Volume : 8 ,
Pattern : "alarm" ,
} , & response )
if err != nil {
return fmt . Errorf ( "making request: %v" , err )
}
if response . SirenState != "on" {
return fmt . Errorf ( "siren not on in response" )
}
return nil
}
func ( b * Basestation ) SirenOff ( ctx context . Context ) error {
var response SetSirenResponse
err := b . makeRequest ( ctx , "set" , "siren" , true , SirenProperties {
SirenState : "off" ,
Duration : 300 ,
Volume : 8 ,
Pattern : "alarm" ,
} , & response )
if err != nil {
return fmt . Errorf ( "making request: %v" , err )
}
if response . SirenState != "off" {
return fmt . Errorf ( "siren not off in response" )
}
return nil
2018-09-17 04:44:41 +00:00
}