From 049f3c5652ad472feb5d68ab641704c077ae34af Mon Sep 17 00:00:00 2001 From: Jeff Walter Date: Thu, 20 Sep 2018 17:38:01 -0500 Subject: [PATCH] More refactoring, internal API almost stable. Added a shitload of external Arlo APIs! The library is at about 60% parity with the Python version. --- arlo.go | 48 ++-- arlobaby.go | 506 +++++++++++++++++++++++++++++++++++++ basestation.go | 380 +++++++++++++++++++--------- camera.go | 274 ++++++++++++++++++-- devices.go | 6 +- internal/request/client.go | 2 + library.go | 59 ++--- responses.go | 21 +- types.go | 200 ++++++++++++--- util.go | 19 +- 10 files changed, 1250 insertions(+), 265 deletions(-) create mode 100644 arlobaby.go diff --git a/arlo.go b/arlo.go index d057378..c8184d6 100644 --- a/arlo.go +++ b/arlo.go @@ -15,7 +15,8 @@ type Arlo struct { Cameras Cameras } -func newArlo(user string, pass string) *Arlo { +func newArlo(user string, pass string) (arlo *Arlo) { + c, _ := request.NewClient(BaseUrl) // Add important headers. @@ -31,14 +32,15 @@ func newArlo(user string, pass string) *Arlo { } } -func Login(user string, pass string) (*Arlo, error) { - a := newArlo(user, pass) +func Login(user string, pass string) (arlo *Arlo, err error) { + arlo = newArlo(user, pass) - body := map[string]string{"email": a.user, "password": a.pass} - resp, err := a.post(LoginUri, "", body, nil) - if err != nil { - return nil, errors.WithMessage(err, "login request failed") + body := map[string]string{"email": arlo.user, "password": arlo.pass} + resp, err := arlo.post(LoginUri, "", body, nil) + if err := checkHttpRequest(resp, err, "login request failed"); err != nil { + return nil, err } + defer resp.Body.Close() var loginResponse LoginResponse if err := resp.Decode(&loginResponse); err != nil { @@ -47,33 +49,33 @@ func Login(user string, pass string) (*Arlo, error) { if loginResponse.Success { // Cache the auth token. - a.client.BaseHttpHeader.Add("Authorization", loginResponse.Data.Token) + arlo.client.BaseHttpHeader.Add("Authorization", loginResponse.Data.Token) // Save the account info with the arlo struct. - a.Account = loginResponse.Data + arlo.Account = loginResponse.Data // Get the devices, which also caches them on the arlo object. - if _, err := a.GetDevices(); err != nil { + if _, err := arlo.GetDevices(); err != nil { return nil, errors.WithMessage(err, "failed to login") } } else { return nil, errors.New("failed to login") } - return a, nil + return arlo, nil } func (a *Arlo) Logout() error { resp, err := a.put(LogoutUri, "", nil, nil) - return checkRequest(*resp, err, "failed to logout") + return checkRequest(resp, err, "failed to logout") } // GetDevices returns an array of all devices. // When you call Login, this method is called and all devices are cached in the arlo object. -func (a *Arlo) GetDevices() (Devices, error) { +func (a *Arlo) GetDevices() (devices Devices, err error) { resp, err := a.get(DevicesUri, "", nil) - if err != nil { - return nil, errors.WithMessage(err, "failed to get devices") + if err := checkHttpRequest(resp, err, "failed to get devices"); err != nil { + return nil, err } defer resp.Body.Close() @@ -82,14 +84,14 @@ func (a *Arlo) GetDevices() (Devices, error) { return nil, err } - if len(deviceResponse.Data) == 0 { - return nil, errors.New("no devices found") - } - if !deviceResponse.Success { return nil, errors.New("failed to get devices") } + if len(deviceResponse.Data) == 0 { + return nil, errors.New("no devices found") + } + for i := range deviceResponse.Data { deviceResponse.Data[i].arlo = a } @@ -118,20 +120,20 @@ func (a *Arlo) GetDevices() (Devices, error) { // UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given. func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) error { resp, err := a.post(DeviceDisplayOrderUri, "", d, nil) - return checkRequest(*resp, err, "failed to display order") + return checkRequest(resp, err, "failed to display order") } // UpdateProfile takes a first and last name, and updates the user profile with that information. func (a *Arlo) UpdateProfile(firstName, lastName string) error { body := map[string]string{"firstName": firstName, "lastName": lastName} resp, err := a.put(UserProfileUri, "", body, nil) - return checkRequest(*resp, err, "failed to update profile") + return checkRequest(resp, err, "failed to update profile") } func (a *Arlo) UpdatePassword(pass string) error { body := map[string]string{"currentPassword": a.pass, "newPassword": pass} resp, err := a.post(UserChangePasswordUri, "", body, nil) - if err := checkRequest(*resp, err, "failed to update password"); err != nil { + if err := checkRequest(resp, err, "failed to update password"); err != nil { return err } @@ -142,5 +144,5 @@ func (a *Arlo) UpdatePassword(pass string) error { func (a *Arlo) UpdateFriends(f Friend) error { resp, err := a.put(UserFriendsUri, "", f, nil) - return checkRequest(*resp, err, "failed to update friends") + return checkRequest(resp, err, "failed to update friends") } diff --git a/arlobaby.go b/arlobaby.go new file mode 100644 index 0000000..97b1574 --- /dev/null +++ b/arlobaby.go @@ -0,0 +1,506 @@ +package arlo + +import ( + "fmt" + "time" + + "github.com/pkg/errors" +) + +/* +The methods in this file are all related to Arlo Baby (afaik). +They may apply to other camera types that have audio playback or nightlight capabilities. +*/ + +/* + The follow methods are all related to the audio features of Arlo Baby. +*/ + +// SetVolume sets the volume of the audio playback to a level from 0-100. +func (c *Camera) SetVolume(volume int) (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: SpeakerProperties{ + Speaker: VolumeProperties{ + Mute: false, + Volume: volume, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to set audio volume" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +// Mute mutes the audio playback. +func (c *Camera) Mute() (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: SpeakerProperties{ + Speaker: VolumeProperties{ + Mute: true, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to mute audio" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +// UnMute un-mutes the audio playback. +func (c *Camera) UnMute() (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: SpeakerProperties{ + Speaker: VolumeProperties{ + Mute: false, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to un-mute audio" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +// Play plays an audio track, specified by the track ID, from a given position starting from 0 seconds. +func (c *Camera) Play(trackId string, position int) error { + + // Defaulting to 'hugh little baby', which is a supplied track. Hopefully, the ID is the same for everyone. + if trackId == "" { + trackId = "2391d620-e491-4412-99f6-e9a40d6046ed" + } + + if position < 0 { + position = 0 + } + + payload := EventStreamPayload{ + Action: "playTrack", + Resource: "audioPlayback/player", + PublishResponse: false, + Properties: PlayTrackProperties{trackId, position}, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to play audio" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return errors.WithMessage(err, msg) + } + + if err := b.NotifyEventStream(payload, msg); err != nil { + return errors.WithMessage(err, msg) + } + return nil +} + +// Pause pauses audio playback. +func (c *Camera) Pause() error { + payload := EventStreamPayload{ + Action: "pause", + Resource: "audioPlayback/player", + PublishResponse: false, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to pause audio" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return errors.WithMessage(err, msg) + } + + if err := b.NotifyEventStream(payload, msg); err != nil { + return errors.WithMessage(err, msg) + } + return nil +} + +// Next moves audio playback to the next track. +func (c *Camera) Next() error { + payload := EventStreamPayload{ + Action: "nextTrack", + Resource: "audioPlayback/player", + PublishResponse: false, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to skip audio" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return errors.WithMessage(err, msg) + } + + if err := b.NotifyEventStream(payload, msg); err != nil { + return errors.WithMessage(err, msg) + } + return nil +} + +// Shuffle toggles the audio play back mode to shuffle or not. +func (c *Camera) Shuffle(on bool) (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: "audioPlayback/config", + PublishResponse: true, + Properties: ShuffleProperties{ + Config: BaseShuffleProperties{ + ShuffleActive: on, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + var msg string + if on { + msg = "failed to enable shuffle" + } else { + msg = "failed to disable shuffle" + } + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +func (c *Camera) Continuous() (response *EventStreamResponse, err error) { + return c.SetLoopBackMode("continuous") +} + +func (c *Camera) SingleTrack() (response *EventStreamResponse, err error) { + return c.SetLoopBackMode("singleTrack") +} + +func (c *Camera) SetLoopBackMode(loopbackMode string) (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: "audioPlayback/config", + PublishResponse: true, + Properties: LoopbackModeProperties{ + Config: BaseLoopbackModeProperties{ + LoopbackMode: loopbackMode, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to set loop back mode to %s" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, fmt.Sprintf(msg, loopbackMode)) + } + return b.makeEventStreamRequest(payload, msg) +} + +func (c *Camera) GetAudioPlayback() (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "get", + Resource: "audioPlayback", + PublishResponse: false, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to get audio playback" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +func (c *Camera) EnableSleepTimer(sleepTime int64 /* milliseconds */, sleepTimeRel int) (response *EventStreamResponse, err error) { + if sleepTime == 0 { + sleepTime = 300 + (time.Now().UnixNano() / 1000000) /* milliseconds */ + } + + payload := EventStreamPayload{ + Action: "set", + Resource: "audioPlayback/config", + PublishResponse: true, + Properties: SleepTimerProperties{ + Config: BaseSleepTimerProperties{ + SleepTime: sleepTime, + SleepTimeRel: sleepTimeRel, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to enable sleep timer" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +func (c *Camera) DisableSleepTimer(sleepTimeRel int) (response *EventStreamResponse, err error) { + if sleepTimeRel == 0 { + sleepTimeRel = 300 + } + + payload := EventStreamPayload{ + Action: "set", + Resource: "audioPlayback/config", + PublishResponse: true, + Properties: SleepTimerProperties{ + Config: BaseSleepTimerProperties{ + SleepTime: 0, + SleepTimeRel: sleepTimeRel, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to disable sleep timer" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +/* +The follow methods are all related to the nightlight features of Arlo Baby. + +NOTE: The current state is in: cameras[0]["properties"][0]["nightLight"] returned from the basestation.GetAssociatedCamerasState() method. +*/ +func (c *Camera) NightLight(on bool) (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: NightLightProperties{ + NightLight: BaseNightLightProperties{ + Enabled: on, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + var msg string + if on { + msg = "failed to turn night light on" + } else { + msg = "failed to turn night light off" + } + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +func (c *Camera) SetNightLightBrightness(level int) (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: NightLightProperties{ + NightLight: BaseNightLightProperties{ + Brightness: level, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to set night light brightness" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +// SetNightLightMode set the night light mode. Valid values are: "rainbow" or "rgb". +func (c *Camera) SetNightLightMode(mode string) (response *EventStreamResponse, err error) { + msg := "failed to set night light brightness" + + if mode != "rainbow" && mode != "rgb" { + return nil, errors.WithMessage(errors.New("mode can only be \"rainbow\" or \"rgb\""), msg) + } + + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: NightLightProperties{ + NightLight: BaseNightLightProperties{ + Mode: mode, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +// SetNightLightColor sets the night light color to the RGB value specified by the three parameters, which have valid values from 0-255. +func (c *Camera) SetNightLightColor(red, blue, green int) (response *EventStreamResponse, err error) { + // Sanity check; if the values are above or below the allowed limits, set them to their limit. + if red < 0 { + red = 0 + } else if red > 255 { + red = 255 + } + if blue < 0 { + blue = 0 + } else if blue > 255 { + blue = 255 + } + if green < 0 { + green = 0 + } else if green > 255 { + green = 255 + } + + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: NightLightProperties{ + NightLight: BaseNightLightProperties{ + RGB: NightLightRGBProperties{ + Red: red, + Blue: blue, + Green: green, + }, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to set night light color" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +func (c *Camera) EnableNightLightTimer(sleepTime int64 /* milliseconds */, sleepTimeRel int) (response *EventStreamResponse, err error) { + if sleepTime == 0 { + sleepTime = 300 + (time.Now().UnixNano() / 1000000) /* milliseconds */ + } + + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: NightLightProperties{ + NightLight: BaseNightLightProperties{ + SleepTime: sleepTime, + SleepTimeRel: sleepTimeRel, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to enable night light timer" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +func (c *Camera) DisableNightLightTimer(sleepTimeRel int) (response *EventStreamResponse, err error) { + if sleepTimeRel == 0 { + sleepTimeRel = 300 + } + + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: NightLightProperties{ + NightLight: BaseNightLightProperties{ + SleepTime: 0, + SleepTimeRel: sleepTimeRel, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to disable night light timer" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} diff --git a/basestation.go b/basestation.go index 73a47cd..685833e 100644 --- a/basestation.go +++ b/basestation.go @@ -2,33 +2,12 @@ package arlo import ( "fmt" + "time" "github.com/pkg/errors" ) -type BaseStationMetadata 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"` - TimeZone string `json:"timeZone"` - OlsonTimeZone string `json:"olsonTimeZone"` - UploadBandwidthSaturated bool `json:"uploadBandwidthSaturated"` - AntiFlicker map[string]int `json:"antiFlicker"` - LowBatteryAlert map[string]bool `json:"lowBatteryAlert"` - LowSignalAlert map[string]bool `json:"lowSignalAlert"` - Claimed bool `json:"claimed"` - TimeSyncState string `json:"timeSyncState"` - Connectivity []struct { - Type string `json:"type"` - Connected bool `json:"connected"` - } `json:"connectivity"` -} +const eventStreamTimeout = 10 * 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. @@ -40,101 +19,7 @@ type Basestation struct { // Basestations is an array of Basestation objects. type Basestations []Basestation -func (b *Basestation) Subscribe() error { - b.eventStream = NewEventStream(BaseUrl+fmt.Sprintf(SubscribeUri, b.arlo.Account.Token), b.arlo.client.HttpClient) - connected := b.eventStream.Listen() - -outoffor: - for { - // TODO: Need to add a timeout here. - // We blocking here because we can't really do anything with the event stream until we're connected. - // Once we have confirmation that we're connected to the event stream, we will "subscribe" to events. - select { - case b.eventStream.Connected = <-connected: - if b.eventStream.Connected { - break outoffor - } else { - // TODO: What do we do if Connected is false? Probably need retry logic here. - break - } - case <-b.eventStream.Close: - return errors.New("failed to subscribe to the event stream") - } - } - - // This is a crude (temporary?) way to monitor the connection. It's late and I'm tired, so this will probably go away. - go func() { - outoffor: - for { - select { - case b.eventStream.Connected = <-connected: - // TODO: What do we do if Connected is false? Probably need retry logic here. - break outoffor - case <-b.eventStream.Close: - // TODO: Figure out what to do here if the eventStream is closed. (Panic?) - return - } - } - }() - - payload := Payload{ - 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, "failed to subscribe to the event stream"); err != nil { - return err - } - - return nil -} - -func (b *Basestation) Unsubscribe() error { - // TODO: Close channel to stop EventStream. - //return errors.New("not implemented") - if b.eventStream != nil { - close(b.eventStream.Close) - } - return nil -} - -func (b *Basestation) IsConnected() error { - if !b.eventStream.Connected { - return errors.New("basestation not connected to event stream") - } - return nil -} - -func (b *Basestation) GetState() (*EventStreamResponse, error) { - - payload := Payload{ - Action: "get", - Resource: "basestation", - PublishResponse: false, - From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), - To: b.DeviceId, - } - - return b.makeEventStreamRequest(payload, "failed to get basestation state") -} - -func (b *Basestation) GetAssociatedCamerasState() (*EventStreamResponse, error) { - payload := Payload{ - Action: "get", - Resource: "cameras", - PublishResponse: false, - From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), - To: b.DeviceId, - } - - return b.makeEventStreamRequest(payload, "failed to get associated cameras state") -} - -func (b *Basestation) makeEventStreamRequest(payload Payload, msg string) (*EventStreamResponse, error) { +func (b *Basestation) makeEventStreamRequest(payload EventStreamPayload, msg string) (response *EventStreamResponse, err error) { transId := genTransId() payload.TransId = transId @@ -145,17 +30,264 @@ func (b *Basestation) makeEventStreamRequest(payload Payload, msg string) (*Even b.eventStream.Subscriptions[transId] = make(chan *EventStreamResponse) defer close(b.eventStream.Subscriptions[transId]) - resp, err := b.arlo.post(fmt.Sprintf(NotifyUri, b.DeviceId), b.XCloudId, payload, nil) - if err := checkRequest(*resp, err, msg); err != nil { + if err := b.NotifyEventStream(payload, msg); err != nil { return nil, err } + timer := time.NewTimer(eventStreamTimeout) + defer timer.Stop() + select { - case eventStreamResponse := <-b.eventStream.Subscriptions[transId]: - return eventStreamResponse, nil + case <-timer.C: + err = fmt.Errorf("event stream response timed out after %.0f second", eventStreamTimeout.Seconds()) + return nil, errors.WithMessage(err, msg) + case response := <-b.eventStream.Subscriptions[transId]: + return response, nil case err = <-b.eventStream.Error: - return nil, errors.Wrap(err, "failed to get basestation") + return nil, errors.Wrap(err, msg) case <-b.eventStream.Close: - return nil, errors.New("event stream was closed before response was read") + err = errors.New("event stream was closed before response was read") + return nil, errors.WithMessage(err, msg) } } + +// 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 +} + +func (b *Basestation) IsConnected() error { + if !b.eventStream.Connected { + return errors.New("basestation not connected to event stream") + } + return nil +} + +func (b *Basestation) Subscribe() error { + b.eventStream = NewEventStream(BaseUrl+fmt.Sprintf(SubscribeUri, b.arlo.Account.Token), b.arlo.client.HttpClient) + connected := b.eventStream.Listen() + +forLoop: + for { + // We blocking here because we can't really do anything with the event stream until we're connected. + // Once we have confirmation that we're connected to the event stream, we will "subscribe" to events. + select { + case b.eventStream.Connected = <-connected: + if b.eventStream.Connected { + break forLoop + } else { + return errors.New("failed to subscribe to the event stream") + } + case <-b.eventStream.Close: + return errors.New("failed to subscribe to the event stream") + } + } + + if err := b.Ping(); err != nil { + return errors.WithMessage(err, "failed to subscribe to the event stream") + } + + // The Arlo event stream requires a "ping" every 30s. + go func() { + for { + time.Sleep(30 * time.Second) + if err := b.Ping(); err != nil { + b.Unsubscribe() + break + } + } + }() + + return nil +} + +func (b *Basestation) Unsubscribe() error { + // Close channel to stop EventStream. + if b.eventStream != nil { + close(b.eventStream.Close) + } + 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, "failed to ping the event stream"); err != nil { + return err + } + return nil +} + +func (b *Basestation) NotifyEventStream(payload EventStreamPayload, msg string) error { + resp, err := b.arlo.post(fmt.Sprintf(NotifyUri, b.DeviceId), b.XCloudId, payload, nil) + if err := checkRequest(resp, err, msg); err != nil { + return errors.WithMessage(err, "failed to notify event stream") + } + defer resp.Body.Close() + + return nil +} + +func (b *Basestation) GetState() (response *EventStreamResponse, err error) { + + payload := EventStreamPayload{ + Action: "get", + Resource: "basestation", + PublishResponse: false, + From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), + To: b.DeviceId, + } + + return b.makeEventStreamRequest(payload, "failed to get basestation state") +} + +func (b *Basestation) GetAssociatedCamerasState() (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "get", + Resource: "cameras", + PublishResponse: false, + From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), + To: b.DeviceId, + } + + return b.makeEventStreamRequest(payload, "failed to get associated cameras state") +} + +func (b *Basestation) GetRules() (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "get", + Resource: "rules", + PublishResponse: false, + From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), + To: b.DeviceId, + } + + return b.makeEventStreamRequest(payload, "failed to get rules") +} + +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, "failed to get schedule") +} + +// 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) (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: "schedule", + PublishResponse: true, + Properties: BasestationScheduleProperties{ + Active: active, + }, + From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), + To: b.DeviceId, + } + + return b.makeEventStreamRequest(payload, "failed to set schedule") +} + +func (b *Basestation) GetModes() (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "get", + Resource: "modes", + PublishResponse: false, + From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), + To: b.DeviceId, + } + + return b.makeEventStreamRequest(payload, "failed to get modes") +} + +func (b *Basestation) SetCustomMode(mode string) (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: "modes", + PublishResponse: true, + Properties: BasestationModeProperties{ + Active: mode, + }, + From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), + To: b.DeviceId, + } + + return b.makeEventStreamRequest(payload, "failed to set mode") +} + +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, "failed to set mode") +} + +func (b *Basestation) Arm() (response *EventStreamResponse, err error) { + return b.SetCustomMode("mode1") +} + +func (b *Basestation) Disarm() (response *EventStreamResponse, err error) { + return b.SetCustomMode("mode0") +} + +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, "failed to get modes") +} + +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, "failed to get modes") +} diff --git a/camera.go b/camera.go index 40e7b4a..d79da48 100644 --- a/camera.go +++ b/camera.go @@ -14,13 +14,102 @@ type Camera Device // Cameras is an array of Camera objects. type Cameras []Camera +// Find returns a camera with the device id passed in. +func (cs *Cameras) Find(deviceId string) *Camera { + for _, c := range *cs { + if c.DeviceId == deviceId { + return &c + } + } + + 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, + } + + msg := "failed to turn camera on" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +// On turns a camera off; meaning it won't detect and record events. +func (c *Camera) Off() (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: CameraProperties{ + PrivacyActive: true, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to turn camera off" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +// 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) { + // Sanity check; if the values are above or below the allowed limits, set them to their limit. + if brightness < -2 { + brightness = -2 + } else if brightness > 2 { + brightness = 2 + } + + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: CameraProperties{ + Brightness: brightness, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to set camera brightness" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + // StartStream returns a json object containing the rtmps url to the requested video stream. // You will need the to install a library to handle streaming of this protocol: https://pypi.python.org/pypi/python-librtmp // // The request to /users/devices/startStream returns: // NOTE: { "url":"rtsp://vzwow09-z2-prod.vz.netgear.com:80/vzmodulelive?egressToken=b1b4b675_ac03_4182_9844_043e02a44f71&userAgent=web&cameraId=48B4597VD8FF5_1473010750131" } -func (c *Camera) StartStream() (*StreamResponse, error) { - body := Payload{ +func (c *Camera) StartStream() (response *StreamResponse, err error) { + payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("cameras/%s", c.DeviceId), PublishResponse: true, @@ -33,55 +122,194 @@ func (c *Camera) StartStream() (*StreamResponse, error) { To: c.ParentId, } - resp, err := c.arlo.post(DeviceStartStreamUri, c.XCloudId, body, nil) - if err != nil { - return nil, errors.WithMessage(err, "failed to start stream") + msg := "failed to start stream" + + resp, err := c.arlo.post(DeviceStartStreamUri, c.XCloudId, payload, nil) + if err := checkHttpRequest(resp, err, msg); err != nil { + return nil, err } defer resp.Body.Close() - var streamResponse StreamResponse - if err := resp.Decode(&streamResponse); err != nil { + if err := resp.Decode(response); err != nil { return nil, err } - if !streamResponse.Success { - return nil, errors.WithMessage(errors.New("status was false"), "failed to start stream") + if !response.Success { + return nil, errors.WithMessage(errors.New("status was false"), msg) } - streamResponse.Data.Url = strings.Replace(streamResponse.Data.Url, "rtsp://", "rtsps://", 1) + response.Data.Url = strings.Replace(response.Data.Url, "rtsp://", "rtsps://", 1) - return &streamResponse, nil + return response, nil } // TakeSnapshot causes the camera to record a snapshot. -func (c *Camera) TakeSnapshot() (*StreamResponse, error) { - streamResponse, err := c.StartStream() +func (c *Camera) TakeSnapshot() (response *StreamResponse, err error) { + msg := "failed to take snapshot" + + response, err = c.StartStream() if err != nil { - return nil, errors.WithMessage(err, "failed to take snapshot") + return nil, errors.WithMessage(err, msg) } body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone} resp, err := c.arlo.post(DeviceTakeSnapshotUri, c.XCloudId, body, nil) - if err := checkRequest(*resp, err, "failed to update device name"); err != nil { - return nil, errors.WithMessage(err, "failed to take snapshot") + if err := checkRequest(resp, err, "failed to update device name"); err != nil { + return nil, errors.WithMessage(err, msg) } - return streamResponse, nil + return response, nil } // StartRecording causes the camera to start recording and returns a url that you must start reading from using ffmpeg // or something similar. -func (c *Camera) StartRecording() (*StreamResponse, error) { - streamResponse, err := c.StartStream() +func (c *Camera) StartRecording() (response *StreamResponse, err error) { + msg := "failed to start recording" + + response, err = c.StartStream() if err != nil { - return nil, errors.WithMessage(err, "failed to start recording") + return nil, errors.WithMessage(err, msg) } body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone} resp, err := c.arlo.post(DeviceStartRecordUri, c.XCloudId, body, nil) - if err := checkRequest(*resp, err, "failed to update device name"); err != nil { - return nil, errors.WithMessage(err, "failed to start recording") + if err := checkRequest(resp, err, "failed to update device name"); err != nil { + return nil, errors.WithMessage(err, msg) } - return streamResponse, nil + return response, nil +} + +func (c *Camera) EnableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: MotionDetectionProperties{ + BaseDetectionProperties: BaseDetectionProperties{ + Armed: true, + Sensitivity: sensitivity, + Zones: zones, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to enable motion alerts" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +func (c *Camera) DisableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: MotionDetectionProperties{ + BaseDetectionProperties: BaseDetectionProperties{ + Armed: false, + Sensitivity: sensitivity, + Zones: zones, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to enable motion alerts" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +func (c *Camera) EnableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: AudioDetectionProperties{ + BaseDetectionProperties: BaseDetectionProperties{ + Armed: true, + Sensitivity: sensitivity, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to enable audio alerts" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +func (c *Camera) DisableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: AudioDetectionProperties{ + BaseDetectionProperties: BaseDetectionProperties{ + Armed: false, + Sensitivity: sensitivity, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to disable audio alerts" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) +} + +// action: disabled OR recordSnapshot OR recordVideo +func (c *Camera) SetAlertNotificationMethods(action string, email, push bool) (response *EventStreamResponse, err error) { + payload := EventStreamPayload{ + Action: "set", + Resource: fmt.Sprintf("cameras/%s", c.DeviceId), + PublishResponse: true, + Properties: EventActionProperties{ + BaseEventActionProperties: BaseEventActionProperties{ + ActionType: action, + StopType: "timeout", + Timeout: 15, + EmailNotification: EmailNotification{ + Enabled: email, + EmailList: []string{"__OWNER_EMAIL__"}, + PushNotification: push, + }, + }, + }, + From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), + To: c.ParentId, + } + + msg := "failed to set alert notification methods" + + b := c.arlo.Basestations.Find(c.ParentId) + if b == nil { + err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId) + return nil, errors.WithMessage(err, msg) + } + return b.makeEventStreamRequest(payload, msg) } diff --git a/devices.go b/devices.go index 2dff301..4b293ae 100644 --- a/devices.go +++ b/devices.go @@ -10,7 +10,7 @@ type Device struct { ArloMobilePlanThreshold int `json:"arloMobilePlanThreshold"` Connectivity Connectivity `json:"connectivity"` CriticalBatteryState bool `json:"criticalBatteryState"` - DateCreated float64 `json:"dateCreated"` + DateCreated int64 `json:"dateCreated"` DeviceId string `json:"deviceId"` DeviceName string `json:"deviceName"` DeviceType string `json:"deviceType"` @@ -19,7 +19,7 @@ type Device struct { InterfaceVersion string `json:"interfaceVersion"` InterfaceSchemaVer string `json:"interfaceSchemaVer"` LastImageUploaded string `json:"lastImageUploaded"` - LastModified float64 `json:"lastModified"` + LastModified int64 `json:"lastModified"` MigrateActivityZone bool `json:"migrateActivityZone"` MobileCarrier string `json:"mobileCarrier"` MobileTrialUsed bool `json:"mobileTrialUsed"` @@ -122,5 +122,5 @@ func (d *Device) UpdateDeviceName(name string) error { body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId} resp, err := d.arlo.put(DeviceRenameUri, d.XCloudId, body, nil) - return checkRequest(*resp, err, "failed to update device name") + return checkRequest(resp, err, "failed to update device name") } diff --git a/internal/request/client.go b/internal/request/client.go index 328149b..87c2806 100644 --- a/internal/request/client.go +++ b/internal/request/client.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "io" + "log" "net/http" "net/http/cookiejar" "net/url" @@ -80,6 +81,7 @@ func (c *Client) newRequest(method string, uri string, body interface{}, header return nil, errors.Wrap(err, "failed to create request object") } } + log.Printf("\n\nBODY (%s): %s\n\n", uri, buf) u := c.BaseURL.String() + uri req, err := http.NewRequest(method, u, buf) diff --git a/library.go b/library.go index 706c414..5a4ec33 100644 --- a/library.go +++ b/library.go @@ -2,8 +2,6 @@ package arlo import ( "time" - - "github.com/pkg/errors" ) // LibraryMetaData is the library meta data. @@ -36,37 +34,34 @@ type Recording struct { type Library []Recording -func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (*LibraryMetaDataResponse, error) { +func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (response *LibraryMetaDataResponse, err error) { body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")} resp, err := a.post(LibraryMetadataUri, "", body, nil) - - if err != nil { - return nil, errors.WithMessage(err, "failed to get library metadata") - } - - var libraryMetaDataResponse LibraryMetaDataResponse - if err := resp.Decode(&libraryMetaDataResponse); err != nil { + if err := checkHttpRequest(resp, err, "failed to get library metadata"); err != nil { return nil, err } - return &libraryMetaDataResponse, nil + if err := resp.Decode(response); err != nil { + return nil, err + } + + return response, nil } -func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (*LibraryResponse, error) { +func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (response *LibraryResponse, err error) { body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")} resp, err := a.post(LibraryUri, "", body, nil) - if err != nil { - return nil, errors.WithMessage(err, "failed to get library") - } - - var libraryResponse LibraryResponse - if err := resp.Decode(&libraryResponse); err != nil { + if err := checkHttpRequest(resp, err, "failed to get library"); err != nil { return nil, err } - return &libraryResponse, nil + if err := resp.Decode(response); err != nil { + return nil, err + } + + return response, nil } /* @@ -76,20 +71,11 @@ func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (*LibraryResponse, error) NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required. */ -func (a *Arlo) DeleteRecording(r Recording) (*Error, error) { +func (a *Arlo) DeleteRecording(r Recording) error { body := map[string]Library{"data": {r}} resp, err := a.post(LibraryRecycleUri, "", body, nil) - if err != nil { - return nil, errors.WithMessage(err, "failed to delete recording") - } - - var status Error - if err := resp.Decode(&status); err != nil { - return nil, err - } - - return &status, nil + return checkRequest(resp, err, "failed to delete recording") } /* @@ -99,18 +85,9 @@ func (a *Arlo) DeleteRecording(r Recording) (*Error, error) { NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required. */ -func (a *Arlo) BatchDeleteRecordings(l Library) (*Error, error) { +func (a *Arlo) BatchDeleteRecordings(l Library) error { body := map[string]Library{"data": l} resp, err := a.post(LibraryRecycleUri, "", body, nil) - if err != nil { - return nil, errors.WithMessage(err, "failed to delete recordings") - } - - var status Error - if err := resp.Decode(&status); err != nil { - return nil, err - } - - return &status, nil + return checkRequest(resp, err, "failed to delete recordings") } diff --git a/responses.go b/responses.go index 747c7bc..0af51e3 100644 --- a/responses.go +++ b/responses.go @@ -3,42 +3,37 @@ package arlo // LoginResponse is an intermediate struct used when parsing data from the Login() call. type LoginResponse struct { Data Account - Error + Status } // DeviceResponse is an intermediate struct used when parsing data from the GetDevices() call. type DeviceResponse struct { Data Devices - Error + Status } // LibraryMetaDataResponse is an intermediate struct used when parsing data from the GetLibraryMetaData() call. type LibraryMetaDataResponse struct { Data LibraryMetaData - Error + Status } type LibraryResponse struct { Data Library - Error + Status } type StreamResponse struct { Data StreamUrl - Error + Status } type RecordingResponse struct { Data StreamUrl - Error + Status } type EventStreamResponse struct { - Action string `json:"action,omitempty"` - Resource string `json:"resource,omitempty"` - Properties interface{} `json:"properties,omitempty"` - TransId string `json:"transId"` - From string `json:"from"` - To string `json:"to"` - Status string `json:"status"` + EventStreamPayload + Status string `json:"status,omitempty"` } diff --git a/types.go b/types.go index 40f292b..345eb7d 100644 --- a/types.go +++ b/types.go @@ -28,50 +28,72 @@ type FullName struct { // Account is the account data. type Account struct { - UserId string `json:"userId"` - Email string `json:"email"` - Token string `json:"token"` - PaymentId string `json:"paymentId"` - Authenticated uint32 `json:"authenticated"` - AccountStatus string `json:"accountStatus"` - SerialNumber string `json:"serialNumber"` - CountryCode string `json:"countryCode"` - TocUpdate bool `json:"tocUpdate"` - PolicyUpdate bool `json:"policyUpdate"` - ValidEmail bool `json:"validEmail"` - Arlo bool `json:"arlo"` - DateCreated float64 `json:"dateCreated"` + UserId string `json:"userId"` + Email string `json:"email"` + Token string `json:"token"` + PaymentId string `json:"paymentId"` + Authenticated uint32 `json:"authenticated"` + AccountStatus string `json:"accountStatus"` + SerialNumber string `json:"serialNumber"` + CountryCode string `json:"countryCode"` + TocUpdate bool `json:"tocUpdate"` + PolicyUpdate bool `json:"policyUpdate"` + ValidEmail bool `json:"validEmail"` + Arlo bool `json:"arlo"` + DateCreated int64 `json:"dateCreated"` } +// Friend is the account data for non-primary account holders designated as friends. type Friend struct { FirstName string `json:"firstName"` LastName string `json:"lastName"` Devices DeviceOrder `json:"devices"` - LastModified float64 `json:"lastModified"` + LastModified int64 `json:"lastModified"` AdminUser bool `json:"adminUser"` Email string `json:"email"` Id string `json:"id"` } -// Owner is part of the Device data. +// Connectivity is part of the Device data. type Connectivity struct { - ActiveNetwork string `json:"activeNetwork"` - APN string `json:"apn"` - CarrierFw string `json:"carrierFw"` + ActiveNetwork string `json:"activeNetwork,omitempty"` + APN string `json:"apn,omitempty"` + CarrierFw string `json:"carrierFw,omitempty"` Connected bool `json:"connected"` - FWVersion string `json:"fwVersion"` - ICCID string `json:"iccid"` - IMEI string `json:"imei"` - MEPStatus string `json:"mepStatus"` - MSISDN string `json:"msisdn"` - NetworkMode string `json:"networkMode"` - NetworkName string `json:"networkName"` - RFBand int `json:"rfBand"` + FWVersion string `json:"fwVersion,omitempty"` + ICCID string `json:"iccid,omitempty"` + IMEI string `json:"imei,omitempty"` + MEPStatus string `json:"mepStatus,omitempty"` + MSISDN string `json:"msisdn,omitempty"` + NetworkMode string `json:"networkMode,omitempty"` + NetworkName string `json:"networkName,omitempty"` + RFBand int `json:"rfBand,omitempty"` Roaming bool `json:"roaming"` RoamingAllowed bool `json:"roamingAllowed"` - SignalStrength string `json:"signalStrength"` - Type string `json:"type"` - WWANIPAddr string `json:"wwanIpAddr"` + SignalStrength string `json:"signalStrength,omitempty"` + Type string `json:"type,omitempty"` + WWANIPAddr string `json:"wwanIpAddr,omitempty"` +} + +type BaseStationMetadata 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"` + TimeZone string `json:"timeZone"` + OlsonTimeZone string `json:"olsonTimeZone"` + UploadBandwidthSaturated bool `json:"uploadBandwidthSaturated"` + AntiFlicker map[string]int `json:"antiFlicker"` + LowBatteryAlert map[string]bool `json:"lowBatteryAlert"` + LowSignalAlert map[string]bool `json:"lowSignalAlert"` + Claimed bool `json:"claimed"` + TimeSyncState string `json:"timeSyncState"` + Connectivity Connectivity `json:"connectivity"` } // Owner is the owner of a Device data. @@ -97,8 +119,118 @@ type StreamUrl struct { Url string `json:"url"` } -// Payload represents the message that will be sent to the arlo servers via the Notify API. -type Payload struct { +type BaseDetectionProperties struct { + Armed bool `json:"armed"` + Sensitivity int `json:"sensitivity"` + Zones []string `json:"zones,omitempty"` +} + +// MotionDetectionProperties is the Properties struct for the EventStreamPayload type. +type MotionDetectionProperties struct { + BaseDetectionProperties `json:"motionDetection"` +} + +// AudioDetectionProperties is the Properties struct for the EventStreamPayload type. +type AudioDetectionProperties struct { + BaseDetectionProperties `json:"audioDetection"` +} + +type EmailNotification struct { + Enabled bool `json:"enabled"` + EmailList []string `json:"emailList"` + PushNotification bool `json:"pushNotification"` +} + +type PlayTrackProperties struct { + TrackId string `json:"trackId"` + Position int `json:"position"` +} + +type BaseLoopbackModeProperties struct { + LoopbackMode string `json:"loopbackMode"` +} + +type LoopbackModeProperties struct { + Config BaseLoopbackModeProperties `json:"config"` +} + +type BaseSleepTimerProperties struct { + SleepTime int64 `json:"sleepTime"` + SleepTimeRel int `json:"sleepTimeRel"` +} + +type SleepTimerProperties struct { + Config BaseSleepTimerProperties `json:"config"` +} +type BaseEventActionProperties struct { + ActionType string `json:"actionType"` + StopType string `json:"stopType"` + Timeout int `json:"timeout"` + EmailNotification `json:"emailNotification"` +} + +type EventActionProperties struct { + BaseEventActionProperties `json:"eventAction"` +} + +type BaseShuffleProperties struct { + ShuffleActive bool `json:"shuffleActive"` +} + +type ShuffleProperties struct { + Config BaseShuffleProperties `json:"config"` +} + +type VolumeProperties struct { + Mute bool `json:"mute"` + Volume int `json:"volume,omitempty"` +} + +type SpeakerProperties struct { + Speaker VolumeProperties `json:"speaker"` +} + +type NightLightRGBProperties struct { + Red int `json:"red"` + Green int `json:"green"` + Blue int `json:"blue"` +} + +type BaseNightLightProperties struct { + Brightness int `json:"brightness,omitempty"` + Enabled bool `json:"enabled"` + Mode string `json:"mode,omitempty"` + RGB NightLightRGBProperties `json:"mode,omitempty"` + SleepTime int64 `json:"sleepTime,omitempty"` + SleepTimeRel int `json:"sleepTimeRel,omitempty"` +} + +type NightLightProperties struct { + NightLight BaseNightLightProperties `json:"nightLight"` +} + +type SirenProperties struct { + SirenState string `json:"sirenState"` + Duration int `json:"duration"` + Volume int `json:"volume"` + Pattern string `json:"pattern"` +} + +type BasestationModeProperties struct { + Active string `json:"active"` +} + +type BasestationScheduleProperties struct { + Active bool `json:"active"` +} + +type CameraProperties struct { + PrivacyActive bool `json:"privacyActive"` + Brightness int `json:"brightness,omitempty"` +} + +// EventStreamPayload is the message that will be sent to the arlo servers via the /notify API. +type EventStreamPayload struct { Action string `json:"action,omitempty"` Resource string `json:"resource,omitempty"` PublishResponse bool `json:"publishResponse"` @@ -108,14 +240,16 @@ type Payload struct { To string `json:"to"` } +// Data is part of the Status message fragment returned by most calls to the Arlo API. +// Data is only populated when Success is false. type Data struct { Message string `json:"message,omitempty"` Reason string `json:"reason,omitempty"` Error string `json:"error,omitempty"` } -// map[data:map[message:The device does not exist. reason:No such device. error:2217] success:false] -type Error struct { +// Status is the message fragment returned from most http calls to the Arlo API. +type Status struct { Data `json:"Data,omitempty"` Success bool `json:"success"` } diff --git a/util.go b/util.go index 88eb390..2c0ac40 100644 --- a/util.go +++ b/util.go @@ -15,19 +15,28 @@ import ( "github.com/pkg/errors" ) -func checkRequest(resp request.Response, err error, msg string) error { +func checkHttpRequest(resp *request.Response, err error, msg string) error { + if resp.StatusCode != 200 { + return errors.WithMessage(errors.New(fmt.Sprintf("http request failed: %s (%d)", resp.Status, resp.StatusCode)), msg) + } + if err != nil { return errors.WithMessage(err, msg) } + + return nil +} + +func checkRequest(resp *request.Response, err error, msg string) error { defer resp.Body.Close() - var status Error - if err := resp.Decode(&status); err != nil { + if err := checkHttpRequest(resp, err, msg); err != nil { return err } - if resp.StatusCode != 200 { - return errors.WithMessage(errors.New(fmt.Sprintf("http request failed: %s (%d)", resp.Status, resp.StatusCode)), msg) + var status Status + if err := resp.Decode(&status); err != nil { + return err } if status.Success == false {