Implementing EventStream support (still a WIP).

This commit is contained in:
Jeff Walter 2018-09-16 23:44:41 -05:00
parent 75454f08b3
commit ceb2bff6c8
16 changed files with 558 additions and 157 deletions

6
.gitignore vendored
View File

@ -12,3 +12,9 @@
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/ .glide/
# Project-local files
*.idea
# Go vendor libraries
vendor/**/

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

14
Gopkg.lock generated
View File

@ -1,21 +1,21 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
revision = "06020f85339e21b2478f756a78e295255ffa4d6a"
[[projects]] [[projects]]
name = "github.com/pkg/errors" name = "github.com/pkg/errors"
packages = ["."] packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d" revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0" version = "v0.8.0"
[[projects]]
name = "github.com/r3labs/sse"
packages = ["."]
revision = "ab73c814bbdece537f16e92302cd99d1618d0e0d"
version = "1.0.1"
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "f9932cc0e893e6f71176657aa0d2f567ec244ca71e930f3d95235e1d8b349a1c" inputs-digest = "559855ebed7f1c0bf0bea2b6f750822d2eb67595d23a2f9006a8302a22b74e47"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@ -1,6 +1,15 @@
package arlo package arlo_golang
import ( import (
"fmt"
"log"
"math"
"math/rand"
"strconv"
"strings"
"time"
"github.com/jeffreydwalter/arlo-golang/internal/util"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -31,6 +40,18 @@ type Friend struct {
Id string `json:"id"` 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) { func Login(user string, pass string) (*Arlo, error) {
a := newArlo(user, pass) a := newArlo(user, pass)
@ -52,7 +73,7 @@ func Login(user string, pass string) (*Arlo, error) {
a.client.BaseHttpHeader.Add("Authorization", loginResponse.Data.Token) a.client.BaseHttpHeader.Add("Authorization", loginResponse.Data.Token)
// Save the account info with the Arlo struct. // Save the account info with the Arlo struct.
a.Account = &loginResponse.Data a.Account = loginResponse.Data
if deviceResponse, err := a.GetDevices(); err != nil { if deviceResponse, err := a.GetDevices(); err != nil {
return nil, err return nil, err
@ -61,12 +82,18 @@ func Login(user string, pass string) (*Arlo, error) {
return nil, err return nil, err
} }
// Cache the devices as their respective types.
a.Basestations = deviceResponse.Data.Basestations()
a.Cameras = deviceResponse.Data.Cameras()
// Set the XCloudId header for future requests. You can override this on a per-request basis if needed. // 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) 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].connect(a)
}
log.Printf("HERE: %v", util.PrettyPrint(a.Basestations))
} }
} else { } else {
return nil, errors.New("failed to login") return nil, errors.New("failed to login")

40
arlo.go
View File

@ -1,16 +1,16 @@
package arlo package arlo_golang
import ( import (
"github.com/jeffreydwalter/arlo/internal/request" "github.com/jeffreydwalter/arlo-golang/internal/request"
) )
type Arlo struct { type Arlo struct {
user string user string
pass string pass string
client *request.Client client *request.Client
Account *Account Account Account
Basestations *Basestations Basestations Basestations
Cameras *Cameras Cameras Cameras
} }
func newArlo(user string, pass string) *Arlo { func newArlo(user string, pass string) *Arlo {
@ -24,33 +24,3 @@ func newArlo(user string, pass string) *Arlo {
return arlo return arlo
} }
/*
##
# This function causes the camera to record a snapshot.
#
# You can get the timezone from GetDevices().
##
func (a *Arlo) TakeSnapshot(camera):
stream_url = self.StartStream(camera)
a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/takeSnapshot', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')})
return stream_url;
##
# This function causes the camera to start recording.
#
# You can get the timezone from GetDevices().
##
func (a *Arlo) StartRecording(camera):
stream_url = self.StartStream(camera)
a.client.Post('https://arlo.netgear.com/hmsweb/users/devices/startRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')})
return stream_url
##
# 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

@ -1 +1 @@
package arlo package arlo_golang

108
basestation.go Normal file
View File

@ -0,0 +1,108 @@
package arlo_golang
import (
"encoding/json"
"fmt"
"time"
"github.com/jeffreydwalter/arlo-golang/internal/util"
"github.com/pkg/errors"
)
type BaseStationMetadata struct {
InterfaceVersion int `json:"interfaceVersion"`
ApiVersion int `json:"apiVersion"`
State string `json:"state"`
SwVersion string `json:"swVersion"`
HwVersion string `json:"hwVersion"`
ModelId string `json:"modelId"`
Capabilities []string `json:"capabilities"`
McsEnabled bool `json:"mcsEnabled"`
AutoUpdateEnabled bool `json:"autoUpdateEnabled"`
TimeZone string `json:"timeZone"`
OlsonTimeZone string `json:"olsonTimeZone"`
UploadBandwidthSaturated bool `json:"uploadBandwidthSaturated"`
AntiFlicker map[string]int `json:"antiFlicker"`
LowBatteryAlert map[string]bool `json:"lowBatteryAlert"`
LowSignalAlert map[string]bool `json:"lowSignalAlert"`
Claimed bool `json:"claimed"`
TimeSyncState string `json:"timeSyncState"`
Connectivity []struct {
Type string `json:"type"`
Connected bool `json:"connected"`
} `json:"connectivity"`
}
// A Basestation is a Device that's not type "camera" (basestation, arloq, arloqs, etc.).
// This type is here just for semantics. Some methods explicitly require a device of a certain type.
type Basestation struct {
Device
eventStream *EventStream
}
// Basestations is an array of Basestation objects.
type Basestations []Basestation
func (b *Basestation) connect(a *Arlo) {
b.eventStream = NewEventStream(BaseUrl+fmt.Sprintf(SubscribeUri, a.Account.Token), util.HeaderToMap(*a.client.BaseHttpHeader))
b.eventStream.Listen()
}
/*
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) GetBasestationState(b Basestation) (*NotifyResponse, error) {
transId := GenTransId()
body := NotifyPayload{
Action: "get",
Resource: "basestation",
PublishResponse: false,
Properties: map[string]string{},
TransId: transId,
From: fmt.Sprintf("%s_%s", b.UserId, TransIdPrefix),
To: b.DeviceId,
}
b.eventStream.Subscriptions[transId] = new(Subscriber)
for b.eventStream.Connected == false {
fmt.Println("Not connected yet.")
time.Sleep(1000 * time.Millisecond)
}
fmt.Println("Connected now.")
resp, err := a.client.Post(fmt.Sprintf(NotifyUri, b.DeviceId), body, nil)
if err != nil {
return nil, errors.WithMessage(err, "failed to start stream")
}
ep := &NotifyResponse{}
err = json.NewDecoder(resp.Body).Decode(ep)
if err != nil {
return nil, errors.WithMessage(err, "failed to decode body")
}
for {
fmt.Println("Subscribing to the eventstream.")
select {
case notifyResponse := <-*b.eventStream.Subscriptions[transId]:
fmt.Println("Recieved a response from the subscription.")
return &notifyResponse, nil
}
}
}

8
camera.go Normal file
View File

@ -0,0 +1,8 @@
package arlo_golang
// 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

View File

@ -1,6 +1,8 @@
package arlo package arlo_golang
const ( const (
TransIdPrefix = "web"
BaseUrl = "https://arlo.netgear.com/hmsweb" BaseUrl = "https://arlo.netgear.com/hmsweb"
LoginUri = "/login/v2" LoginUri = "/login/v2"
LogoutUri = "/logout" LogoutUri = "/logout"
@ -26,4 +28,8 @@ const (
DeviceStartRecordUri = "/users/devices/startRecord" DeviceStartRecordUri = "/users/devices/startRecord"
DeviceStopRecordUri = "/users/devices/stopRecord" DeviceStopRecordUri = "/users/devices/stopRecord"
DeviceStartStreamUri = "/users/devices/startStream" DeviceStartStreamUri = "/users/devices/startStream"
DeviceTypeBasestation = "basestation"
DeviceTypeCamera = "camera"
DeviceTypeArloQ = "arloq"
) )

View File

@ -1,6 +1,9 @@
package arlo package arlo_golang
import ( import (
"fmt"
"strings"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -25,25 +28,12 @@ type Device struct {
UserRole string `json:"userRole"` UserRole string `json:"userRole"`
InterfaceSchemaVer string `json:"interfaceSchemaVer"` InterfaceSchemaVer string `json:"interfaceSchemaVer"`
DeviceId string `json:"deviceId"` DeviceId string `json:"deviceId"`
Metadata interface{}
} }
// Devices is an array of Device objects. // Devices is an array of Device objects.
type Devices []Device type Devices []Device
// A Basestation is a Device that's not type "camera" (basestation, arloq, arloqs, etc.).
// This type is here just for semantics. Some methods explicitly require a device of a certain type.
type Basestation Device
// Basestations is an array of Basestation objects.
type Basestations []Basestation
// 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
// A DeviceOrder holds a map of device ids and a numeric index. The numeric index is the device order. // A DeviceOrder holds a map of device ids and a numeric index. The numeric index is the device order.
// Device order is mainly used by the UI to determine which order to show the devices. // Device order is mainly used by the UI to determine which order to show the devices.
/* /*
@ -69,32 +59,51 @@ func (ds *Devices) Find(deviceId string) *Device {
return nil return nil
} }
// Basestations returns a Basestations object containing all devices that are NOT type "camera". func (ds *Devices) FindCameras(basestationId string) *Cameras {
// I did this because some device types, like arloq, don't have a basestation. cs := new(Cameras)
// So, when interacting with them you must treat them like a basestation and a camera.
// Cameras also includes decvices of this type, so you can get the same data there or cast.
func (ds *Devices) Basestations() *Basestations {
var basestations Basestations
for _, d := range *ds { for _, d := range *ds {
if d.DeviceType != "camera" { if d.ParentId == basestationId {
basestations = append(basestations, Basestation(d)) *cs = append(*cs, Camera(d))
} }
} }
return &basestations
return cs
} }
// Cameras returns a Cameras object containing all devices that are of type "camera". func (d Device) IsBasestation() bool {
return d.DeviceType == DeviceTypeBasestation
}
func (d Device) IsCamera() bool {
return d.DeviceType == DeviceTypeCamera
}
// GetBasestations returns a Basestations object containing all devices that are NOT 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.
// Cameras also includes devices of this type, so you can get the same data there or cast.
func (ds *Devices) GetBasestations() Basestations {
var basestations Basestations
for _, d := range *ds {
if !d.IsCamera() {
basestations = append(basestations, Basestation{Device: d})
}
}
return 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. // 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 decvices of this type, so you can get the same data there or cast.
func (ds *Devices) Cameras() *Cameras { func (ds *Devices) GetCameras() Cameras {
var cameras Cameras var cameras Cameras
for _, d := range *ds { for _, d := range *ds {
if d.DeviceType != "basestation" { if !d.IsBasestation() {
cameras = append(cameras, Camera(d)) cameras = append(cameras, Camera(d))
} }
} }
return &cameras return cameras
} }
// GetDevices returns an array of all devices. // GetDevices returns an array of all devices.
@ -159,30 +168,91 @@ func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) (*Status, error) {
// You will need the to install a library to handle streaming of this protocol: https://pypi.python.org/pypi/python-librtmp // You will need the to install a library to handle streaming of this protocol: https://pypi.python.org/pypi/python-librtmp
// //
// The request to /users/devices/startStream returns: // The request to /users/devices/startStream returns:
// NOTE: { "url":"rtmps://vzwow09-z2-prod.vz.netgear.com:80/vzmodulelive?egressToken=b1b4b675_ac03_4182_9844_043e02a44f71&userAgent=web&cameraId=48B4597VD8FF5_1473010750131" } // NOTE: { "url":"rtsp://vzwow09-z2-prod.vz.netgear.com:80/vzmodulelive?egressToken=b1b4b675_ac03_4182_9844_043e02a44f71&userAgent=web&cameraId=48B4597VD8FF5_1473010750131" }
func (a *Arlo) StartStream(c Camera) (*StartStreamResponse, error) { func (a *Arlo) StartStream(c Camera) (*StreamResponse, error) {
var n Notification body := map[string]interface{}{
n.To = c.ParentId "to": c.ParentId,
n.From = c.UserId "from": fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
n.Resource = "cameras/" + c.DeviceId "resource": fmt.Sprintf("cameras/%s", c.DeviceId),
n.Action = "set" "action": "set",
n.PublishResponse = true "publishResponse": true,
n.TransId = "" "transId": GenTransId(),
n.Properties.ActivityState = "startUserStream" "properties": map[string]string{
n.Properties.CameraId = c.DeviceId "activityState": "startUserStream",
"cameraId": c.DeviceId,
},
}
// {"to":camera.get('parentId'),"from":self.user_id+"_web","resource":"cameras/"+camera.get('deviceId'),"action":"set","publishResponse":True,"transId":self.genTransId(),"properties":{"activityState":"startUserStream","cameraId":camera.get('deviceId')}}, headers={"xcloudId":camera.get('xCloudId')} resp, err := a.client.Post(DeviceStartStreamUri, body, nil)
resp, err := a.client.Post(DeviceStartStreamUri, n, nil)
if err != nil { if err != nil {
return nil, errors.WithMessage(err, "failed to start stream") return nil, errors.WithMessage(err, "failed to start stream")
} }
var startStreamResponse StartStreamResponse var streamResponse StreamResponse
if err := resp.Decode(&startStreamResponse); err != nil { if err := resp.Decode(&streamResponse); err != nil {
return nil, err return nil, err
} }
return &startStreamResponse, nil 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")
}
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")
}
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')})
*/

147
events_stream.go Normal file
View File

@ -0,0 +1,147 @@
package arlo_golang
import (
"bytes"
"encoding/json"
"fmt"
"log"
"sync"
"github.com/pkg/errors"
"github.com/r3labs/sse"
)
var FAILED_TO_PUBLISH = errors.New("Failed to publish")
var FAILED_TO_DECODE_JSON = errors.New("Failed to decode JSON")
var FAILED_TO_SUBSCRIBE = errors.New("Failed to subscribe to SSEClient")
type Subscriber chan NotifyResponse
type EventStream struct {
Registered bool
Connected bool
SSEClient *sse.Client
Events chan *sse.Event
Subscriptions map[string]*Subscriber
ErrorChan chan error
Responses map[string]NotifyResponse
Verbose bool
sync.Mutex
}
func NewEventStream(url string, headers map[string]string) *EventStream {
SSEClient := sse.NewClient(url)
SSEClient.Headers = headers
return &EventStream{
SSEClient: SSEClient,
Events: make(chan *sse.Event),
Subscriptions: map[string]*Subscriber{},
ErrorChan: make(chan error, 1),
}
}
func (e *EventStream) Listen() {
go func() {
err := e.SSEClient.SubscribeChan("", e.Events)
if err != nil {
fmt.Println(FAILED_TO_SUBSCRIBE)
e.ErrorChan <- FAILED_TO_SUBSCRIBE
}
}()
for event := range e.Events {
fmt.Println("Got event message here.")
fmt.Printf("EVENT: %s\n", event.Event)
fmt.Printf("DATA: %s\n", event.Data)
if event.Data != nil {
notifyResponse := &NotifyResponse{}
b := bytes.NewBuffer(event.Data)
err := json.NewDecoder(b).Decode(notifyResponse)
if err != nil {
e.ErrorChan <- errors.WithMessage(err, "failed to decode JSON")
break
}
if notifyResponse.Status == "connected" {
e.Connected = true
fmt.Println("Connected.")
break
}
}
}
go func() {
for event := range e.Events {
fmt.Println("Got event message.")
fmt.Printf("EVENT: %X\n", event.Event)
fmt.Printf("DATA: %X\n", event.Data)
if event.Data != nil {
notifyResponse := &NotifyResponse{}
b := bytes.NewBuffer(event.Data)
err := json.NewDecoder(b).Decode(notifyResponse)
if err != nil {
e.ErrorChan <- FAILED_TO_DECODE_JSON
break
}
if notifyResponse.Status == "connected" {
fmt.Println("Connected.")
e.Connected = true
} else if notifyResponse.Status == "disconnected" {
fmt.Println("Disconnected.")
e.Connected = false
} else {
fmt.Printf("Message for transId: %s", notifyResponse.TransId)
if subscriber, ok := e.Subscriptions[notifyResponse.TransId]; ok {
e.Lock()
*subscriber <- *notifyResponse
close(*subscriber)
delete(e.Subscriptions, notifyResponse.TransId)
e.Unlock()
} else {
// Throw away the message.
fmt.Println("Throwing away message.")
}
}
}
}
}()
/*
go func() {
fmt.Println("go func to recieve a subscription.")
for {
fmt.Println("go func for loop to recieve a subscription.")
select {
case s := <-e.Subscriptions:
if resp, ok := e.Responses[s.transId]; ok {
fmt.Println("Recieved a subscription, sending response.")
s.ResponseChan <- resp
e.Lock()
delete(e.Responses, s.transId)
e.Unlock()
} else {
fmt.Println("Recieved a subscription error, sending error response.")
e.ErrorChan <- FAILED_TO_PUBLISH
break
}
}
}
}()
*/
}
func (e *EventStream) verbose(params ...interface{}) {
if e.Verbose {
log.Println(params...)
}
}

View File

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"io" "io"
"log"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
@ -14,8 +13,8 @@ import (
type Client struct { type Client struct {
BaseURL *url.URL BaseURL *url.URL
BaseHttpHeader http.Header BaseHttpHeader *http.Header
httpClient http.Client HttpClient *http.Client
} }
func NewClient(baseurl string) (*Client, error) { func NewClient(baseurl string) (*Client, error) {
@ -40,8 +39,8 @@ func NewClient(baseurl string) (*Client, error) {
return &Client{ return &Client{
BaseURL: u, BaseURL: u,
BaseHttpHeader: header, BaseHttpHeader: &header,
httpClient: http.Client{Jar: jar}, HttpClient: &http.Client{Jar: jar},
}, nil }, nil
} }
@ -79,14 +78,14 @@ func (c *Client) newRequest(method string, uri string, body interface{}, header
return nil, errors.Wrap(err, "failed to create request object") return nil, errors.Wrap(err, "failed to create request object")
} }
} }
log.Printf("JSON: %v", buf) // log.Printf("JSON: %v", buf)
u := c.BaseURL.String() + uri u := c.BaseURL.String() + uri
req, err := http.NewRequest(method, u, buf) req, err := http.NewRequest(method, u, buf)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to create request object") return nil, errors.Wrap(err, "failed to create request object")
} }
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) //fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, k, h)
req.Header.Add(k, h) req.Header.Add(k, h)
@ -114,15 +113,15 @@ 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)) //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) //fmt.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 {
return nil, errors.Wrap(err, "failed to execute http request") return nil, errors.Wrap(err, "failed to execute http request")
} }
if resp.StatusCode >= 400 { if resp.StatusCode >= http.StatusBadRequest {
defer resp.Body.Close() defer resp.Body.Close()
return nil, errors.New("http request failed with status: " + resp.Status) return nil, errors.New("http request failed with status: " + resp.Status)
} }

