arlo-go/basestation.go

427 lines
12 KiB
Go
Raw Normal View History

package arlo
import (
2020-05-27 08:49:40 +00:00
"context"
"encoding/json"
"fmt"
"net/http"
"time"
2020-05-27 08:49:40 +00:00
log "github.com/sirupsen/logrus"
)
2020-05-27 08:49:40 +00:00
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
}
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
// 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
}
}
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) {
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)
}
}
2020-05-27 19:03:26 +00:00
transId := genTransId()
payload.TransId = transId
2020-05-27 08:49:40 +00:00
responseChan := make(chan *EventStreamResponse)
2020-05-27 19:03:26 +00:00
b.eventStream.subscribe(transId, responseChan)
defer b.eventStream.unsubscribe(transId)
// 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)
}
timer := time.NewTimer(eventStreamTimeout)
select {
2020-05-27 08:49:40 +00:00
case response := <-responseChan:
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)
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())
}
}
2020-05-27 19:03:26 +00:00
func (b *Basestation) IsConnected() bool {
select {
2020-05-27 19:03:26 +00:00
case <-b.eventStream.disconnectedChan:
return false
default:
2020-05-27 19:03:26 +00:00
return true
}
}
2020-05-27 08:49:40 +00:00
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})
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)
}
forLoop:
for {
select {
2020-05-27 08:49:40 +00:00
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")
}
2020-05-27 08:49:40 +00:00
break forLoop
}
}
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)
}
// 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)
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
}
}
}
2020-05-27 08:49:40 +00:00
}(ctx)
return nil
}
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
}
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.
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")
}
}
return nil
}
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 {
payload := EventStreamPayload{
2020-05-27 08:49:40 +00:00
Action: action,
Resource: resource,
PublishResponse: publishResponse,
Properties: properties,
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
}
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)
}
2020-05-27 08:49:40 +00:00
return &state, nil
}
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)
}
2020-05-27 08:49:40 +00:00
return states, nil
}
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
}
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)
}
2020-05-27 11:35:02 +00:00
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.
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)
}
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
}
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)
}
2020-05-27 08:49:40 +00:00
return &resp, nil
}
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)
}
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
}
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)
}
2020-05-27 11:35:02 +00:00
return nil
}
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
}
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
}
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"`
}
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
}