package arlo import ( "context" "encoding/json" "fmt" "net/http" "time" log "github.com/sirupsen/logrus" ) const eventStreamTimeout = 30 * time.Second const pingTime = 30 * time.Second // 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 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"` } type CalendarMode struct { Active bool `json:"active"` Schedule []struct { ModeID string `json:"modeId"` StartTime int `json:"startTime"` } `json:"schedule"` } // Basestations is a slice of Basestation objects. type Basestations []*Basestation // Find returns a basestation with the device id passed in. func (bs *Basestations) Find(deviceId string) *Basestation { for _, b := range *bs { if b.DeviceId == deviceId { return b } } return nil } // makeEventStreamRequest is a helper function sets up a response channel, sends a message to the event stream, and blocks waiting for the response. func (b *Basestation) makeEventStreamRequest(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) { var state BaseStationState err := b.makeRequest(ctx, "get", "basestation", false, nil, &state) if err != nil { return nil, fmt.Errorf("getting basestation %s state: %v", b.DeviceName, err) } return &state, nil } func (b *Basestation) GetAllCameraState(ctx context.Context) ([]CameraState, error) { var states []CameraState err := b.makeRequest(ctx, "get", "cameras", false, nil, &states) if err != nil { return nil, fmt.Errorf("getting associated cameras state: %v", err) } return states, nil } func (b *Basestation) GetRules(ctx context.Context) ([]Rule, error) { var resp GetRulesResponse err := b.makeRequest(ctx, "get", "rules", false, nil, &resp) if err != nil { return nil, fmt.Errorf("getting rules: %v", err) } return resp.Rules, nil } func (b *Basestation) GetCalendarMode(ctx context.Context) (*CalendarMode, error) { var calendarMode CalendarMode err := b.makeRequest(ctx, "get", "schedule", false, nil, &calendarMode) if err != nil { return nil, fmt.Errorf("getting calendar mode: %v", err) } return &calendarMode, nil } // 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(ctx context.Context, active bool) error { resp := make(map[string]bool) err := b.makeRequest(ctx, "set", "schedule", true, struct { Active bool `json:"active"` }{ Active: active, }, &resp) if err != nil { return fmt.Errorf("setting calendar mode %t: %v", active, err) } activemode, ok := resp["active"] if !ok { return fmt.Errorf("active mode not present in response") } if activemode != active { return fmt.Errorf("active mode is not the mode requested: requested %t, set %t", active, activemode) } return nil } func (b *Basestation) GetModes(ctx context.Context) (*GetModesResponse, error) { var resp GetModesResponse err := b.makeRequest(ctx, "get", "modes", false, nil, &resp) if err != nil { return nil, fmt.Errorf("getting modes: %v", err) } return &resp, nil } func (b *Basestation) SetCustomMode(ctx context.Context, mode string) error { resp := make(map[string]string) err := b.makeRequest(ctx, "set", "modes", true, struct { Active string `json:"active"` }{ Active: mode, }, &resp) if err != nil { return fmt.Errorf("setting custom mode %s: %v", mode, err) } activemode, ok := resp["active"] if !ok { return fmt.Errorf("active mode not present in response") } if activemode != mode { return fmt.Errorf("active mode is not the mode requested: requested %s, set %s", mode, activemode) } return nil } func (b *Basestation) DeleteMode(ctx context.Context, mode string) error { err := b.makeRequest(ctx, "delete", fmt.Sprintf("modes/%s", mode), true, nil, nil) if err != nil { return fmt.Errorf("deleting mode %s: %v", mode, err) } return nil } func (b *Basestation) Arm(ctx context.Context) error { err := b.SetCustomMode(ctx, "mode1") if err != nil { return fmt.Errorf("arming (mode1): %v", err) } return nil } func (b *Basestation) Disarm(ctx context.Context) error { err := b.SetCustomMode(ctx, "mode0") if err != nil { return fmt.Errorf("disarming (mode0): %v", err) } return nil } type SetSirenResponse struct { SirenState string `json:"sirenState"` SirenTrigger string `json:"sirenTrigger"` Duration int `json:"duration"` Timestamp int64 `json:"timestamp"` } 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 }