2018-09-22 19:22:42 +00:00
/ *
* 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 .
* /
2018-09-19 07:07:32 +00:00
package arlo
2018-09-17 04:44:41 +00:00
import (
"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
"github.com/pkg/errors"
2018-09-17 04:44:41 +00:00
)
2018-09-20 22:38:01 +00:00
const eventStreamTimeout = 10 * 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
}
2018-09-22 19:22:42 +00:00
// Basestations is a slice of Basestation objects.
2018-09-17 04:44:41 +00:00
type Basestations [ ] Basestation
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 {
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.
2018-09-20 22:38:01 +00:00
func ( b * Basestation ) makeEventStreamRequest ( payload EventStreamPayload , msg string ) ( response * EventStreamResponse , err error ) {
transId := genTransId ( )
payload . TransId = transId
if err := b . IsConnected ( ) ; err != nil {
2018-12-11 23:48:51 +00:00
//if err := b.Subscribe(); err != nil {
return nil , errors . WithMessage ( errors . WithMessage ( err , msg ) , "failed to reconnect to event stream" )
//}
2018-09-20 22:38:01 +00:00
}
2018-09-22 14:15:22 +00:00
subscriber := make ( subscriber )
// Add the response channel to the event stream queue so the response can be written to it.
b . eventStream . subscribe ( transId , subscriber )
// Make sure we close and remove the response channel before returning.
defer b . eventStream . unsubscribe ( transId )
2018-09-20 22:38:01 +00:00
2018-09-22 14:15:22 +00:00
// Send the payload to the event stream.
2018-09-20 22:38:01 +00:00
if err := b . NotifyEventStream ( payload , msg ) ; err != nil {
return nil , err
}
timer := time . NewTimer ( eventStreamTimeout )
defer timer . Stop ( )
2018-09-22 14:15:22 +00:00
// Wait for the response to come back from the event stream on the response channel.
2018-09-20 22:38:01 +00:00
select {
2018-09-22 14:15:22 +00:00
// If we get a response, return it to the caller.
case response := <- subscriber :
2018-09-20 22:38:01 +00:00
return response , nil
case err = <- b . eventStream . Error :
return nil , errors . Wrap ( err , msg )
2018-09-22 14:15:22 +00:00
// If the event stream is closed, return an error about it.
case <- b . eventStream . Disconnected :
2018-09-20 22:38:01 +00:00
err = errors . New ( "event stream was closed before response was read" )
return nil , errors . WithMessage ( err , msg )
2018-09-22 14:15:22 +00:00
// If we timeout, return an error about it.
case <- timer . C :
err = fmt . Errorf ( "event stream response timed out after %.0f second" , eventStreamTimeout . Seconds ( ) )
return nil , errors . WithMessage ( err , msg )
2018-09-20 22:38:01 +00:00
}
}
func ( b * Basestation ) IsConnected ( ) error {
2018-09-22 14:15:22 +00:00
// If the event stream is closed, return an error about it.
select {
case <- b . eventStream . Disconnected :
2018-09-20 22:38:01 +00:00
return errors . New ( "basestation not connected to event stream" )
2018-09-22 14:15:22 +00:00
default :
return nil
2018-09-20 22:38:01 +00:00
}
}
2018-09-19 19:12:06 +00:00
func ( b * Basestation ) Subscribe ( ) error {
2018-12-11 23:48:51 +00:00
b . eventStream = newEventStream ( BaseUrl + fmt . Sprintf ( NotifyResponsesPushServiceUri , b . arlo . Account . Token ) , & http . Client { Jar : b . arlo . client . HttpClient . Jar } )
2018-09-19 21:35:05 +00:00
2018-09-20 22:38:01 +00:00
forLoop :
2018-09-19 21:35:05 +00:00
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 {
2018-09-22 14:15:22 +00:00
case connected := <- b . eventStream . listen ( ) :
if connected {
2018-09-20 22:38:01 +00:00
break forLoop
2018-09-19 21:35:05 +00:00
} else {
2018-09-20 22:38:01 +00:00
return errors . New ( "failed to subscribe to the event stream" )
2018-09-19 21:35:05 +00:00
}
2018-09-22 14:15:22 +00:00
case <- b . eventStream . Disconnected :
err := errors . New ( "event stream was closed" )
return errors . WithMessage ( err , "failed to subscribe to the event stream" )
2018-09-19 21:35:05 +00:00
}
}
2018-09-18 18:02:21 +00:00
2018-09-20 22:38:01 +00:00
if err := b . Ping ( ) ; err != nil {
return errors . WithMessage ( err , "failed to subscribe to the event stream" )
}
// The Arlo event stream requires a "ping" every 30s.
2018-09-19 21:35:05 +00:00
go func ( ) {
for {
2018-09-22 14:15:22 +00:00
time . Sleep ( pingTime )
2018-09-20 22:38:01 +00:00
if err := b . Ping ( ) ; err != nil {
2018-09-22 14:15:22 +00:00
b . Disconnect ( )
2018-09-20 22:38:01 +00:00
break
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 {
resp , err := b . arlo . get ( UnsubscribeUri , b . XCloudId , nil )
return checkRequest ( resp , err , "failed to unsubscribe from event stream" )
}
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.
func ( b * Basestation ) Ping ( ) error {
payload := EventStreamPayload {
2018-09-18 18:02:21 +00:00
Action : "set" ,
2018-09-19 19:12:06 +00:00
Resource : fmt . Sprintf ( "subscriptions/%s_%s" , b . UserId , TransIdPrefix ) ,
2018-09-18 18:02:21 +00:00
PublishResponse : false ,
2018-09-19 19:12:06 +00:00
Properties : map [ string ] [ 1 ] string { "devices" : { b . DeviceId } } ,
2018-09-18 18:02:21 +00:00
From : fmt . Sprintf ( "%s_%s" , b . UserId , TransIdPrefix ) ,
To : b . DeviceId ,
}
2018-09-19 21:35:05 +00:00
2018-09-20 22:38:01 +00:00
if _ , err := b . makeEventStreamRequest ( payload , "failed to ping the event stream" ) ; err != nil {
2018-09-19 21:35:05 +00:00
return err
}
return nil
2018-09-19 19:12:06 +00:00
}
2018-09-18 18:02:21 +00:00
2018-09-20 22:38:01 +00:00
func ( b * Basestation ) NotifyEventStream ( payload EventStreamPayload , msg string ) error {
resp , err := b . arlo . post ( fmt . Sprintf ( NotifyUri , b . DeviceId ) , b . XCloudId , payload , nil )
if err := checkRequest ( resp , err , msg ) ; err != nil {
return errors . WithMessage ( err , "failed to notify event stream" )
2018-09-19 21:35:05 +00:00
}
2018-09-20 22:38:01 +00:00
defer resp . Body . Close ( )
2018-09-17 04:44:41 +00:00
2018-09-19 21:35:05 +00:00
return nil
}
2018-09-17 04:44:41 +00:00
2018-09-20 22:38:01 +00:00
func ( b * Basestation ) GetState ( ) ( response * EventStreamResponse , err error ) {
2018-09-17 04:44:41 +00:00
2018-09-20 22:38:01 +00:00
payload := EventStreamPayload {
2018-09-17 04:44:41 +00:00
Action : "get" ,
Resource : "basestation" ,
PublishResponse : false ,
From : fmt . Sprintf ( "%s_%s" , b . UserId , TransIdPrefix ) ,
To : b . DeviceId ,
}
2018-09-19 21:35:05 +00:00
return b . makeEventStreamRequest ( payload , "failed to get basestation state" )
2018-09-19 07:07:32 +00:00
}
2018-09-20 22:38:01 +00:00
func ( b * Basestation ) GetAssociatedCamerasState ( ) ( response * EventStreamResponse , err error ) {
payload := EventStreamPayload {
2018-09-19 07:07:32 +00:00
Action : "get" ,
Resource : "cameras" ,
PublishResponse : false ,
From : fmt . Sprintf ( "%s_%s" , b . UserId , TransIdPrefix ) ,
To : b . DeviceId ,
}
2018-09-17 04:44:41 +00:00
2018-09-19 21:35:05 +00:00
return b . makeEventStreamRequest ( payload , "failed to get associated cameras state" )
}
2018-09-20 22:38:01 +00:00
func ( b * Basestation ) GetRules ( ) ( response * EventStreamResponse , err error ) {
payload := EventStreamPayload {
Action : "get" ,
Resource : "rules" ,
PublishResponse : false ,
From : fmt . Sprintf ( "%s_%s" , b . UserId , TransIdPrefix ) ,
To : b . DeviceId ,
}
2018-09-19 21:35:05 +00:00
2018-09-20 22:38:01 +00:00
return b . makeEventStreamRequest ( payload , "failed to get rules" )
}
func ( b * Basestation ) GetCalendarMode ( ) ( response * EventStreamResponse , err error ) {
payload := EventStreamPayload {
Action : "get" ,
Resource : "schedule" ,
PublishResponse : false ,
From : fmt . Sprintf ( "%s_%s" , b . UserId , TransIdPrefix ) ,
To : b . DeviceId ,
2018-09-19 21:35:05 +00:00
}
2018-09-20 22:38:01 +00:00
return b . makeEventStreamRequest ( payload , "failed to get schedule" )
}
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.
func ( b * Basestation ) SetCalendarMode ( active bool ) ( response * EventStreamResponse , err error ) {
payload := EventStreamPayload {
Action : "set" ,
Resource : "schedule" ,
PublishResponse : true ,
Properties : BasestationScheduleProperties {
Active : active ,
} ,
From : fmt . Sprintf ( "%s_%s" , b . UserId , TransIdPrefix ) ,
To : b . DeviceId ,
2018-09-17 04:44:41 +00:00
}
2018-09-20 22:38:01 +00:00
return b . makeEventStreamRequest ( payload , "failed to set schedule" )
}
func ( b * Basestation ) GetModes ( ) ( response * EventStreamResponse , err error ) {
payload := EventStreamPayload {
Action : "get" ,
Resource : "modes" ,
PublishResponse : false ,
From : fmt . Sprintf ( "%s_%s" , b . UserId , TransIdPrefix ) ,
To : b . DeviceId ,
}
return b . makeEventStreamRequest ( payload , "failed to get modes" )
}
func ( b * Basestation ) SetCustomMode ( mode string ) ( response * EventStreamResponse , err error ) {
payload := EventStreamPayload {
Action : "set" ,
Resource : "modes" ,
PublishResponse : true ,
Properties : BasestationModeProperties {
Active : mode ,
} ,
From : fmt . Sprintf ( "%s_%s" , b . UserId , TransIdPrefix ) ,
To : b . DeviceId ,
2018-09-19 21:35:05 +00:00
}
2018-09-20 22:38:01 +00:00
return b . makeEventStreamRequest ( payload , "failed to set mode" )
}
func ( b * Basestation ) DeleteMode ( mode string ) ( response * EventStreamResponse , err error ) {
payload := EventStreamPayload {
Action : "delete" ,
Resource : fmt . Sprintf ( "modes/%s" , mode ) ,
PublishResponse : true ,
From : fmt . Sprintf ( "%s_%s" , b . UserId , TransIdPrefix ) ,
To : b . DeviceId ,
}
return b . makeEventStreamRequest ( payload , "failed to set mode" )
}
func ( b * Basestation ) Arm ( ) ( response * EventStreamResponse , err error ) {
return b . SetCustomMode ( "mode1" )
}
func ( b * Basestation ) Disarm ( ) ( response * EventStreamResponse , err error ) {
return b . SetCustomMode ( "mode0" )
}
func ( b * Basestation ) SirenOn ( ) ( response * EventStreamResponse , err error ) {
payload := EventStreamPayload {
Action : "set" ,
Resource : "siren" ,
PublishResponse : true ,
Properties : SirenProperties {
SirenState : "on" ,
Duration : 300 ,
Volume : 8 ,
Pattern : "alarm" ,
} ,
From : fmt . Sprintf ( "%s_%s" , b . UserId , TransIdPrefix ) ,
To : b . DeviceId ,
}
return b . makeEventStreamRequest ( payload , "failed to get modes" )
}
func ( b * Basestation ) SirenOff ( ) ( response * EventStreamResponse , err error ) {
payload := EventStreamPayload {
Action : "set" ,
Resource : "siren" ,
PublishResponse : true ,
Properties : SirenProperties {
SirenState : "off" ,
Duration : 300 ,
Volume : 8 ,
Pattern : "alarm" ,
} ,
From : fmt . Sprintf ( "%s_%s" , b . UserId , TransIdPrefix ) ,
To : b . DeviceId ,
}
return b . makeEventStreamRequest ( payload , "failed to get modes" )
2018-09-17 04:44:41 +00:00
}