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:
parent
930ecd3bad
commit
01c4316571
189
account.go
189
account.go
@ -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
126
arlo.go
@ -2,6 +2,8 @@ package arlo
|
||||
|
||||
import (
|
||||
"github.com/jeffreydwalter/arlo-golang/internal/request"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Arlo struct {
|
||||
@ -14,13 +16,129 @@ type Arlo struct {
|
||||
}
|
||||
|
||||
func newArlo(user string, pass string) *Arlo {
|
||||
|
||||
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,
|
||||
pass: pass,
|
||||
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")
|
||||
}
|
||||
|
@ -2,8 +2,6 @@ package arlo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type BaseStationMetadata struct {
|
||||
@ -35,46 +33,40 @@ type BaseStationMetadata struct {
|
||||
type Basestation struct {
|
||||
Device
|
||||
eventStream *EventStream
|
||||
arlo *Arlo
|
||||
}
|
||||
|
||||
// Basestations is an array of Basestation objects.
|
||||
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.Listen()
|
||||
|
||||
transId := GenTransId()
|
||||
|
||||
body := NotifyPayload{
|
||||
body := Payload{
|
||||
Action: "set",
|
||||
Resource: fmt.Sprintf("subscriptions/%s_%s", b.UserId, "web"),
|
||||
Resource: fmt.Sprintf("subscriptions/%s_%s", b.UserId, TransIdPrefix),
|
||||
PublishResponse: false,
|
||||
Properties: map[string][]string{"devices": []string{b.DeviceId}},
|
||||
TransId: transId,
|
||||
Properties: map[string][1]string{"devices": {b.DeviceId}},
|
||||
TransId: genTransId(),
|
||||
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
|
||||
To: b.DeviceId,
|
||||
}
|
||||
|
||||
resp, err := b.arlo.client.Post(fmt.Sprintf(NotifyUri, b.DeviceId), body, nil)
|
||||
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
|
||||
resp, err := b.arlo.post(fmt.Sprintf(NotifyUri, b.DeviceId), b.XCloudId, body, nil)
|
||||
return checkRequest(*resp, err, "failed to subscribe to the event stream")
|
||||
}
|
||||
|
||||
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",
|
||||
Resource: "basestation",
|
||||
PublishResponse: false,
|
||||
@ -83,30 +75,20 @@ func (b *Basestation) GetState() (*NotifyResponse, error) {
|
||||
To: b.DeviceId,
|
||||
}
|
||||
|
||||
b.eventStream.Subscriptions[transId] = make(chan *NotifyResponse)
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !status.Success {
|
||||
return nil, errors.New("failed to get basestation status")
|
||||
}
|
||||
|
||||
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",
|
||||
Resource: "cameras",
|
||||
PublishResponse: false,
|
||||
@ -115,21 +97,10 @@ func (b *Basestation) GetCameraState() (*NotifyResponse, error) {
|
||||
To: b.DeviceId,
|
||||
}
|
||||
|
||||
b.eventStream.Subscriptions[transId] = make(chan *NotifyResponse)
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !status.Success {
|
||||
return nil, errors.New("failed to get basestation status")
|
||||
}
|
||||
|
||||
return <-b.eventStream.Subscriptions[transId], nil
|
||||
}
|
||||
|
79
camera.go
79
camera.go
@ -1,8 +1,87 @@
|
||||
package arlo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// 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.
|
||||
type Camera Device
|
||||
|
||||
// Cameras is an array of Camera objects.
|
||||
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
|
||||
}
|
||||
|
240
devices.go
240
devices.go
@ -1,34 +1,45 @@
|
||||
package arlo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// A Device is the device data, this can be a camera, basestation, arloq, etc.
|
||||
type Device struct {
|
||||
DeviceType string `json:"deviceType"`
|
||||
XCloudId string `json:"xCloudId"`
|
||||
DisplayOrder uint8 `json:"displayOrder"`
|
||||
State string `json:"state"`
|
||||
ModelId string `json:"modelId"`
|
||||
InterfaceVersion string `json:"interfaceVersion"`
|
||||
ParentId string `json:"parentId"`
|
||||
UserId string `json:"userId"`
|
||||
DeviceName string `json:"deviceName"`
|
||||
FirmwareVersion string `json:"firmwareVersion"`
|
||||
MediaObjectCount uint8 `json:"mediaObjectCount"`
|
||||
DateCreated float64 `json:"dateCreated"`
|
||||
Owner Owner `json:"owner"`
|
||||
Properties Properties `json:"properties"`
|
||||
UniqueId string `json:"uniqueId"`
|
||||
LastModified float64 `json:"lastModified"`
|
||||
UserRole string `json:"userRole"`
|
||||
InterfaceSchemaVer string `json:"interfaceSchemaVer"`
|
||||
DeviceId string `json:"deviceId"`
|
||||
Metadata interface{}
|
||||
arlo *Arlo // Let's hold a reference to the parent arlo object since it holds the http.Client object and references to all devices.
|
||||
AnalyticsEnabled bool `json:"analyticsEnabled"`
|
||||
ArloMobilePlan bool `json:"arloMobilePlan"`
|
||||
ArloMobilePlanId string `json:"arloMobilePlanId"`
|
||||
ArloMobilePlanName string `json:"arloMobilePlanName"`
|
||||
ArloMobilePlanThreshold int `json:"arloMobilePlanThreshold"`
|
||||
Connectivity Connectivity `json:"connectivity"`
|
||||
CriticalBatteryState bool `json:"criticalBatteryState"`
|
||||
DateCreated float64 `json:"dateCreated"`
|
||||
DeviceId string `json:"deviceId"`
|
||||
DeviceName string `json:"deviceName"`
|
||||
DeviceType string `json:"deviceType"`
|
||||
DisplayOrder uint8 `json:"displayOrder"`
|
||||
FirmwareVersion string `json:"firmwareVersion"`
|
||||
InterfaceVersion string `json:"interfaceVersion"`
|
||||
InterfaceSchemaVer string `json:"interfaceSchemaVer"`
|
||||
LastImageUploaded string `json:"lastImageUploaded"`
|
||||
LastModified float64 `json:"lastModified"`
|
||||
MigrateActivityZone bool `json:"migrateActivityZone"`
|
||||
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.
|
||||
@ -59,7 +70,7 @@ func (ds *Devices) Find(deviceId string) *Device {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Devices) FindCameras(basestationId string) *Cameras {
|
||||
func (ds *Devices) FindCameras(basestationId string) Cameras {
|
||||
cs := new(Cameras)
|
||||
for _, d := range *ds {
|
||||
if d.ParentId == basestationId {
|
||||
@ -67,11 +78,11 @@ func (ds *Devices) FindCameras(basestationId string) *Cameras {
|
||||
}
|
||||
}
|
||||
|
||||
return cs
|
||||
return *cs
|
||||
}
|
||||
|
||||
func (d Device) IsBasestation() bool {
|
||||
return d.DeviceType == DeviceTypeBasestation
|
||||
return d.DeviceType == DeviceTypeBasestation || d.DeviceId == d.ParentId
|
||||
}
|
||||
|
||||
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.
|
||||
// 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.
|
||||
func (ds *Devices) GetBasestations() Basestations {
|
||||
func (ds Devices) GetBasestations() Basestations {
|
||||
var basestations Basestations
|
||||
for _, d := range *ds {
|
||||
if !d.IsCamera() {
|
||||
for _, d := range ds {
|
||||
if d.IsBasestation() || !d.IsCamera() {
|
||||
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".
|
||||
// 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.
|
||||
// Basestations also includes decvices of this type, so you can get the same data there or cast.
|
||||
func (ds *Devices) GetCameras() Cameras {
|
||||
// Basestations also includes devices of this type, so you can get the same data there or cast.
|
||||
func (ds Devices) GetCameras() Cameras {
|
||||
var cameras Cameras
|
||||
for _, d := range *ds {
|
||||
if !d.IsBasestation() {
|
||||
for _, d := range ds {
|
||||
if d.IsCamera() || !d.IsBasestation() {
|
||||
cameras = append(cameras, Camera(d))
|
||||
}
|
||||
}
|
||||
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.
|
||||
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}
|
||||
|
||||
resp, err := a.client.Put(DeviceRenameUri, body, nil)
|
||||
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")
|
||||
resp, err := d.arlo.put(DeviceRenameUri, d.XCloudId, body, nil)
|
||||
return checkRequest(*resp, err, "failed to update device name")
|
||||
}
|
||||
|
||||
// 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')})
|
||||
*/
|
||||
|
@ -20,7 +20,7 @@ var (
|
||||
|
||||
type EventStream struct {
|
||||
SSEClient *sse.Client
|
||||
Subscriptions map[string]chan *NotifyResponse
|
||||
Subscriptions map[string]chan *EventStreamResponse
|
||||
Events chan *sse.Event
|
||||
ErrorChan chan error
|
||||
Registered bool
|
||||
@ -38,7 +38,7 @@ func NewEventStream(url string, client *http.Client) *EventStream {
|
||||
return &EventStream{
|
||||
SSEClient: SSEClient,
|
||||
Events: make(chan *sse.Event),
|
||||
Subscriptions: make(map[string]chan *NotifyResponse),
|
||||
Subscriptions: make(map[string]chan *EventStreamResponse),
|
||||
ErrorChan: make(chan error, 1),
|
||||
}
|
||||
}
|
||||
@ -62,7 +62,7 @@ func (e *EventStream) Listen() {
|
||||
*/
|
||||
|
||||
if event.Data != nil {
|
||||
notifyResponse := &NotifyResponse{}
|
||||
notifyResponse := &EventStreamResponse{}
|
||||
b := bytes.NewBuffer(event.Data)
|
||||
err := json.NewDecoder(b).Decode(notifyResponse)
|
||||
if err != nil {
|
||||
|
@ -35,9 +35,9 @@ func NewClient(baseurl string) (*Client, error) {
|
||||
}
|
||||
|
||||
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.Add("Content-Type", "application/json")
|
||||
header.Add("Accept", "application/json")
|
||||
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.Set("Content-Type", "application/json")
|
||||
header.Set("Accept", "application/json")
|
||||
|
||||
return &Client{
|
||||
BaseURL: u,
|
||||
@ -89,15 +89,15 @@ func (c *Client) newRequest(method string, uri string, body interface{}, header
|
||||
|
||||
for k, v := range *c.BaseHttpHeader {
|
||||
for _, h := range v {
|
||||
//fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h)
|
||||
req.Header.Add(k, h)
|
||||
//log.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h)
|
||||
req.Header.Set(k, h)
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range header {
|
||||
for _, h := range v {
|
||||
//fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h)
|
||||
req.Header.Add(k, h)
|
||||
//log.Printf("Adding header (%s): (%s - %s)\n\n", u, 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) {
|
||||
|
||||
//fmt.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\nCOOKIES (%s): %v\n\n", req.URL, c.HttpClient.Jar.Cookies(req.URL))
|
||||
//log.Printf("\n\nHEADERS (%s): %v\n\n", req.URL, req.Header)
|
||||
|
||||
resp, err := c.HttpClient.Do(&req.Request)
|
||||
if err != nil {
|
||||
|
12
library.go
12
library.go
@ -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.
|
||||
|
||||
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}}
|
||||
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")
|
||||
}
|
||||
|
||||
var status Status
|
||||
var status Error
|
||||
if err := resp.Decode(&status); err != nil {
|
||||
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.
|
||||
|
||||
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}
|
||||
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")
|
||||
}
|
||||
|
||||
var status Status
|
||||
var status Error
|
||||
if err := resp.Decode(&status); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
27
responses.go
27
responses.go
@ -1,39 +1,44 @@
|
||||
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.
|
||||
type LoginResponse struct {
|
||||
Data Account
|
||||
*Status
|
||||
Error
|
||||
}
|
||||
|
||||
// DeviceResponse is an intermediate struct used when parsing data from the GetDevices() call.
|
||||
type DeviceResponse struct {
|
||||
Data Devices
|
||||
*Status
|
||||
Error
|
||||
}
|
||||
|
||||
// LibraryMetaDataResponse is an intermediate struct used when parsing data from the GetLibraryMetaData() call.
|
||||
type LibraryMetaDataResponse struct {
|
||||
Data LibraryMetaData
|
||||
*Status
|
||||
Error
|
||||
}
|
||||
|
||||
type LibraryResponse struct {
|
||||
Data Library
|
||||
*Status
|
||||
Error
|
||||
}
|
||||
|
||||
type StreamResponse struct {
|
||||
Data StreamUrl
|
||||
*Status
|
||||
Error
|
||||
}
|
||||
|
||||
type RecordingResponse struct {
|
||||
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"`
|
||||
}
|
||||
|
94
types.go
94
types.go
@ -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.
|
||||
type Owner struct {
|
||||
FirstName string `json:"firstName"`
|
||||
@ -49,8 +97,8 @@ type StreamUrl struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
// NotifyPayload represents the message that will be sent to the Arlo servers via the Notify API.
|
||||
type NotifyPayload struct {
|
||||
// Payload represents the message that will be sent to the arlo servers via the Notify API.
|
||||
type Payload struct {
|
||||
Action string `json:"action,omitempty"`
|
||||
Resource string `json:"resource,omitempty"`
|
||||
PublishResponse bool `json:"publishResponse"`
|
||||
@ -60,38 +108,14 @@ type NotifyPayload struct {
|
||||
To string `json:"to"`
|
||||
}
|
||||
|
||||
type NotifyResponse 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"`
|
||||
type Data struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
/*
|
||||
{"status":"connected"}
|
||||
|
||||
{"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"}}
|
||||
|
||||
{"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"}}
|
||||
*/
|
||||
// map[data:map[message:The device does not exist. reason:No such device. error:2217] success:false]
|
||||
type Error struct {
|
||||
Data `json:"Data,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
74
util.go
Normal file
74
util.go
Normal 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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user