View File

@ -3,6 +3,8 @@ package util
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http"
"strconv"
) )
func PrettyPrint(data interface{}) string { func PrettyPrint(data interface{}) string {
@ -13,13 +15,64 @@ func PrettyPrint(data interface{}) string {
return fmt.Sprint(string(j)) return fmt.Sprint(string(j))
} }
const TransIdPrefix = "web" func FloatToHex(x float64) string {
var result []byte
quotient := int(x)
fraction := x - float64(quotient)
func GenTransId(transType string) { for quotient > 0 {
/* quotient = int(x / 16)
func divmod(numerator, denominator int64) (quotient, remainder int64) { remainder := int(x - (float64(quotient) * 16))
quotient = numerator / denominator // integer division, decimals are truncated
remainder = numerator % denominator if remainder > 9 {
return result = append([]byte{byte(remainder + 55)}, result...)
}*/ } else {
for _, c := range strconv.Itoa(int(remainder)) {
result = append([]byte{byte(c)}, result...)
}
}
x = float64(quotient)
}
if fraction == 0 {
return string(result)
}
result = append(result, '.')
for fraction > 0 {
fraction = fraction * 16
integer := int(fraction)
fraction = fraction - float64(integer)
if integer > 9 {
result = append(result, byte(integer+55))
} else {
for _, c := range strconv.Itoa(int(integer)) {
result = append(result, byte(c))
}
}
}
return string(result)
}
func HeaderToArray(header http.Header) (res []string) {
for name, values := range header {
for _, value := range values {
res = append(res, fmt.Sprintf("%s: %s", name, value))
}
}
return
}
func HeaderToMap(header http.Header) map[string]string {
h := make(map[string]string)
for name, values := range header {
for _, value := range values {
h[name] = value
}
}
return h
} }

