diff --git a/basestation.go b/basestation.go index 1586bb4..18d8c5d 100644 --- a/basestation.go +++ b/basestation.go @@ -114,50 +114,48 @@ func (bs *Basestations) Find(deviceId string) *Basestation { } // 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) { +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 - 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) + b.eventStream.subscribe(transId, responseChan) 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 <-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() error { - // If the event stream is closed, return an error about it. +func (b *Basestation) IsConnected() bool { select { - case <-b.eventStream.DisconnectedChan: - return fmt.Errorf("basestation not connected to event stream") + case <-b.eventStream.disconnectedChan: + return false default: - return nil + return true } } @@ -178,12 +176,10 @@ forLoop: 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 { + if err := b.Ping(ctx); err != nil { _ = b.Disconnect() return fmt.Errorf("Pingloop > error while pinging: %v > disconnect event stream", err) } @@ -196,7 +192,7 @@ forLoop: case <-ctx.Done(): return case _ = <-ticker.C: - if err := b.Ping(); err != nil { + if err := b.Ping(ctx); err != nil { log.Errorf("Pingloop > error while pinging: %v > disconnect event stream", err) _ = b.Disconnect() return @@ -246,7 +242,7 @@ func (b *Basestation) NotifyEventStream(payload EventStreamPayload) error { return nil } -func (b *Basestation) makeRequest(action string, resource string, publishResponse bool, properties interface{}, result interface{}) error { +func (b *Basestation) makeRequest(ctx context.Context, action string, resource string, publishResponse bool, properties interface{}, result interface{}) error { payload := EventStreamPayload{ Action: action, Resource: resource, @@ -255,7 +251,7 @@ func (b *Basestation) makeRequest(action string, resource string, publishRespons From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), To: b.DeviceId, } - resp, err := b.makeEventStreamRequest(payload) + resp, err := b.makeEventStreamRequest(ctx, payload) if err != nil { return fmt.Errorf("making event stream request: %v", err) } @@ -268,44 +264,44 @@ func (b *Basestation) makeRequest(action string, resource string, publishRespons return nil } -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) +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 fmt.Errorf("getting basestation %s state: %v", b.DeviceName, err) + return err } return nil } -func (b *Basestation) GetState() (*BaseStationState, error) { +func (b *Basestation) GetState(ctx context.Context) (*BaseStationState, error) { var state BaseStationState - err := b.makeRequest("get", "basestation", false, nil, &state) + 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() ([]CameraState, error) { +func (b *Basestation) GetAllCameraState(ctx context.Context) ([]CameraState, error) { var states []CameraState - err := b.makeRequest("get", "cameras", false, nil, &states) + 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() ([]Rule, error) { +func (b *Basestation) GetRules(ctx context.Context) ([]Rule, error) { var resp GetRulesResponse - err := b.makeRequest("get", "rules", false, nil, &resp) + 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() (*CalendarMode, error) { +func (b *Basestation) GetCalendarMode(ctx context.Context) (*CalendarMode, error) { var calendarMode CalendarMode - err := b.makeRequest("get", "schedule", false, nil, &calendarMode) + err := b.makeRequest(ctx, "get", "schedule", false, nil, &calendarMode) if err != nil { return nil, fmt.Errorf("getting calendar mode: %v", err) } @@ -315,9 +311,9 @@ func (b *Basestation) GetCalendarMode() (*CalendarMode, error) { // 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 { +func (b *Basestation) SetCalendarMode(ctx context.Context, active bool) error { resp := make(map[string]bool) - err := b.makeRequest("set", "schedule", true, struct { + err := b.makeRequest(ctx, "set", "schedule", true, struct { Active bool `json:"active"` }{ Active: active, @@ -335,18 +331,18 @@ func (b *Basestation) SetCalendarMode(active bool) error { return nil } -func (b *Basestation) GetModes() (*GetModesResponse, error) { +func (b *Basestation) GetModes(ctx context.Context) (*GetModesResponse, error) { var resp GetModesResponse - err := b.makeRequest("get", "modes", false, nil, &resp) + 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(mode string) error { +func (b *Basestation) SetCustomMode(ctx context.Context, mode string) error { resp := make(map[string]string) - err := b.makeRequest("set", "modes", true, struct { + err := b.makeRequest(ctx, "set", "modes", true, struct { Active string `json:"active"` }{ Active: mode, @@ -364,62 +360,67 @@ func (b *Basestation) SetCustomMode(mode string) error { return nil } -func (b *Basestation) DeleteMode(mode string) error { - err := b.makeRequest("delete", fmt.Sprintf("modes/%s", mode), true, nil, 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() error { - err := b.SetCustomMode("mode1") +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() error { - err := b.SetCustomMode("mode0") +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 } -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) +type SetSirenResponse struct { + SirenState string `json:"sirenState"` + SirenTrigger string `json:"sirenTrigger"` + Duration int `json:"duration"` + Timestamp int64 `json:"timestamp"` } -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, +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) } - - return b.makeEventStreamRequest(payload) + 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 } diff --git a/camera.go b/camera.go index 6ba4304..06da3b7 100644 --- a/camera.go +++ b/camera.go @@ -1,6 +1,7 @@ package arlo import ( + "context" "fmt" ) @@ -90,28 +91,23 @@ func (cs *Cameras) Find(deviceId string) *Camera { return nil } -// On turns a camera on; meaning it will detect and record events. -func (c *Camera) On() (response *EventStreamResponse, err error) { - payload := EventStreamPayload{ - Action: "set", - Resource: fmt.Sprintf("cameras/%s", c.DeviceId), - PublishResponse: true, - Properties: CameraProperties{ - PrivacyActive: false, - }, - From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), - To: c.ParentId, - } - +func (c *Camera) On(ctx context.Context) error { b := c.arlo.Basestations.Find(c.ParentId) if b == nil { - return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) } - return b.makeEventStreamRequest(payload) + + err := b.makeRequest(ctx, "set", fmt.Sprintf("cameras/%s", c.DeviceId), true, CameraProperties{ + PrivacyActive: false, + }, nil) + if err != nil { + return err + } + return nil } // On turns a camera off; meaning it won't detect and record events. -func (c *Camera) Off() (response *EventStreamResponse, err error) { +func (c *Camera) Off(ctx context.Context) (response *EventStreamResponse, err error) { payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("cameras/%s", c.DeviceId), @@ -127,13 +123,13 @@ func (c *Camera) Off() (response *EventStreamResponse, err error) { if b == nil { return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) } - return b.makeEventStreamRequest(payload) + return b.makeEventStreamRequest(ctx, payload) } // SetBrightness sets the camera brightness. // NOTE: Brightness is between -2 and 2 in increments of 1 (-2, -1, 0, 1, 2). // Setting it to an invalid value has no effect. -func (c *Camera) SetBrightness(brightness int) (response *EventStreamResponse, err error) { +func (c *Camera) SetBrightness(ctx context.Context, brightness int) (response *EventStreamResponse, err error) { // Sanity check; if the values are above or below the allowed limits, set them to their limit. if brightness < -2 { brightness = -2 @@ -155,10 +151,10 @@ func (c *Camera) SetBrightness(brightness int) (response *EventStreamResponse, e if b == nil { return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) } - return b.makeEventStreamRequest(payload) + return b.makeEventStreamRequest(ctx, payload) } -func (c *Camera) EnableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) { +func (c *Camera) EnableMotionAlerts(ctx context.Context, sensitivity int, zones []string) (response *EventStreamResponse, err error) { payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("cameras/%s", c.DeviceId), @@ -177,10 +173,10 @@ func (c *Camera) EnableMotionAlerts(sensitivity int, zones []string) (response * if b == nil { return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) } - return b.makeEventStreamRequest(payload) + return b.makeEventStreamRequest(ctx, payload) } -func (c *Camera) DisableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) { +func (c *Camera) DisableMotionAlerts(ctx context.Context, sensitivity int, zones []string) (response *EventStreamResponse, err error) { payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("cameras/%s", c.DeviceId), @@ -200,10 +196,10 @@ func (c *Camera) DisableMotionAlerts(sensitivity int, zones []string) (response if b == nil { return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) } - return b.makeEventStreamRequest(payload) + return b.makeEventStreamRequest(ctx, payload) } -func (c *Camera) EnableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) { +func (c *Camera) EnableAudioAlerts(ctx context.Context, sensitivity int) (response *EventStreamResponse, err error) { payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("cameras/%s", c.DeviceId), @@ -222,10 +218,10 @@ func (c *Camera) EnableAudioAlerts(sensitivity int) (response *EventStreamRespon if b == nil { return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) } - return b.makeEventStreamRequest(payload) + return b.makeEventStreamRequest(ctx, payload) } -func (c *Camera) DisableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) { +func (c *Camera) DisableAudioAlerts(ctx context.Context, sensitivity int) (response *EventStreamResponse, err error) { payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("cameras/%s", c.DeviceId), @@ -244,11 +240,11 @@ func (c *Camera) DisableAudioAlerts(sensitivity int) (response *EventStreamRespo if b == nil { return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) } - return b.makeEventStreamRequest(payload) + return b.makeEventStreamRequest(ctx, payload) } // action: disabled OR recordSnapshot OR recordVideo -func (c *Camera) SetAlertNotificationMethods(action string, email, push bool) (response *EventStreamResponse, err error) { +func (c *Camera) SetAlertNotificationMethods(ctx context.Context, action string, email, push bool) (response *EventStreamResponse, err error) { payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("cameras/%s", c.DeviceId), @@ -273,5 +269,5 @@ func (c *Camera) SetAlertNotificationMethods(action string, email, push bool) (r if b == nil { return nil, fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) } - return b.makeEventStreamRequest(payload) + return b.makeEventStreamRequest(ctx, payload) } diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..c8b1951 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + + "git.lehouerou.net/laurent/arlo-go" + log "github.com/sirupsen/logrus" +) + +func main() { + ctx := context.Background() + a := arlo.NewArlo() + err := a.Login(ctx, "hass@lehouerou.net", "TiPXMVLUeZfUg6RrmwzK") + if err != nil { + log.Errorf("login: %v", err) + return + } + + for _, device := range a.Cameras { + log.Infof("%s", device.DeviceName) + err := device.On(ctx) + if err != nil { + log.Errorf("setting camera %s on: %v", device.DeviceName, err) + } + } + + select {} + +} diff --git a/const.go b/const.go index f84c79a..716d390 100644 --- a/const.go +++ b/const.go @@ -9,7 +9,7 @@ const ( DeviceTypeSiren = "siren" TransIdPrefix = "web" - BaseUrl = "https://my.arlo.com/hmsweb" + BaseUrl = "https://myapi.arlo.com/hmsweb" // TODO: Implement all of the following urls. There are many here I don't have devices for. :/ ActiveAutomationUri = "/users/devices/automation/active" diff --git a/events_stream.go b/events_stream.go index c1b8d35..334f741 100644 --- a/events_stream.go +++ b/events_stream.go @@ -17,25 +17,20 @@ type eventStream struct { Events chan *sse.Event Error chan error Verbose bool - DisconnectedChan chan interface{} + disconnectedChan chan interface{} once *sync.Once - subscribers map[string]chan *EventStreamResponse - subscribersMutex sync.RWMutex - - errorsubsribers map[string]chan error - errorMutex sync.RWMutex + transactionSubscribers map[string]chan *EventStreamResponse + transactionSubscribersMutex sync.RWMutex } func newEventStream(url string, client *http.Client) *eventStream { e := &eventStream{ - Events: make(chan *sse.Event), - subscribers: make(map[string]chan *EventStreamResponse), - subscribersMutex: sync.RWMutex{}, - errorsubsribers: make(map[string]chan error), - errorMutex: sync.RWMutex{}, - DisconnectedChan: make(chan interface{}), - once: new(sync.Once), + Events: make(chan *sse.Event), + transactionSubscribers: make(map[string]chan *EventStreamResponse), + transactionSubscribersMutex: sync.RWMutex{}, + disconnectedChan: make(chan interface{}), + once: new(sync.Once), } SSEClient := sse.NewClient(url) @@ -49,17 +44,19 @@ func newEventStream(url string, client *http.Client) *eventStream { func (e *eventStream) disconnect() { e.once.Do(func() { - close(e.DisconnectedChan) + close(e.disconnectedChan) }) } func (e *eventStream) listen(ctx context.Context) (chan bool, error) { - connectedChan := make(chan bool) + err := e.SSEClient.SubscribeChanRaw(e.Events) if err != nil { return nil, fmt.Errorf("failed to subscribe to seeclient") } + connectedChan := make(chan bool) + go func() { for { select { @@ -104,14 +101,14 @@ func (e *eventStream) listen(ctx context.Context) (chan bool, error) { e.disconnect() continue } - e.subscribersMutex.RLock() - subscriber, ok := e.subscribers[notifyResponse.TransId] - e.subscribersMutex.RUnlock() + e.transactionSubscribersMutex.RLock() + subscriber, ok := e.transactionSubscribers[notifyResponse.TransId] + e.transactionSubscribersMutex.RUnlock() if ok { subscriber <- ¬ifyResponse } - case <-e.DisconnectedChan: + case <-e.disconnectedChan: connectedChan <- false return } @@ -122,28 +119,16 @@ func (e *eventStream) listen(ctx context.Context) (chan bool, error) { } func (e *eventStream) unsubscribe(transId string) { - e.subscribersMutex.Lock() - if c, ok := e.subscribers[transId]; ok { + e.transactionSubscribersMutex.Lock() + if c, ok := e.transactionSubscribers[transId]; ok { close(c) - delete(e.subscribers, transId) + delete(e.transactionSubscribers, transId) } - e.subscribersMutex.Unlock() - - e.errorMutex.Lock() - if c, ok := e.errorsubsribers[transId]; ok { - close(c) - delete(e.errorsubsribers, transId) - } - e.errorMutex.Unlock() - + e.transactionSubscribersMutex.Unlock() } -func (e *eventStream) subscribe(transId string, subscriber chan *EventStreamResponse, errorChan chan error) { - e.subscribersMutex.Lock() - e.subscribers[transId] = subscriber - e.subscribersMutex.Unlock() - - e.errorMutex.Lock() - e.errorsubsribers[transId] = errorChan - e.errorMutex.Unlock() +func (e *eventStream) subscribe(transId string, subscriber chan *EventStreamResponse) { + e.transactionSubscribersMutex.Lock() + e.transactionSubscribers[transId] = subscriber + e.transactionSubscribersMutex.Unlock() }