Implementing EventStream support (still a WIP).
This commit is contained in:
parent
75454f08b3
commit
ceb2bff6c8
6
.gitignore
vendored
6
.gitignore
vendored
@ -12,3 +12,9 @@
|
||||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
|
||||
# Project-local files
|
||||
*.idea
|
||||
|
||||
# Go vendor libraries
|
||||
vendor/**/
|
||||
|
@ -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
14
Gopkg.lock
generated
@ -1,21 +1,21 @@
|
||||
# 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]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/r3labs/sse"
|
||||
packages = ["."]
|
||||
revision = "ab73c814bbdece537f16e92302cd99d1618d0e0d"
|
||||
version = "1.0.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "f9932cc0e893e6f71176657aa0d2f567ec244ca71e930f3d95235e1d8b349a1c"
|
||||
inputs-digest = "559855ebed7f1c0bf0bea2b6f750822d2eb67595d23a2f9006a8302a22b74e47"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
39
account.go
39
account.go
@ -1,6 +1,15 @@
|
||||
package arlo
|
||||
package arlo_golang
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jeffreydwalter/arlo-golang/internal/util"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -31,6 +40,18 @@ type Friend struct {
|
||||
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)
|
||||
@ -52,7 +73,7 @@ func Login(user string, pass string) (*Arlo, error) {
|
||||
a.client.BaseHttpHeader.Add("Authorization", loginResponse.Data.Token)
|
||||
|
||||
// Save the account info with the Arlo struct.
|
||||
a.Account = &loginResponse.Data
|
||||
a.Account = loginResponse.Data
|
||||
|
||||
if deviceResponse, err := a.GetDevices(); err != nil {
|
||||
return nil, err
|
||||
@ -61,12 +82,18 @@ func Login(user string, pass string) (*Arlo, error) {
|
||||
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.
|
||||
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 {
|
||||
return nil, errors.New("failed to login")
|
||||
|
40
arlo.go
40
arlo.go
@ -1,16 +1,16 @@
|
||||
package arlo
|
||||
package arlo_golang
|
||||
|
||||
import (
|
||||
"github.com/jeffreydwalter/arlo/internal/request"
|
||||
"github.com/jeffreydwalter/arlo-golang/internal/request"
|
||||
)
|
||||
|
||||
type Arlo struct {
|
||||
user string
|
||||
pass string
|
||||
client *request.Client
|
||||
Account *Account
|
||||
Basestations *Basestations
|
||||
Cameras *Cameras
|
||||
Account Account
|
||||
Basestations Basestations
|
||||
Cameras Cameras
|
||||
}
|
||||
|
||||
func newArlo(user string, pass string) *Arlo {
|
||||
@ -24,33 +24,3 @@ func newArlo(user string, pass string) *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')})
|
||||
*/
|
||||
|
@ -1 +1 @@
|
||||
package arlo
|
||||
package arlo_golang
|
||||
|
108
basestation.go
Normal file
108
basestation.go
Normal 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 ¬ifyResponse, nil
|
||||
}
|
||||
}
|
||||
}
|
8
camera.go
Normal file
8
camera.go
Normal 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
|
8
const.go
8
const.go
@ -1,6 +1,8 @@
|
||||
package arlo
|
||||
package arlo_golang
|
||||
|
||||
const (
|
||||
TransIdPrefix = "web"
|
||||
|
||||
BaseUrl = "https://arlo.netgear.com/hmsweb"
|
||||
LoginUri = "/login/v2"
|
||||
LogoutUri = "/logout"
|
||||
@ -26,4 +28,8 @@ const (
|
||||
DeviceStartRecordUri = "/users/devices/startRecord"
|
||||
DeviceStopRecordUri = "/users/devices/stopRecord"
|
||||
DeviceStartStreamUri = "/users/devices/startStream"
|
||||
|
||||
DeviceTypeBasestation = "basestation"
|
||||
DeviceTypeCamera = "camera"
|
||||
DeviceTypeArloQ = "arloq"
|
||||
)
|
||||
|
160
devices.go
160
devices.go
@ -1,6 +1,9 @@
|
||||
package arlo
|
||||
package arlo_golang
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -25,25 +28,12 @@ type Device struct {
|
||||
UserRole string `json:"userRole"`
|
||||
InterfaceSchemaVer string `json:"interfaceSchemaVer"`
|
||||
DeviceId string `json:"deviceId"`
|
||||
Metadata interface{}
|
||||
}
|
||||
|
||||
// Devices is an array of Device objects.
|
||||
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.
|
||||
// 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
|
||||
}
|
||||
|
||||
// Basestations 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 decvices of this type, so you can get the same data there or cast.
|
||||
func (ds *Devices) Basestations() *Basestations {
|
||||
var basestations Basestations
|
||||
func (ds *Devices) FindCameras(basestationId string) *Cameras {
|
||||
cs := new(Cameras)
|
||||
for _, d := range *ds {
|
||||
if d.DeviceType != "camera" {
|
||||
basestations = append(basestations, Basestation(d))
|
||||
if d.ParentId == basestationId {
|
||||
*cs = append(*cs, Camera(d))
|
||||
}
|
||||
}
|
||||
return &basestations
|
||||
}
|
||||
|
||||
// Cameras returns a Cameras object containing all devices that are of type "camera".
|
||||
return cs
|
||||
}
|
||||
|
||||
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.
|
||||
// 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) Cameras() *Cameras {
|
||||
func (ds *Devices) GetCameras() Cameras {
|
||||
var cameras Cameras
|
||||
for _, d := range *ds {
|
||||
if d.DeviceType != "basestation" {
|
||||
if !d.IsBasestation() {
|
||||
cameras = append(cameras, Camera(d))
|
||||
}
|
||||
}
|
||||
return &cameras
|
||||
return cameras
|
||||
}
|
||||
|
||||
// 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
|
||||
//
|
||||
// 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" }
|
||||
func (a *Arlo) StartStream(c Camera) (*StartStreamResponse, error) {
|
||||
// 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) {
|
||||
|
||||
var n Notification
|
||||
n.To = c.ParentId
|
||||
n.From = c.UserId
|
||||
n.Resource = "cameras/" + c.DeviceId
|
||||
n.Action = "set"
|
||||
n.PublishResponse = true
|
||||
n.TransId = ""
|
||||
n.Properties.ActivityState = "startUserStream"
|
||||
n.Properties.CameraId = c.DeviceId
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
// {"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, n, nil)
|
||||
resp, err := a.client.Post(DeviceStartStreamUri, body, nil)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to start stream")
|
||||
}
|
||||
|
||||
var startStreamResponse StartStreamResponse
|
||||
if err := resp.Decode(&startStreamResponse); err != nil {
|
||||
var streamResponse StreamResponse
|
||||
if err := resp.Decode(&streamResponse); err != nil {
|
||||
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
147
events_stream.go
Normal 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...)
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
@ -14,8 +13,8 @@ import (
|
||||
|
||||
type Client struct {
|
||||
BaseURL *url.URL
|
||||
BaseHttpHeader http.Header
|
||||
httpClient http.Client
|
||||
BaseHttpHeader *http.Header
|
||||
HttpClient *http.Client
|
||||
}
|
||||
|
||||
func NewClient(baseurl string) (*Client, error) {
|
||||
@ -40,8 +39,8 @@ func NewClient(baseurl string) (*Client, error) {
|
||||
|
||||
return &Client{
|
||||
BaseURL: u,
|
||||
BaseHttpHeader: header,
|
||||
httpClient: http.Client{Jar: jar},
|
||||
BaseHttpHeader: &header,
|
||||
HttpClient: &http.Client{Jar: jar},
|
||||
}, 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")
|
||||
}
|
||||
}
|
||||
log.Printf("JSON: %v", buf)
|
||||
// log.Printf("JSON: %v", buf)
|
||||
u := c.BaseURL.String() + uri
|
||||
req, err := http.NewRequest(method, u, buf)
|
||||
if err != nil {
|
||||
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 {
|
||||
//fmt.Printf("Adding header (%s): (%s - %s)\n\n", u, 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) {
|
||||
|
||||
//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)
|
||||
|
||||
resp, err := c.httpClient.Do(&req.Request)
|
||||
resp, err := c.HttpClient.Do(&req.Request)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to execute http request")
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
defer resp.Body.Close()
|
||||
return nil, errors.New("http request failed with status: " + resp.Status)
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package util
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func PrettyPrint(data interface{}) string {
|
||||
@ -13,13 +15,64 @@ func PrettyPrint(data interface{}) string {
|
||||
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) {
|
||||
/*
|
||||
func divmod(numerator, denominator int64) (quotient, remainder int64) {
|
||||
quotient = numerator / denominator // integer division, decimals are truncated
|
||||
remainder = numerator % denominator
|
||||
return
|
||||
}*/
|
||||
for quotient > 0 {
|
||||
quotient = int(x / 16)
|
||||
remainder := int(x - (float64(quotient) * 16))
|
||||
|
||||
if remainder > 9 {
|
||||
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
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package arlo
|
||||
package arlo_golang
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package arlo
|
||||
package arlo_golang
|
||||
|
||||
// UpdateResponse is an intermediate struct used when parsing data from the UpdateProfile() call.
|
||||
type Status struct {
|
||||
@ -28,7 +28,12 @@ type LibraryResponse struct {
|
||||
*Status
|
||||
}
|
||||
|
||||
type StartStreamResponse struct {
|
||||
type StreamResponse struct {
|
||||
Data StreamUrl
|
||||
*Status
|
||||
}
|
||||
|
||||
type RecordingResponse struct {
|
||||
Data StreamUrl
|
||||
*Status
|
||||
}
|
||||
|
78
types.go
78
types.go
@ -1,4 +1,4 @@
|
||||
package arlo
|
||||
package arlo_golang
|
||||
|
||||
/*
|
||||
// Credentials is the login credential data.
|
||||
@ -45,45 +45,53 @@ type Favorite struct {
|
||||
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 {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type NotificationProperties struct {
|
||||
ActivityState string `json:"activityState"`
|
||||
CameraId string `json:"cameraId"`
|
||||
// NotifyPayload represents the message that will be sent to the Arlo servers via the Notify API.
|
||||
type NotifyPayload struct {
|
||||
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 {
|
||||
To string `json:"to"`
|
||||
From string `json:"from"`
|
||||
Resource string `json:"resource"`
|
||||
Action string `json:"action"`
|
||||
PublishResponse bool `json:"publishResourcec"`
|
||||
type NotifyResponse struct {
|
||||
Action string `json:"action,omitempty"`
|
||||
Resource string `json:"resource,omitempty"`
|
||||
Properties interface{} `json:"properties,omitempty"`
|
||||
TransId string `json:"transId"`
|
||||
Properties NotificationProperties `json:"properties"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
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"}}
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user