Big refactor, moved methods around so they are more logically associated. Also made every device carry a reference to the arlo object. Finally, added some sugar to the http code so xCloudId can be properly set. Half way home, but still miles to go...

This commit is contained in:
Jeff Walter 2018-09-19 14:12:06 -05:00
parent 930ecd3bad
commit 01c4316571
11 changed files with 444 additions and 500 deletions

View File

@ -1,189 +0,0 @@
package arlo
import (
"fmt"
"math"
"math/rand"
"strconv"
"strings"
"time"
"github.com/jeffreydwalter/arlo-golang/internal/util"
"github.com/pkg/errors"
)
// 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"`
}
type Friend struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Devices DeviceOrder `json:"devices"`
LastModified float64 `json:"lastModified"`
AdminUser bool `json:"adminUser"`
Email string `json:"email"`
Id string `json:"id"`
}
func GenTransId() string {
source := rand.NewSource(time.Now().UnixNano())
random := rand.New(source)
e := random.Float64() * math.Pow(2, 32)
ms := time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
return fmt.Sprintf("%s!%s!%s", TransIdPrefix, strings.ToLower(util.FloatToHex(e)), strconv.Itoa(int(ms)))
}
func Login(user string, pass string) (*Arlo, error) {
a := newArlo(user, pass)
body := map[string]string{"email": a.user, "password": a.pass}
resp, err := a.client.Post(LoginUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "login request failed")
}
var loginResponse LoginResponse
if err := resp.Decode(&loginResponse); err != nil {
return nil, err
}
if loginResponse.Success {
// Cache the auth token.
a.client.BaseHttpHeader.Add("Authorization", loginResponse.Data.Token)
// Add other important headers.
a.client.BaseHttpHeader.Add("DNT", "1")
a.client.BaseHttpHeader.Add("schemaVersion", "1")
a.client.BaseHttpHeader.Add("Host", "arlo.netgear.com")
a.client.BaseHttpHeader.Add("Referer", "https://arlo.netgear.com/")
// Save the account info with the Arlo struct.
a.Account = loginResponse.Data
if deviceResponse, err := a.GetDevices(); err != nil {
return nil, err
} else {
if !deviceResponse.Success {
return nil, err
}
// Set the XCloudId header for future requests. You can override this on a per-request basis if needed.
a.client.BaseHttpHeader.Add("xcloudId", deviceResponse.Data[0].XCloudId)
// Cache the devices as their respective types.
a.Cameras = deviceResponse.Data.GetCameras()
a.Basestations = deviceResponse.Data.GetBasestations()
// Connect each basestation to the EventStream.
for i := range a.Basestations {
a.Basestations[i].arlo = a
a.Basestations[i].Subscribe()
}
}
} else {
return nil, errors.New("failed to login")
}
return a, nil
}
func (a *Arlo) Logout() (*Status, error) {
resp, err := a.client.Put(LogoutUri, nil, nil)
if err != nil {
return nil, errors.WithMessage(err, "logout request failed")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
return &status, nil
}
// UpdateProfile takes a first and last name, and updates the user profile with that information.
func (a *Arlo) UpdateProfile(firstName, lastName string) (*Status, error) {
body := map[string]string{"firstName": firstName, "lastName": lastName}
resp, err := a.client.Put(UserProfileUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update profile")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
return &status, nil
}
func (a *Arlo) UpdatePassword(pass string) (*Status, error) {
body := map[string]string{"currentPassword": a.pass, "newPassword": pass}
resp, err := a.client.Post(UserChangePasswordUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update password")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
if status.Success {
a.pass = pass
}
return &status, nil
}
/*
This is an example of the json you would pass in the body to UpdateFriends():
{
"firstName":"Some",
"lastName":"Body",
"devices":{
"XXXXXXXXXXXXX":"Camera 1",
"XXXXXXXXXXXXX":"Camera 2 ",
"XXXXXXXXXXXXX":"Camera 3"
},
"lastModified":1463977440911,
"adminUser":true,
"email":"user@example.com",
"id":"XXX-XXXXXXX"
}
*/
func (a *Arlo) UpdateFriends(f Friend) (*Status, error) {
resp, err := a.client.Put(UserFriendsUri, f, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update friends")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
return &status, nil
}

126
arlo.go
View File

@ -2,6 +2,8 @@ package arlo
import ( import (
"github.com/jeffreydwalter/arlo-golang/internal/request" "github.com/jeffreydwalter/arlo-golang/internal/request"
"github.com/pkg/errors"
) )
type Arlo struct { type Arlo struct {
@ -14,13 +16,129 @@ type Arlo struct {
} }
func newArlo(user string, pass string) *Arlo { func newArlo(user string, pass string) *Arlo {
c, _ := request.NewClient(BaseUrl) c, _ := request.NewClient(BaseUrl)
arlo := &Arlo{
// Add important headers.
c.BaseHttpHeader.Add("DNT", "1")
c.BaseHttpHeader.Add("schemaVersion", "1")
c.BaseHttpHeader.Add("Host", "arlo.netgear.com")
c.BaseHttpHeader.Add("Referer", "https://arlo.netgear.com/")
return &Arlo{
user: user, user: user,
pass: pass, pass: pass,
client: c, client: c,
} }
}
return arlo
func Login(user string, pass string) (*Arlo, error) {
a := 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")
}
var loginResponse LoginResponse
if err := resp.Decode(&loginResponse); err != nil {
return nil, err
}
if loginResponse.Success {
// Cache the auth token.
a.client.BaseHttpHeader.Add("Authorization", loginResponse.Data.Token)
// Save the account info with the arlo struct.
a.Account = loginResponse.Data
// Get the devices, which also caches them on the arlo object.
if _, err := a.GetDevices(); err != nil {
return nil, errors.WithMessage(err, "failed to login")
}
} else {
return nil, errors.New("failed to login")
}
return a, nil
}
func (a *Arlo) Logout() error {
resp, err := a.put(LogoutUri, "", nil, nil)
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) {
resp, err := a.get(DevicesUri, "", nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to get devices")
}
defer resp.Body.Close()
var deviceResponse DeviceResponse
if err := resp.Decode(&deviceResponse); err != nil {
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")
}
for i := range deviceResponse.Data {
deviceResponse.Data[i].arlo = a
}
// Cache the devices as their respective types.
a.Cameras = deviceResponse.Data.GetCameras()
a.Basestations = deviceResponse.Data.GetBasestations()
// Connect each basestation to the EventStream.
for i := range a.Basestations {
if err := a.Basestations[i].Unsubscribe(); err != nil {
return nil, errors.WithMessage(err, "failed to get devices")
}
if err := a.Basestations[i].Subscribe(); err != nil {
return nil, errors.WithMessage(err, "failed to get devices")
}
}
return deviceResponse.Data, nil
}
// 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")
}
// 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")
}
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 {
return err
}
a.pass = pass
return nil
}
func (a *Arlo) UpdateFriends(f Friend) error {
resp, err := a.put(UserFriendsUri, "", f, nil)
return checkRequest(*resp, err, "failed to update friends")
} }

View File

@ -2,8 +2,6 @@ package arlo
import ( import (
"fmt" "fmt"
"github.com/pkg/errors"
) )
type BaseStationMetadata struct { type BaseStationMetadata struct {
@ -35,46 +33,40 @@ type BaseStationMetadata struct {
type Basestation struct { type Basestation struct {
Device Device
eventStream *EventStream eventStream *EventStream
arlo *Arlo
} }
// Basestations is an array of Basestation objects. // Basestations is an array of Basestation objects.
type Basestations []Basestation type Basestations []Basestation
func (b *Basestation) Subscribe() (*Status, error) { func (b *Basestation) Subscribe() error {
b.eventStream = NewEventStream(BaseUrl+fmt.Sprintf(SubscribeUri, b.arlo.Account.Token), b.arlo.client.HttpClient) b.eventStream = NewEventStream(BaseUrl+fmt.Sprintf(SubscribeUri, b.arlo.Account.Token), b.arlo.client.HttpClient)
b.eventStream.Listen() b.eventStream.Listen()
transId := GenTransId() body := Payload{
body := NotifyPayload{
Action: "set", Action: "set",
Resource: fmt.Sprintf("subscriptions/%s_%s", b.UserId, "web"), Resource: fmt.Sprintf("subscriptions/%s_%s", b.UserId, TransIdPrefix),
PublishResponse: false, PublishResponse: false,
Properties: map[string][]string{"devices": []string{b.DeviceId}}, Properties: map[string][1]string{"devices": {b.DeviceId}},
TransId: transId, TransId: genTransId(),
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix), From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
To: b.DeviceId, To: b.DeviceId,
} }
resp, err := b.arlo.post(fmt.Sprintf(NotifyUri, b.DeviceId), b.XCloudId, body, nil)
resp, err := b.arlo.client.Post(fmt.Sprintf(NotifyUri, b.DeviceId), body, nil) return checkRequest(*resp, err, "failed to subscribe to the event stream")
if err != nil {
return nil, errors.WithMessage(err, "failed to subscribe to the event stream")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
return &status, nil
} }
func (b *Basestation) GetState() (*NotifyResponse, error) { func (b *Basestation) Unsubscribe() error {
// TODO: Close channel to stop EventStream.
//return errors.New("not implemented")
return nil
}
transId := GenTransId() func (b *Basestation) GetState() (*EventStreamResponse, error) {
transId := genTransId()
body := NotifyPayload{ b.eventStream.Subscriptions[transId] = make(chan *EventStreamResponse)
body := Payload{
Action: "get", Action: "get",
Resource: "basestation", Resource: "basestation",
PublishResponse: false, PublishResponse: false,
@ -83,30 +75,20 @@ func (b *Basestation) GetState() (*NotifyResponse, error) {
To: b.DeviceId, To: b.DeviceId,
} }
b.eventStream.Subscriptions[transId] = make(chan *NotifyResponse) resp, err := b.arlo.post(fmt.Sprintf(NotifyUri, b.DeviceId), b.XCloudId, body, nil)
if err := checkRequest(*resp, err, "failed to get basestation state"); err != nil {
resp, err := b.arlo.client.Post(fmt.Sprintf(NotifyUri, b.DeviceId), body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to get basestation state")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err return nil, err
} }
if !status.Success {
return nil, errors.New("failed to get basestation status")
}
return <-b.eventStream.Subscriptions[transId], nil return <-b.eventStream.Subscriptions[transId], nil
} }
func (b *Basestation) GetCameraState() (*NotifyResponse, error) { func (b *Basestation) GetAssociatedCamerasState() (*EventStreamResponse, error) {
transId := genTransId()
transId := GenTransId() b.eventStream.Subscriptions[transId] = make(chan *EventStreamResponse)
body := NotifyPayload{ body := Payload{
Action: "get", Action: "get",
Resource: "cameras", Resource: "cameras",
PublishResponse: false, PublishResponse: false,
@ -115,21 +97,10 @@ func (b *Basestation) GetCameraState() (*NotifyResponse, error) {
To: b.DeviceId, To: b.DeviceId,
} }
b.eventStream.Subscriptions[transId] = make(chan *NotifyResponse) resp, err := b.arlo.post(fmt.Sprintf(NotifyUri, b.DeviceId), b.XCloudId, body, nil)
if err := checkRequest(*resp, err, "failed to get camera state"); err != nil {
resp, err := b.arlo.client.Post(fmt.Sprintf(NotifyUri, b.DeviceId), body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to get basestation state")
}
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err return nil, err
} }
if !status.Success {
return nil, errors.New("failed to get basestation status")
}
return <-b.eventStream.Subscriptions[transId], nil return <-b.eventStream.Subscriptions[transId], nil
} }

View File

@ -1,8 +1,87 @@
package arlo package arlo
import (
"fmt"
"strings"
"github.com/pkg/errors"
)
// A Camera is a Device of type "camera". // A Camera is a Device of type "camera".
// 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.
type Camera Device type Camera Device
// Cameras is an array of Camera objects. // Cameras is an array of Camera objects.
type Cameras []Camera type Cameras []Camera
// 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{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: map[string]string{
"activityState": "startUserStream",
"cameraId": c.DeviceId,
},
TransId: genTransId(),
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
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")
}
defer resp.Body.Close()
var streamResponse StreamResponse
if err := resp.Decode(&streamResponse); err != nil {
return nil, err
}
if !streamResponse.Success {
return nil, errors.WithMessage(errors.New("status was false"), "failed to start stream")
}
streamResponse.Data.Url = strings.Replace(streamResponse.Data.Url, "rtsp://", "rtsps://", 1)
return &streamResponse, nil
}
// TakeSnapshot causes the camera to record a snapshot.
func (c *Camera) TakeSnapshot() (*StreamResponse, error) {
streamResponse, err := c.StartStream()
if err != nil {
return nil, errors.WithMessage(err, "failed to take snapshot")
}
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")
}
return streamResponse, 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()
if err != nil {
return nil, errors.WithMessage(err, "failed to start recording")
}
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")
}
return streamResponse, nil
}