View File

@ -1,4 +1,4 @@
package arlo package arlo_golang
import ( import (
"time" "time"

View File

@ -1,4 +1,4 @@
package arlo package arlo_golang
// UpdateResponse is an intermediate struct used when parsing data from the UpdateProfile() call. // UpdateResponse is an intermediate struct used when parsing data from the UpdateProfile() call.
type Status struct { type Status struct {
@ -28,7 +28,12 @@ type LibraryResponse struct {
*Status *Status
} }
type StartStreamResponse struct { type StreamResponse struct {
Data StreamUrl
*Status
}
type RecordingResponse struct {
Data StreamUrl Data StreamUrl
*Status *Status
} }

View File

@ -1,4 +1,4 @@
package arlo package arlo_golang
/* /*
// Credentials is the login credential data. // Credentials is the login credential data.
@ -45,45 +45,53 @@ type Favorite struct {
Favorite uint8 `json:"Favorite"` Favorite uint8 `json:"Favorite"`
} }
/*
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"`
}
*/
type StreamUrl struct { type StreamUrl struct {
Url string `json:"url"` Url string `json:"url"`
} }
type NotificationProperties struct { // NotifyPayload represents the message that will be sent to the Arlo servers via the Notify API.
ActivityState string `json:"activityState"` type NotifyPayload struct {
CameraId string `json:"cameraId"` Action string `json:"action,omitempty"`
Resource string `json:"resource,omitempty"`
PublishResponse bool `json:"publishResponse,omitempty"`
Properties interface{} `json:"properties,omitempty"`
TransId string `json:"transId"`
From string `json:"from"`
To string `json:"to"`
} }
type Notification struct { type NotifyResponse struct {
To string `json:"to"` Action string `json:"action,omitempty"`
From string `json:"from"` Resource string `json:"resource,omitempty"`
Resource string `json:"resource"` Properties interface{} `json:"properties,omitempty"`
Action string `json:"action"` TransId string `json:"transId"`
PublishResponse bool `json:"publishResourcec"` From string `json:"from"`
TransId string `json:"transId"` To string `json:"to"`
Properties NotificationProperties `json:"properties"` Status string `json:"status"`
} }
/*
{"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"}}
*/