/* * Copyright (c) 2018 Jeffrey Walter * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package arlo import ( "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"` } // 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(payload EventStreamPayload) (*EventStreamResponse, error) { transId := genTransId() payload.TransId = transId if err := b.IsConnected(); err != nil { return nil, fmt.Errorf("event stream not connected") } responseChan := make(chan *EventStreamResponse) errorChan := make(chan error) b.eventStream.subscribe(transId, responseChan, errorChan) defer b.eventStream.unsubscribe(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) defer timer.Stop() // Wait for the response to come back from the event stream on the response channel. select { // If we get a response, return it to the caller. case response := <-responseChan: return response, nil case err := <-b.eventStream.Error: return nil, fmt.Errorf("event stream error: %v", err) // If the event stream is closed, return an error about it. case <-b.eventStream.DisconnectedChan: return nil, fmt.Errorf("event stream was closed before response was read") // If we timeout, return an error about it. case <-timer.C: return nil, fmt.Errorf("event stream response timed out after %.0f second", eventStreamTimeout.Seconds()) } } func (b *Basestation) IsConnected() error { // If the event stream is closed, return an error about it. select { case <-b.eventStream.DisconnectedChan: return fmt.Errorf("basestation not connected to event stream") default: return nil } } 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) } forLoop: for { 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") } break forLoop case <-b.eventStream.DisconnectedChan: return fmt.Errorf("failed to subscribe to the event stream: event stream was closed") } } if err := b.Ping(); 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(); 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 Status _, err := b.arlo.client.R(). SetResult(&response). SetHeader("xcloudId", b.XCloudId). Put(UnsubscribeUri) if err != nil { return fmt.Errorf("unsubscribing from event stream: %v", err) } if response.Success == false { return fmt.Errorf("unsubscribing from event stream: %s", response.Reason) } return nil } func (b *Basestation) Disconnect() error { // 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) Ping() error { payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("subscriptions/%s_%s", b.UserId, TransIdPrefix), PublishResponse: false, Properties: map[string][1]string{"devices": {b.DeviceId}}, From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), To: b.DeviceId, } if _, err := b.makeEventStreamRequest(payload); err != nil { return err } return nil } func (b *Basestation) NotifyEventStream(payload EventStreamPayload) error { var response Status _, err := b.arlo.client.R(). SetBody(payload). SetResult(&response). SetHeader("xcloudId", b.XCloudId). Post(fmt.Sprintf(NotifyUri, b.DeviceId)) if err != nil { return fmt.Errorf("notifying event stream: %v", err) } if response.Success == false { return fmt.Errorf("notifying event stream: %s", response.Reason) } return nil } func (b *Basestation) makeRequest(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(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) GetState() (*BaseStationState, error) { var state BaseStationState err := b.makeRequest("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() ([]CameraState, error) { var states []CameraState err := b.makeRequest("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() ([]Rule, error) { var resp GetRulesResponse err := b.makeRequest("get", "rules", false, nil, &resp) if err != nil { return nil, fmt.Errorf("getting rules: %v", err) } return resp.Rules, nil } func (b *Basestation) GetCalendarMode() (response *EventStreamResponse, err error) { payload := EventStreamPayload{ Action: "get", Resource: "schedule", PublishResponse: false, From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), To: b.DeviceId, } return b.makeEventStreamRequest(payload) } // 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) error { resp := make(map[string]bool) err := b.makeRequest("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() (*GetModesResponse, error) { var resp GetModesResponse err := b.makeRequest("get", "modes", false, nil, &resp) if err != nil { return nil, fmt.Errorf("getting modes: %v", err) } return &resp, nil } func (b *Basestation) SetCustomMode(mode string) error { resp := make(map[string]string) err := b.makeRequest("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(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) } func (b *Basestation) Arm() error { err := b.SetCustomMode("mode1") if err != nil { return fmt.Errorf("arming (mode1): %v", err) } return nil } func (b *Basestation) Disarm() error { err := b.SetCustomMode("mode0") if err != nil { return fmt.Errorf("disarming (mode0): %v", err) } return nil } 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) } 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) }