arlo-go/basestation.go

451 lines
14 KiB
Go

/*
* 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.
*/
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)
}