More refactoring, internal API almost stable. Added a shitload of external Arlo APIs! The library is at about 60% parity with the Python version.

This commit is contained in:
Jeff Walter 2018-09-20 17:38:01 -05:00
parent 533202c8b9
commit 049f3c5652
10 changed files with 1250 additions and 265 deletions

48
arlo.go
View File

@ -15,7 +15,8 @@ type Arlo struct {
Cameras Cameras Cameras Cameras
} }
func newArlo(user string, pass string) *Arlo { func newArlo(user string, pass string) (arlo *Arlo) {
c, _ := request.NewClient(BaseUrl) c, _ := request.NewClient(BaseUrl)
// Add important headers. // Add important headers.
@ -31,14 +32,15 @@ func newArlo(user string, pass string) *Arlo {
} }
} }
func Login(user string, pass string) (*Arlo, error) { func Login(user string, pass string) (arlo *Arlo, err error) {
a := newArlo(user, pass) arlo = newArlo(user, pass)
body := map[string]string{"email": a.user, "password": a.pass} body := map[string]string{"email": arlo.user, "password": arlo.pass}
resp, err := a.post(LoginUri, "", body, nil) resp, err := arlo.post(LoginUri, "", body, nil)
if err != nil { if err := checkHttpRequest(resp, err, "login request failed"); err != nil {
return nil, errors.WithMessage(err, "login request failed") return nil, err
} }
defer resp.Body.Close()
var loginResponse LoginResponse var loginResponse LoginResponse
if err := resp.Decode(&loginResponse); err != nil { if err := resp.Decode(&loginResponse); err != nil {
@ -47,33 +49,33 @@ func Login(user string, pass string) (*Arlo, error) {
if loginResponse.Success { if loginResponse.Success {
// Cache the auth token. // 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. // 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. // 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") return nil, errors.WithMessage(err, "failed to login")
} }
} else { } else {
return nil, errors.New("failed to login") return nil, errors.New("failed to login")
} }
return a, nil return arlo, nil
} }
func (a *Arlo) Logout() error { func (a *Arlo) Logout() error {
resp, err := a.put(LogoutUri, "", nil, nil) 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. // GetDevices returns an array of all devices.
// When you call Login, this method is called and all devices are cached in the arlo object. // 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) resp, err := a.get(DevicesUri, "", nil)
if err != nil { if err := checkHttpRequest(resp, err, "failed to get devices"); err != nil {
return nil, errors.WithMessage(err, "failed to get devices") return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -82,14 +84,14 @@ func (a *Arlo) GetDevices() (Devices, error) {
return nil, err return nil, err
} }
if len(deviceResponse.Data) == 0 {
return nil, errors.New("no devices found")
}
if !deviceResponse.Success { if !deviceResponse.Success {
return nil, errors.New("failed to get devices") 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 { for i := range deviceResponse.Data {
deviceResponse.Data[i].arlo = a 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. // UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given.
func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) error { func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) error {
resp, err := a.post(DeviceDisplayOrderUri, "", d, nil) 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. // UpdateProfile takes a first and last name, and updates the user profile with that information.
func (a *Arlo) UpdateProfile(firstName, lastName string) error { func (a *Arlo) UpdateProfile(firstName, lastName string) error {
body := map[string]string{"firstName": firstName, "lastName": lastName} body := map[string]string{"firstName": firstName, "lastName": lastName}
resp, err := a.put(UserProfileUri, "", body, nil) 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 { func (a *Arlo) UpdatePassword(pass string) error {
body := map[string]string{"currentPassword": a.pass, "newPassword": pass} body := map[string]string{"currentPassword": a.pass, "newPassword": pass}
resp, err := a.post(UserChangePasswordUri, "", body, nil) 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 return err
} }
@ -142,5 +144,5 @@ func (a *Arlo) UpdatePassword(pass string) error {
func (a *Arlo) UpdateFriends(f Friend) error { func (a *Arlo) UpdateFriends(f Friend) error {
resp, err := a.put(UserFriendsUri, "", f, nil) resp, err := a.put(UserFriendsUri, "", f, nil)
return checkRequest(*resp, err, "failed to update friends") return checkRequest(resp, err, "failed to update friends")
} }

506
arlobaby.go Normal file
View File

@ -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)
}

View File

@ -2,33 +2,12 @@ package arlo
import ( import (
"fmt" "fmt"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type BaseStationMetadata struct { const eventStreamTimeout = 10 * time.Second
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"`
}
// A Basestation is a Device that's not type "camera" (basestation, arloq, arloqs, etc.). // 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. // 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. // Basestations is an array of Basestation objects.
type Basestations []Basestation type Basestations []Basestation
func (b *Basestation) Subscribe() error { func (b *Basestation) makeEventStreamRequest(payload EventStreamPayload, msg string) (response *EventStreamResponse, err 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) {
transId := genTransId() transId := genTransId()
payload.TransId = transId payload.TransId = transId
@ -145,17 +30,264 @@ func (b *Basestation) makeEventStreamRequest(payload Payload, msg string) (*Even
b.eventStream.Subscriptions[transId] = make(chan *EventStreamResponse) b.eventStream.Subscriptions[transId] = make(chan *EventStreamResponse)
defer close(b.eventStream.Subscriptions[transId]) defer close(b.eventStream.Subscriptions[transId])
resp, err := b.arlo.post(fmt.Sprintf(NotifyUri, b.DeviceId), b.XCloudId, payload, nil) if err := b.NotifyEventStream(payload, msg); err != nil {
if err := checkRequest(*resp, err, msg); err != nil {
return nil, err return nil, err
} }
timer := time.NewTimer(eventStreamTimeout)
defer timer.Stop()
select { select {
case eventStreamResponse := <-b.eventStream.Subscriptions[transId]: case <-timer.C:
return eventStreamResponse, nil 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: case err = <-b.eventStream.Error:
return nil, errors.Wrap(err, "failed to get basestation") return nil, errors.Wrap(err, msg)
case <-b.eventStream.Close: 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")
}

274
camera.go
View File

@ -14,13 +14,102 @@ type Camera Device
// Cameras is an array of Camera objects. // Cameras is an array of Camera objects.
type Cameras []Camera 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. // 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 // 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: // 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" } // 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) { func (c *Camera) StartStream() (response *StreamResponse, err error) {
body := Payload{ payload := EventStreamPayload{
Action: "set", Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId), Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true, PublishResponse: true,
@ -33,55 +122,194 @@ func (c *Camera) StartStream() (*StreamResponse, error) {
To: c.ParentId, To: c.ParentId,
} }
resp, err := c.arlo.post(DeviceStartStreamUri, c.XCloudId, body, nil) msg := "failed to start stream"
if err != nil {
return nil, errors.WithMessage(err, "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() defer resp.Body.Close()
var streamResponse StreamResponse if err := resp.Decode(response); err != nil {
if err := resp.Decode(&streamResponse); err != nil {
return nil, err return nil, err
} }
if !streamResponse.Success { if !response.Success {
return nil, errors.WithMessage(errors.New("status was false"), "failed to start stream") 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. // TakeSnapshot causes the camera to record a snapshot.
func (c *Camera) TakeSnapshot() (*StreamResponse, error) { func (c *Camera) TakeSnapshot() (response *StreamResponse, err error) {
streamResponse, err := c.StartStream() msg := "failed to take snapshot"
response, err = c.StartStream()
if err != nil { 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} 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) resp, err := c.arlo.post(DeviceTakeSnapshotUri, c.XCloudId, body, nil)
if err := checkRequest(*resp, err, "failed to update device name"); err != nil { if err := checkRequest(resp, err, "failed to update device name"); err != nil {
return nil, errors.WithMessage(err, "failed to take snapshot") 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 // StartRecording causes the camera to start recording and returns a url that you must start reading from using ffmpeg
// or something similar. // or something similar.
func (c *Camera) StartRecording() (*StreamResponse, error) { func (c *Camera) StartRecording() (response *StreamResponse, err error) {
streamResponse, err := c.StartStream() msg := "failed to start recording"
response, err = c.StartStream()
if err != nil { 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} 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) resp, err := c.arlo.post(DeviceStartRecordUri, c.XCloudId, body, nil)
if err := checkRequest(*resp, err, "failed to update device name"); err != nil { if err := checkRequest(resp, err, "failed to update device name"); err != nil {
return nil, errors.WithMessage(err, "failed to start recording") 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)
} }

View File

@ -10,7 +10,7 @@ type Device struct {
ArloMobilePlanThreshold int `json:"arloMobilePlanThreshold"` ArloMobilePlanThreshold int `json:"arloMobilePlanThreshold"`
Connectivity Connectivity `json:"connectivity"` Connectivity Connectivity `json:"connectivity"`
CriticalBatteryState bool `json:"criticalBatteryState"` CriticalBatteryState bool `json:"criticalBatteryState"`
DateCreated float64 `json:"dateCreated"` DateCreated int64 `json:"dateCreated"`
DeviceId string `json:"deviceId"` DeviceId string `json:"deviceId"`
DeviceName string `json:"deviceName"` DeviceName string `json:"deviceName"`
DeviceType string `json:"deviceType"` DeviceType string `json:"deviceType"`
@ -19,7 +19,7 @@ type Device struct {
InterfaceVersion string `json:"interfaceVersion"` InterfaceVersion string `json:"interfaceVersion"`
InterfaceSchemaVer string `json:"interfaceSchemaVer"` InterfaceSchemaVer string `json:"interfaceSchemaVer"`
LastImageUploaded string `json:"lastImageUploaded"` LastImageUploaded string `json:"lastImageUploaded"`
LastModified float64 `json:"lastModified"` LastModified int64 `json:"lastModified"`
MigrateActivityZone bool `json:"migrateActivityZone"` MigrateActivityZone bool `json:"migrateActivityZone"`
MobileCarrier string `json:"mobileCarrier"` MobileCarrier string `json:"mobileCarrier"`
MobileTrialUsed bool `json:"mobileTrialUsed"` 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} body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId}
resp, err := d.arlo.put(DeviceRenameUri, d.XCloudId, body, nil) 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")
} }

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"io" "io"
"log"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "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") 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 u := c.BaseURL.String() + uri
req, err := http.NewRequest(method, u, buf) req, err := http.NewRequest(method, u, buf)

View File

@ -2,8 +2,6 @@ package arlo
import ( import (
"time" "time"
"github.com/pkg/errors"
) )
// LibraryMetaData is the library meta data. // LibraryMetaData is the library meta data.
@ -36,37 +34,34 @@ type Recording struct {
type Library []Recording 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")} body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
resp, err := a.post(LibraryMetadataUri, "", body, nil) resp, err := a.post(LibraryMetadataUri, "", body, nil)
if err := checkHttpRequest(resp, err, "failed to get library metadata"); err != nil {
if err != nil {
return nil, errors.WithMessage(err, "failed to get library metadata")
}
var libraryMetaDataResponse LibraryMetaDataResponse
if err := resp.Decode(&libraryMetaDataResponse); err != nil {
return nil, err return nil, err
} }
return &libraryMetaDataResponse, nil if err := resp.Decode(response); err != nil {
return nil, err
} }
func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (*LibraryResponse, error) { return response, nil
}
func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (response *LibraryResponse, err error) {
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")} body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
resp, err := a.post(LibraryUri, "", body, nil) resp, err := a.post(LibraryUri, "", body, nil)
if err != nil { if err := checkHttpRequest(resp, err, "failed to get library"); err != nil {
return nil, errors.WithMessage(err, "failed to get library")
}
var libraryResponse LibraryResponse
if err := resp.Decode(&libraryResponse); err != nil {
return nil, err 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. 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}} body := map[string]Library{"data": {r}}
resp, err := a.post(LibraryRecycleUri, "", body, nil) resp, err := a.post(LibraryRecycleUri, "", body, nil)
if err != nil { return checkRequest(resp, err, "failed to delete recording")
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
} }
/* /*
@ -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. 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} body := map[string]Library{"data": l}
resp, err := a.post(LibraryRecycleUri, "", body, nil) resp, err := a.post(LibraryRecycleUri, "", body, nil)
if err != nil { return checkRequest(resp, err, "failed to delete recordings")
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
} }

View File

@ -3,42 +3,37 @@ package arlo
// LoginResponse is an intermediate struct used when parsing data from the Login() call. // LoginResponse is an intermediate struct used when parsing data from the Login() call.
type LoginResponse struct { type LoginResponse struct {
Data Account Data Account
Error Status
} }
// DeviceResponse is an intermediate struct used when parsing data from the GetDevices() call. // DeviceResponse is an intermediate struct used when parsing data from the GetDevices() call.
type DeviceResponse struct { type DeviceResponse struct {
Data Devices Data Devices
Error Status
} }
// LibraryMetaDataResponse is an intermediate struct used when parsing data from the GetLibraryMetaData() call. // LibraryMetaDataResponse is an intermediate struct used when parsing data from the GetLibraryMetaData() call.
type LibraryMetaDataResponse struct { type LibraryMetaDataResponse struct {
Data LibraryMetaData Data LibraryMetaData
Error Status
} }
type LibraryResponse struct { type LibraryResponse struct {
Data Library Data Library
Error Status
} }
type StreamResponse struct { type StreamResponse struct {
Data StreamUrl Data StreamUrl
Error Status
} }
type RecordingResponse struct { type RecordingResponse struct {
Data StreamUrl Data StreamUrl
Error Status
} }
type EventStreamResponse struct { type EventStreamResponse struct {
Action string `json:"action,omitempty"` EventStreamPayload
Resource string `json:"resource,omitempty"` Status string `json:"status,omitempty"`
Properties interface{} `json:"properties,omitempty"`
TransId string `json:"transId"`
From string `json:"from"`
To string `json:"to"`
Status string `json:"status"`
} }

176
types.go
View File

@ -40,38 +40,60 @@ type Account struct {
PolicyUpdate bool `json:"policyUpdate"` PolicyUpdate bool `json:"policyUpdate"`
ValidEmail bool `json:"validEmail"` ValidEmail bool `json:"validEmail"`
Arlo bool `json:"arlo"` Arlo bool `json:"arlo"`
DateCreated float64 `json:"dateCreated"` DateCreated int64 `json:"dateCreated"`
} }
// Friend is the account data for non-primary account holders designated as friends.
type Friend struct { type Friend struct {
FirstName string `json:"firstName"` FirstName string `json:"firstName"`
LastName string `json:"lastName"` LastName string `json:"lastName"`
Devices DeviceOrder `json:"devices"` Devices DeviceOrder `json:"devices"`
LastModified float64 `json:"lastModified"` LastModified int64 `json:"lastModified"`
AdminUser bool `json:"adminUser"` AdminUser bool `json:"adminUser"`
Email string `json:"email"` Email string `json:"email"`
Id string `json:"id"` Id string `json:"id"`
} }
// Owner is part of the Device data. // Connectivity is part of the Device data.
type Connectivity struct { type Connectivity struct {
ActiveNetwork string `json:"activeNetwork"` ActiveNetwork string `json:"activeNetwork,omitempty"`
APN string `json:"apn"` APN string `json:"apn,omitempty"`
CarrierFw string `json:"carrierFw"` CarrierFw string `json:"carrierFw,omitempty"`
Connected bool `json:"connected"` Connected bool `json:"connected"`
FWVersion string `json:"fwVersion"` FWVersion string `json:"fwVersion,omitempty"`
ICCID string `json:"iccid"` ICCID string `json:"iccid,omitempty"`
IMEI string `json:"imei"` IMEI string `json:"imei,omitempty"`
MEPStatus string `json:"mepStatus"` MEPStatus string `json:"mepStatus,omitempty"`
MSISDN string `json:"msisdn"` MSISDN string `json:"msisdn,omitempty"`
NetworkMode string `json:"networkMode"` NetworkMode string `json:"networkMode,omitempty"`
NetworkName string `json:"networkName"` NetworkName string `json:"networkName,omitempty"`
RFBand int `json:"rfBand"` RFBand int `json:"rfBand,omitempty"`
Roaming bool `json:"roaming"` Roaming bool `json:"roaming"`
RoamingAllowed bool `json:"roamingAllowed"` RoamingAllowed bool `json:"roamingAllowed"`
SignalStrength string `json:"signalStrength"` SignalStrength string `json:"signalStrength,omitempty"`
Type string `json:"type"` Type string `json:"type,omitempty"`
WWANIPAddr string `json:"wwanIpAddr"` 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. // Owner is the owner of a Device data.
@ -97,8 +119,118 @@ type StreamUrl struct {
Url string `json:"url"` Url string `json:"url"`
} }
// Payload represents the message that will be sent to the arlo servers via the Notify API. type BaseDetectionProperties struct {
type Payload 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"` Action string `json:"action,omitempty"`
Resource string `json:"resource,omitempty"` Resource string `json:"resource,omitempty"`
PublishResponse bool `json:"publishResponse"` PublishResponse bool `json:"publishResponse"`
@ -108,14 +240,16 @@ type Payload struct {
To string `json:"to"` 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 { type Data struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
// map[data:map[message:The device does not exist. reason:No such device. error:2217] success:false] // Status is the message fragment returned from most http calls to the Arlo API.
type Error struct { type Status struct {
Data `json:"Data,omitempty"` Data `json:"Data,omitempty"`
Success bool `json:"success"` Success bool `json:"success"`
} }

19
util.go
View File

@ -15,19 +15,28 @@ import (
"github.com/pkg/errors" "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 { if err != nil {
return errors.WithMessage(err, msg) return errors.WithMessage(err, msg)
} }
return nil
}
func checkRequest(resp *request.Response, err error, msg string) error {
defer resp.Body.Close() defer resp.Body.Close()
var status Error if err := checkHttpRequest(resp, err, msg); err != nil {
if err := resp.Decode(&status); err != nil {
return err return err
} }
if resp.StatusCode != 200 { var status Status
return errors.WithMessage(errors.New(fmt.Sprintf("http request failed: %s (%d)", resp.Status, resp.StatusCode)), msg) if err := resp.Decode(&status); err != nil {
return err
} }
if status.Success == false { if status.Success == false {