arlo-go/basestation.go

426 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 08:49:40 +00:00
func (b *Basestation) makeEventStreamRequest(payload EventStreamPayload) (*EventStreamResponse, error) {
transId := genTransId()
payload.TransId = transId
if err := b.IsConnected(); err != nil {
2020-05-27 08:49:40 +00:00
return nil, fmt.Errorf("event stream not connected")
}
2020-05-27 08:49:40 +00:00
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.
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)
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.
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)
// If the event stream is closed, return an error about it.
2020-05-27 08:49:40 +00:00
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:
2020-05-27 08:49:40 +00:00
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 {
2020-05-27 08:49:40 +00:00
case <-b.eventStream.DisconnectedChan:
return fmt.Errorf("basestation not connected to event stream")
default:
return nil
}
}
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
case <-b.eventStream.DisconnectedChan:
return fmt.Errorf("failed to subscribe to the event stream: event stream was closed")
}
}
if err := b.Ping(); 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:
if err := b.Ping(); err != nil {
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 08:49:40 +00:00
func (b *Basestation) makeRequest(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 08:49:40 +00:00
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
}
2020-05-27 11:35:02 +00:00
func (b *Basestation) Ping() error {
err := b.makeRequest("set", fmt.Sprintf("subscriptions/%s_%s", b.UserId, TransIdPrefix), false, map[string][1]string{"devices": {b.DeviceId}}, nil)
if err != nil {
return fmt.Errorf("getting basestation %s state: %v", b.DeviceName, err)
}
return nil
}
2020-05-27 08:49:40 +00:00
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)
}
2020-05-27 08:49:40 +00:00
return &state, nil
}
2020-05-27 08:49:40 +00:00
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)
}
2020-05-27 08:49:40 +00:00
return states, nil
}
2020-05-27 08:49:40 +00:00
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
}
2020-05-27 11:35:02 +00:00
func (b *Basestation) GetCalendarMode() (*CalendarMode, error) {
var calendarMode CalendarMode
err := b.makeRequest("get", "schedule", false, nil, &calendarMode)
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 08:49:40 +00:00
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)
}
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 08:49:40 +00:00
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)
}
2020-05-27 08:49:40 +00:00
return &resp, nil
}
2020-05-27 08:49:40 +00:00
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)
}
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 11:35:02 +00:00
func (b *Basestation) DeleteMode(mode string) error {
err := b.makeRequest("delete", fmt.Sprintf("modes/%s", mode), true, nil, nil)
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 08:49:40 +00:00
func (b *Basestation) Arm() error {
err := b.SetCustomMode("mode1")
if err != nil {
return fmt.Errorf("arming (mode1): %v", err)
}
return nil
}
2020-05-27 08:49:40 +00:00
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,
}
2020-05-27 08:49:40 +00:00
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,
}
2020-05-27 08:49:40 +00:00
return b.makeEventStreamRequest(payload)
}