View File

@ -1,34 +1,45 @@
package arlo package arlo
import (
"fmt"
"strings"
"github.com/pkg/errors"
)
// A Device is the device data, this can be a camera, basestation, arloq, etc. // A Device is the device data, this can be a camera, basestation, arloq, etc.
type Device struct { type Device struct {
DeviceType string `json:"deviceType"` arlo *Arlo // Let's hold a reference to the parent arlo object since it holds the http.Client object and references to all devices.
XCloudId string `json:"xCloudId"` AnalyticsEnabled bool `json:"analyticsEnabled"`
DisplayOrder uint8 `json:"displayOrder"` ArloMobilePlan bool `json:"arloMobilePlan"`
State string `json:"state"` ArloMobilePlanId string `json:"arloMobilePlanId"`
ModelId string `json:"modelId"` ArloMobilePlanName string `json:"arloMobilePlanName"`
InterfaceVersion string `json:"interfaceVersion"` ArloMobilePlanThreshold int `json:"arloMobilePlanThreshold"`
ParentId string `json:"parentId"` Connectivity Connectivity `json:"connectivity"`
UserId string `json:"userId"` CriticalBatteryState bool `json:"criticalBatteryState"`
DeviceName string `json:"deviceName"` DateCreated float64 `json:"dateCreated"`
FirmwareVersion string `json:"firmwareVersion"` DeviceId string `json:"deviceId"`
MediaObjectCount uint8 `json:"mediaObjectCount"` DeviceName string `json:"deviceName"`
DateCreated float64 `json:"dateCreated"` DeviceType string `json:"deviceType"`
Owner Owner `json:"owner"` DisplayOrder uint8 `json:"displayOrder"`
Properties Properties `json:"properties"` FirmwareVersion string `json:"firmwareVersion"`
UniqueId string `json:"uniqueId"` InterfaceVersion string `json:"interfaceVersion"`
LastModified float64 `json:"lastModified"` InterfaceSchemaVer string `json:"interfaceSchemaVer"`
UserRole string `json:"userRole"` LastImageUploaded string `json:"lastImageUploaded"`
InterfaceSchemaVer string `json:"interfaceSchemaVer"` LastModified float64 `json:"lastModified"`
DeviceId string `json:"deviceId"` MigrateActivityZone bool `json:"migrateActivityZone"`
Metadata interface{} MobileCarrier string `json:"mobileCarrier"`
MobileTrialUsed bool `json:"mobileTrialUsed"`
PermissionsFilePath string `json:"permissionsFilePath"`
PermissionsSchemaVer string `json:"permissionsSchemaVer"`
PermissionsVerison string `json:"permissionsVerison"` // WTF? Netgear developers think this is OK... *sigh*
PermissionsVersion string `json:"permissionsVersion"`
PresignedFullFrameSnapshotUrl string `json:"presignedFullFrameSnapshotUrl"`
PresignedLastImageUrl string `json:"presignedLastImageUrl"`
PresignedSnapshotUrl string `json:"presignedSnapshotUrl"`
MediaObjectCount uint8 `json:"mediaObjectCount"`
ModelId string `json:"modelId"`
Owner Owner `json:"owner"`
ParentId string `json:"parentId"`
Properties Properties `json:"properties"`
UniqueId string `json:"uniqueId"`
UserId string `json:"userId"`
UserRole string `json:"userRole"`
State string `json:"state"`
XCloudId string `json:"xCloudId"`
} }
// Devices is an array of Device objects. // Devices is an array of Device objects.
@ -59,7 +70,7 @@ func (ds *Devices) Find(deviceId string) *Device {
return nil return nil
} }
func (ds *Devices) FindCameras(basestationId string) *Cameras { func (ds *Devices) FindCameras(basestationId string) Cameras {
cs := new(Cameras) cs := new(Cameras)
for _, d := range *ds { for _, d := range *ds {
if d.ParentId == basestationId { if d.ParentId == basestationId {
@ -67,11 +78,11 @@ func (ds *Devices) FindCameras(basestationId string) *Cameras {
} }
} }
return cs return *cs
} }
func (d Device) IsBasestation() bool { func (d Device) IsBasestation() bool {
return d.DeviceType == DeviceTypeBasestation return d.DeviceType == DeviceTypeBasestation || d.DeviceId == d.ParentId
} }
func (d Device) IsCamera() bool { func (d Device) IsCamera() bool {
@ -82,10 +93,10 @@ func (d Device) IsCamera() bool {
// I did this because some device types, like arloq, don't have a basestation. // I did this because some device types, like arloq, don't have a basestation.
// So, when interacting with them you must treat them like a basestation and a camera. // So, when interacting with them you must treat them like a basestation and a camera.
// Cameras also includes devices of this type, so you can get the same data there or cast. // Cameras also includes devices of this type, so you can get the same data there or cast.
func (ds *Devices) GetBasestations() Basestations { func (ds Devices) GetBasestations() Basestations {
var basestations Basestations var basestations Basestations
for _, d := range *ds { for _, d := range ds {
if !d.IsCamera() { if d.IsBasestation() || !d.IsCamera() {
basestations = append(basestations, Basestation{Device: d}) basestations = append(basestations, Basestation{Device: d})
} }
} }
@ -95,170 +106,21 @@ func (ds *Devices) GetBasestations() Basestations {
// GetCameras returns a Cameras object containing all devices that are of type "camera". // GetCameras returns a Cameras object containing all devices that are of type "camera".
// I did this because some device types, like arloq, don't have a basestation. // I did this because some device types, like arloq, don't have a basestation.
// So, when interacting with them you must treat them like a basestation and a camera. // So, when interacting with them you must treat them like a basestation and a camera.
// Basestations also includes decvices of this type, so you can get the same data there or cast. // Basestations also includes devices of this type, so you can get the same data there or cast.
func (ds *Devices) GetCameras() Cameras { func (ds Devices) GetCameras() Cameras {
var cameras Cameras var cameras Cameras
for _, d := range *ds { for _, d := range ds {
if !d.IsBasestation() { if d.IsCamera() || !d.IsBasestation() {
cameras = append(cameras, Camera(d)) cameras = append(cameras, Camera(d))
} }
} }
return cameras return cameras
} }
// 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() (*DeviceResponse, error) {
resp, err := a.client.Get(DevicesUri, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to get devices")
}
defer resp.Body.Close()
var deviceResponse DeviceResponse
if err := resp.Decode(&deviceResponse); err != nil {
return nil, err
}
if len(deviceResponse.Data) == 0 {
return nil, errors.New("no devices found")
}
return &deviceResponse, nil
}
// UpdateDeviceName sets the name of the given device to the name argument. // UpdateDeviceName sets the name of the given device to the name argument.
func (a *Arlo) UpdateDeviceName(d Device, name string) (*Status, error) { 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 := a.client.Put(DeviceRenameUri, body, nil) return checkRequest(*resp, err, "failed to update device name")
if err != nil {
return nil, errors.WithMessage(err, "failed to update device name")
}
defer resp.Body.Close()
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
return &status, nil
return nil, errors.New("device not found")
} }
// UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given.
func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) (*Status, error) {
resp, err := a.client.Post(DeviceDisplayOrderUri, d, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to update display order")
}
defer resp.Body.Close()
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
return &status, nil
}
// 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 (a *Arlo) StartStream(c Camera) (*StreamResponse, error) {
body := map[string]interface{}{
"to": c.ParentId,
"from": fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
"resource": fmt.Sprintf("cameras/%s", c.DeviceId),
"action": "set",
"publishResponse": true,
"transId": GenTransId(),
"properties": map[string]string{
"activityState": "startUserStream",
"cameraId": c.DeviceId,
},
}
resp, err := a.client.Post(DeviceStartStreamUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to start stream")
}
defer resp.Body.Close()
var streamResponse StreamResponse
if err := resp.Decode(&streamResponse); err != nil {
return nil, err
}
streamResponse.Data.Url = strings.Replace(streamResponse.Data.Url, "rtsp://", "rtsps://", 1)
return &streamResponse, nil
}
// TakeSnapshot causes the camera to record a snapshot.
func (a *Arlo) TakeSnapshot(c Camera) (*StreamResponse, error) {
stream, err := a.StartStream(c)
if err != nil {
return nil, errors.WithMessage(err, "failed to take snapshot")
}
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
resp, err := a.client.Post(DeviceTakeSnapshotUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to take snapshot")
}
defer resp.Body.Close()
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
streamResponse := StreamResponse{stream.Data, &status}
return &streamResponse, nil
}
// StartRecording causes the camera to start recording and returns a url that you must start reading from using ffmpeg
// or something similar.
func (a *Arlo) StartRecording(c Camera) (*StreamResponse, error) {
stream, err := a.StartStream(c)
if err != nil {
return nil, errors.WithMessage(err, "failed to start recording")
}
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
resp, err := a.client.Post(DeviceStartRecordUri, body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to start recording")
}
defer resp.Body.Close()
var status Status
if err := resp.Decode(&status); err != nil {
return nil, err
}
streamResponse := StreamResponse{stream.Data, &status}
return &streamResponse, nil
}
/*
##
# This function causes the camera to stop recording.
#
# You can get the timezone from GetDevices().
##
func (a *Arlo) StopRecording(camera):
return a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/stopRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')})
*/

View File

@ -20,7 +20,7 @@ var (
type EventStream struct { type EventStream struct {
SSEClient *sse.Client SSEClient *sse.Client
Subscriptions map[string]chan *NotifyResponse Subscriptions map[string]chan *EventStreamResponse
Events chan *sse.Event Events chan *sse.Event
ErrorChan chan error ErrorChan chan error
Registered bool Registered bool
@ -38,7 +38,7 @@ func NewEventStream(url string, client *http.Client) *EventStream {
return &EventStream{ return &EventStream{
SSEClient: SSEClient, SSEClient: SSEClient,
Events: make(chan *sse.Event), Events: make(chan *sse.Event),
Subscriptions: make(map[string]chan *NotifyResponse), Subscriptions: make(map[string]chan *EventStreamResponse),
ErrorChan: make(chan error, 1), ErrorChan: make(chan error, 1),
} }
} }
@ -62,7 +62,7 @@ func (e *EventStream) Listen() {
*/ */
if event.Data != nil { if event.Data != nil {
notifyResponse := &NotifyResponse{} notifyResponse := &EventStreamResponse{}
b := bytes.NewBuffer(event.Data) b := bytes.NewBuffer(event.Data)
err := json.NewDecoder(b).Decode(notifyResponse) err := json.NewDecoder(b).Decode(notifyResponse)
if err != nil { if err != nil {

View File

@ -35,9 +35,9 @@ func NewClient(baseurl string) (*Client, error) {
} }
header := make(http.Header) header := make(http.Header)
header.Add("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_2 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Mobile/15B202 NETGEAR/v1 (iOS Vuezone)") header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_2 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Mobile/15B202 NETGEAR/v1 (iOS Vuezone)")
header.Add("Content-Type", "application/json") header.Set("Content-Type", "application/json")
header.Add("Accept", "application/json") header.Set("Accept", "application/json")
return &Client{ return &Client{
BaseURL: u, BaseURL: u,
@ -89,15 +89,15 @@ func (c *Client) newRequest(method string, uri string, body interface{}, header
for k, v := range *c.BaseHttpHeader { for k, v := range *c.BaseHttpHeader {
for _, h := range v { for _, h := range v {
//fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h) //log.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h)
req.Header.Add(k, h) req.Header.Set(k, h)
} }
} }
for k, v := range header { for k, v := range header {
for _, h := range v { for _, h := range v {
//fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h) //log.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h)
req.Header.Add(k, h) req.Header.Set(k, h)
} }
} }
@ -115,8 +115,8 @@ func (c *Client) newResponse(resp *http.Response) (*Response, error) {
func (c *Client) do(req *Request) (*Response, error) { func (c *Client) do(req *Request) (*Response, error) {
//fmt.Printf("\n\nCOOKIES (%s): %v\n\n", req.URL, c.HttpClient.Jar.Cookies(req.URL)) //log.Printf("\n\nCOOKIES (%s): %v\n\n", req.URL, c.HttpClient.Jar.Cookies(req.URL))
//fmt.Printf("\n\nHEADERS (%s): %v\n\n", req.URL, req.Header) //log.Printf("\n\nHEADERS (%s): %v\n\n", req.URL, req.Header)
resp, err := c.HttpClient.Do(&req.Request) resp, err := c.HttpClient.Do(&req.Request)
if err != nil { if err != nil {

View File

@ -70,13 +70,13 @@ func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (*LibraryResponse, error)
} }
/* /*
Delete a single video recording from Arlo. Delete a single video recording from arlo.
All of the date info and device id you need to pass into this method are given in the results of the GetLibrary() call. All of the date info and device id you need to pass into this method are given in the results of the GetLibrary() call.
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) (*Status, error) { func (a *Arlo) DeleteRecording(r Recording) (*Error, error) {
body := map[string]Library{"data": {r}} body := map[string]Library{"data": {r}}
resp, err := a.client.Post(LibraryRecycleUri, body, nil) resp, err := a.client.Post(LibraryRecycleUri, body, nil)
@ -84,7 +84,7 @@ func (a *Arlo) DeleteRecording(r Recording) (*Status, error) {
return nil, errors.WithMessage(err, "failed to delete recording") return nil, errors.WithMessage(err, "failed to delete recording")
} }
var status Status var status Error
if err := resp.Decode(&status); err != nil { if err := resp.Decode(&status); err != nil {
return nil, err return nil, err
} }
@ -93,13 +93,13 @@ func (a *Arlo) DeleteRecording(r Recording) (*Status, error) {
} }
/* /*
Delete a batch of video recordings from Arlo. Delete a batch of video recordings from arlo.
The GetLibrary() call response json can be passed directly to this method if you'd like to delete the same list of videos you queried for. The GetLibrary() call response json can be passed directly to this method if you'd like to delete the same list of videos you queried for.
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) (*Status, error) { func (a *Arlo) BatchDeleteRecordings(l Library) (*Error, error) {
body := map[string]Library{"data": l} body := map[string]Library{"data": l}
resp, err := a.client.Post(LibraryRecycleUri, body, nil) resp, err := a.client.Post(LibraryRecycleUri, body, nil)
@ -107,7 +107,7 @@ func (a *Arlo) BatchDeleteRecordings(l Library) (*Status, error) {
return nil, errors.WithMessage(err, "failed to delete recordings") return nil, errors.WithMessage(err, "failed to delete recordings")
} }
var status Status var status Error
if err := resp.Decode(&status); err != nil { if err := resp.Decode(&status); err != nil {
return nil, err return nil, err
} }

View File

@ -1,39 +1,44 @@
package arlo package arlo
// UpdateResponse is an intermediate struct used when parsing data from the UpdateProfile() call.
type Status struct {
Success bool `json:"success"`
}
// 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
*Status Error
} }
// 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
*Status Error
} }
// 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
*Status Error
} }
type LibraryResponse struct { type LibraryResponse struct {
Data Library Data Library
*Status Error
} }
type StreamResponse struct { type StreamResponse struct {
Data StreamUrl Data StreamUrl
*Status Error
} }
type RecordingResponse struct { type RecordingResponse struct {
Data StreamUrl Data StreamUrl
*Status Error
}
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"`
} }

View File

@ -26,6 +26,54 @@ 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"`
}
type Friend struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Devices DeviceOrder `json:"devices"`
LastModified float64 `json:"lastModified"`
AdminUser bool `json:"adminUser"`
Email string `json:"email"`
Id string `json:"id"`
}
// Owner is part of the Device data.
type Connectivity struct {
ActiveNetwork string `json:"activeNetwork"`
APN string `json:"apn"`
CarrierFw string `json:"carrierFw"`
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"`
Roaming bool `json:"roaming"`
RoamingAllowed bool `json:"roamingAllowed"`
SignalStrength string `json:"signalStrength"`
Type string `json:"type"`
WWANIPAddr string `json:"wwanIpAddr"`
}
// Owner is the owner of a Device data. // Owner is the owner of a Device data.
type Owner struct { type Owner struct {
FirstName string `json:"firstName"` FirstName string `json:"firstName"`
@ -49,8 +97,8 @@ type StreamUrl struct {
Url string `json:"url"` Url string `json:"url"`
} }
// NotifyPayload represents the message that will be sent to the Arlo servers via the Notify API. // Payload represents the message that will be sent to the arlo servers via the Notify API.
type NotifyPayload struct { type Payload 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"`
@ -60,38 +108,14 @@ type NotifyPayload struct {
To string `json:"to"` To string `json:"to"`
} }
type NotifyResponse struct { type Data struct {
Action string `json:"action,omitempty"` Message string `json:"message,omitempty"`
Resource string `json:"resource,omitempty"` Reason string `json:"reason,omitempty"`
Properties interface{} `json:"properties,omitempty"` Error string `json:"error,omitempty"`
TransId string `json:"transId"`
From string `json:"from"`
To string `json:"to"`
Status string `json:"status"`
} }
/* // map[data:map[message:The device does not exist. reason:No such device. error:2217] success:false]
{"status":"connected"} type Error struct {
Data `json:"Data,omitempty"`
{"resource":"subscriptions/336-4764296_web","transId":"web!f94fbae4.46e6e!1520148142862","action":"is","from":"48935B7SA9847","to":"336-4764296_web","properties":{"devices":["48935B7SA9847"],"url":"https://vzweb05-prod.vz.netgear.com/hmsweb/publish/48935B7SA9847/336-4764296/c16ec5b2-f914-4140-aa5d-880feda292a0"}} Success bool `json:"success"`
}
{"resource":"cameras/48B45974D8E54","properties":{"batteryLevel":15},"transId":"48935B7SA9847!cfa2b5ed!1520148143870","from":"48935B7SA9847","action":"is"}
{"resource":"basestation","properties":{"interfaceVersion":3,"apiVersion":1,"state":"idle","swVersion":"1.9.8.0_16666","hwVersion":"VMB3010r2","modelId":"VMB3010","capabilities":["gateway"],"mcsEnabled":true,"autoUpdateEnabled":true,"timeZone":"CST6CDT,M3.2.0,M11.1.0","olsonTimeZone":"America/Chicago","uploadBandwidthSaturated":false,"antiFlicker":{"mode":0,"autoDefault":1},"lowBatteryAlert":{"enabled":true},"lowSignalAlert":{"enabled":false},"claimed":true,"timeSyncState":"synchronized","connectivity":[{"type":"ethernet","connected":true}]},"action":"is","transId":"web!ffe75798.f6dca!1520148144127","to":"336-4764296_web","from":"48935B7SA9847"}
{"resource":"basestation","properties":{"interfaceVersion":3,"apiVersion":1,"state":"idle","swVersion":"1.9.8.0_16666","hwVersion":"VMB3010r2","modelId":"VMB3010","capabilities":["gateway"],"mcsEnabled":true,"autoUpdateEnabled":true,"timeZone":"CST6CDT,M3.2.0,M11.1.0","olsonTimeZone":"America/Chicago","uploadBandwidthSaturated":false,"antiFlicker":{"mode":0,"autoDefault":1},"lowBatteryAlert":{"enabled":true},"lowSignalAlert":{"enabled":false},"claimed":true,"timeSyncState":"synchronized","connectivity":[{"type":"ethernet","connected":true}]},"action":"is","transId":"web!ffe75798.f6dca!1520148144127","to":"336-4764296_web","from":"48935B7SA9847"}
{"resource":"cameras","properties":[{"interfaceVersion":3,"serialNumber":"48B45974D8E54","batteryLevel":15,"signalStrength":4,"brightness":0,"mirror":true,"flip":true,"powerSaveMode":3,"capabilities":["H.264Streaming","JPEGSnapshot","SignalStrength","Privacy","Standby",{"Resolutions":[{"text":"1080p","x":1920,"y":1088},{"text":"720p","x":1280,"y":720},{"text":"480p","x":848,"y":480},{"text":"360p","x":640,"y":352},{"text":"240p","x":416,"y":240}]},{"TimedStreamDuration":{"min":5,"max":120,"default":10}},{"TriggerEndStreamDuration":{"min":5,"max":300,"default":300}},{"Actions":[{"recordVideo":[{"StopActions":["timeout","triggerEndDetected"]}]},"sendEmailAlert","pushNotification"]},{"Triggers":[{"type":"pirMotionActive","sensitivity":{"type":"integer","min":1,"max":100,"step":1,"default":80}}]}],"zoom":{"topleftx":0,"toplefty":0,"bottomrightx":1280,"bottomrighty":720},"mic":{"mute":false,"volume":100},"speaker":{"mute":false,"volume":100},"streamingMode":"eventBased","continuousStreamState":"inactive","motion":{"sensitivity":5,"zones":[]},"resolution":{"width":1280,"height":720},"idleLedEnable":true,"privacyActive":false,"standbyActive":false,"connectionState":"available","activityState":"idle","swVersion":"1.2.16720","hwVersion":"H7","modelId":"VMC3030","motionSetupModeEnabled":false,"motionSetupModeSensitivity":80,"motionDetected":false,"audioDetected":false,"hasStreamed":true,"olsonTimeZone":"America/Chicago","name":"","nightVisionMode":1},{"interfaceVersion":3,"serialNumber":"48B4597FD9B8E","batteryLevel":0,"signalStrength":4,"brightness":0,"mirror":false,"flip":false,"powerSaveMode":3,"capabilities":["H.264Streaming","JPEGSnapshot","SignalStrength","Privacy","Standby",{"Resolutions":[{"text":"1080p","x":1920,"y":1088},{"text":"720p","x":1280,"y":720},{"text":"480p","x":848,"y":480},{"text":"360p","x":640,"y":352},{"text":"240p","x":416,"y":240}]},{"TimedStreamDuration":{"min":5,"max":120,"default":10}},{"TriggerEndStreamDuration":{"min":5,"max":300,"default":300}},{"Actions":[{"recordVideo":[{"StopActions":["timeout","triggerEndDetected"]}]},"sendEmailAlert","pushNotification"]},{"Triggers":[{"type":"pirMotionActive","sensitivity":{"type":"integer","min":1,"max":100,"step":1,"default":80}}]}],"zoom":{"topleftx":0,"toplefty":0,"bottomrightx":1280,"bottomrighty":720},"mic":{"mute":false,"volume":100},"speaker":{"mute":false,"volume":100},"streamingMode":"eventBased","continuousStreamState":"inactive","motion":{"sensitivity":5,"zones":[]},"resolution":{"width":1280,"height":720},"idleLedEnable":true,"privacyActive":false,"standbyActive":false,"connectionState":"batteryCritical","activityState":"idle","swVersion":"1.2.16720","hwVersion":"H7","modelId":"VMC3030","motionSetupModeEnabled":false,"motionSetupModeSensitivity":80,"motionDetected":false,"audioDetected":false,"hasStreamed":true,"olsonTimeZone":"America/Chicago","name":"","nightVisionMode":1},{"interfaceVersion":3,"serialNumber":"48B4597VD8FF5","batteryLevel":0,"signalStrength":4,"brightness":2,"mirror":true,"flip":true,"powerSaveMode":3,"capabilities":["H.264Streaming","JPEGSnapshot","SignalStrength","Privacy","Standby",{"Resolutions":[{"text":"1080p","x":1920,"y":1088},{"text":"720p","x":1280,"y":720},{"text":"480p","x":848,"y":480},{"text":"360p","x":640,"y":352},{"text":"240p","x":416,"y":240}]},{"TimedStreamDuration":{"min":5,"max":120,"default":10}},{"TriggerEndStreamDuration":{"min":5,"max":300,"default":300}},{"Actions":[{"recordVideo":[{"StopActions":["timeout","triggerEndDetected"]}]},"sendEmailAlert","pushNotification"]},{"Triggers":[{"type":"pirMotionActive","sensitivity":{"type":"integer","min":1,"max":100,"step":1,"default":80}}]}],"zoom":{"topleftx":0,"toplefty":0,"bottomrightx":1280,"bottomrighty":720},"mic":{"mute":false,"volume":100},"speaker":{"mute":false,"volume":100},"streamingMode":"eventBased","continuousStreamState":"inactive","motion":{"sensitivity":5,"zones":[]},"resolution":{"width":1280,"height":720},"idleLedEnable":true,"privacyActive":false,"standbyActive":false,"connectionState":"batteryCritical","activityState":"idle","swVersion":"1.2.16720","hwVersion":"H7","modelId":"VMC3030","motionSetupModeEnabled":false,"motionSetupModeSensitivity":80,"motionDetected":false,"audioDetected":false,"hasStreamed":true,"olsonTimeZone":"America/Chicago","name":"","nightVisionMode":1}],"action":"is","transId":"web!2dc849b8.9ffc2!1520148144127","to":"336-4764296_web","from":"48935B7SA9847"}
{"resource":"modes","properties":{"active":"mode1","modes":[{"name":"","type":"disarmed","rules":[],"id":"mode0"},{"name":"","type":"armed","rules":["rule5","rule3","rule0"],"id":"mode1"},{"name":"*****_DEFAULT_MODE_ARMED_*****","rules":["rule1"],"id":"mode2"},{"name":"Test Mode","rules":["rule6"],"id":"mode3"}]},"action":"is","transId":"web!bbb0ff1f.3c85f!1520148144127","to":"336-4764296_web","from":"48935B7SA9847"}
{"resource":"rules","properties":{"rules":[{"name":"Push notification if Front Door detects motion","protected":true,"triggers":[{"deviceId":"48B45974D8E54","sensitivity":80,"type":"pirMotionActive"}],"actions":[{"type":"recordVideo","deviceId":"48B45974D8E54","stopCondition":{"type":"timeout","timeout":120}},{"type":"pushNotification"}],"id":"rule0"},{"name":"Record camera (Back Patio) on motion.","protected":false,"triggers":[{"type":"pirMotionActive","deviceId":"48B4597VD8FF5","sensitivity":80}],"actions":[{"deviceId":"48B4597VD8FF5","type":"recordVideo","stopCondition":{"type":"timeout","timeout":10}},{"type":"pushNotification"}],"id":"rule1"},{"name":"Push notification if Inside detects motion","protected":true,"triggers":[{"deviceId":"48B4597FD9B8E","sensitivity":90,"type":"pirMotionActive"}],"actions":[{"deviceId":"48B4597FD9B8E","type":"recordVideo","stopCondition":{"type":"timeout","timeout":120}}],"id":"rule3"},{"name":"Push notification if Back Patio detects motion","protected":true,"triggers":[{"deviceId":"48B4597VD8FF5","sensitivity":100,"type":"pirMotionActive"}],"actions":[{"deviceId":"48B4597VD8FF5","type":"recordVideo","stopCondition":{"type":"timeout","timeout":30}},{"type":"pushNotification"}],"id":"rule5"},{"name":"Push notification & Email alert if Back Patio detects motion","protected":false,"triggers":[{"type":"pirMotionActive","deviceId":"48B4597VD8FF5","sensitivity":80}],"actions":[{"type":"sendEmailAlert","recipients":["__OWNER_EMAIL__"]},{"type":"pushNotification"}],"id":"rule6"}]},"action":"is","transId":"web!bff59099.cbd6d!1520148144127","to":"336-4764296_web","from":"48935B7SA9847"}
{"resource":"subscriptions/336-4764296_web","transId":"web!ddda6350.ba92c!1520148172685","action":"is","from":"48935B7SA9847","to":"336-4764296_web","properties":{"devices":["48935B7SA9847"],"url":"https://vzweb05-prod.vz.netgear.com/hmsweb/publish/48935B7SA9847/336-4764296/37da66eb-023f-4965-bb8b-480687881b65"}}
{"resource":"subscriptions/336-4764296_web","transId":"web!d5739e5.077af!1520148202738","action":"is","from":"48935B7SA9847","to":"336-4764296_web","properties":{"devices":["48935B7SA9847"],"url":"https://vzweb05-prod.vz.netgear.com/hmsweb/publish/48935B7SA9847/336-4764296/7d9cc5d7-a908-4f22-aaaa-dbb70c8616d6"}}
*/

74
util.go Normal file
View File

@ -0,0 +1,74 @@
package arlo
import (
"fmt"
"math"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
"github.com/jeffreydwalter/arlo-golang/internal/request"
"github.com/jeffreydwalter/arlo-golang/internal/util"
"github.com/pkg/errors"
)
func checkRequest(resp request.Response, err error, msg string) error {
if err != nil {
return errors.WithMessage(err, msg)
}
defer resp.Body.Close()
var status Error
if err := resp.Decode(&status); 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)
}
if status.Success == false {
return errors.WithMessage(errors.New(status.Reason), msg)
}
return nil
}
func genTransId() string {
source := rand.NewSource(time.Now().UnixNano())
random := rand.New(source)
e := random.Float64() * math.Pow(2, 32)
ms := time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
return fmt.Sprintf("%s!%s!%s", TransIdPrefix, strings.ToLower(util.FloatToHex(e)), strconv.Itoa(int(ms)))
}
func (a *Arlo) get(uri, xCloudId string, header http.Header) (*request.Response, error) {
if len(xCloudId) > 0 {
a.client.BaseHttpHeader.Set("xcloudId", xCloudId)
}
return a.client.Get(uri, header)
}
func (a *Arlo) put(uri, xCloudId string, body interface{}, header http.Header) (*request.Response, error) {
if len(xCloudId) > 0 {
a.client.BaseHttpHeader.Set("xcloudId", xCloudId)
}
return a.client.Put(uri, body, header)
}
func (a *Arlo) post(uri, xCloudId string, body interface{}, header http.Header) (*request.Response, error) {
if len(xCloudId) > 0 {
a.client.BaseHttpHeader.Set("xcloudId", xCloudId)
}
return a.client.Post(uri, body, header)
}