Lots of changes... Fixed issue with timeout of event stream. Added new APIs. Cleaned up existing schemas and APIs.
This commit is contained in:
parent
c29dd1066d
commit
c9ca29b60a
@ -1,4 +1,4 @@
|
|||||||
# arlo-golang
|
# arlo-go
|
||||||
![](gopher-arlo.png)
|
![](gopher-arlo.png)
|
||||||
> Go package for interacting with Netgear's Arlo camera system.
|
> Go package for interacting with Netgear's Arlo camera system.
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ Please read the [Issue Guidelines and Policies](https://github.com/jeffreydwalte
|
|||||||
## Install
|
## Install
|
||||||
```bash
|
```bash
|
||||||
# Install latest stable package
|
# Install latest stable package
|
||||||
$ go get github.com/jeffreydwalter/arlo-golang
|
$ go get github.com/jeffreydwalter/arlo-go
|
||||||
|
|
||||||
# Note: This package uses the `go dep` package for dependency management. If you plan on contributing to this package, you will be required to use [dep](https://github.com/golang/dep). Setting it up is outside the scope of this README, but if you want to contribute and aren't familiar with `dep`, I'm happy to get you.
|
# Note: This package uses the `go dep` package for dependency management. If you plan on contributing to this package, you will be required to use [dep](https://github.com/golang/dep). Setting it up is outside the scope of this README, but if you want to contribute and aren't familiar with `dep`, I'm happy to get you.
|
||||||
```
|
```
|
||||||
@ -52,7 +52,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jeffreydwalter/arlo-golang"
|
"github.com/jeffreydwalter/arlo-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -122,4 +122,4 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
** (coming soon) For more code examples check out the [wiki](https://github.com/jeffreydwalter/arlo-golang/wiki)**
|
** (coming soon) For more code examples check out the [wiki](https://github.com/jeffreydwalter/arlo-go/wiki)**
|
||||||
|
73
arlo.go
73
arlo.go
@ -17,10 +17,12 @@
|
|||||||
package arlo
|
package arlo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jeffreydwalter/arlo-golang/internal/request"
|
"github.com/jeffreydwalter/arlo-go/internal/request"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -57,7 +59,7 @@ func Login(user string, pass string) (arlo *Arlo, err error) {
|
|||||||
arlo = newArlo(user, pass)
|
arlo = newArlo(user, pass)
|
||||||
|
|
||||||
body := map[string]string{"email": arlo.user, "password": arlo.pass}
|
body := map[string]string{"email": arlo.user, "password": arlo.pass}
|
||||||
resp, err := arlo.post(LoginUri, "", body, nil)
|
resp, err := arlo.post(LoginV2Uri, "", body, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithMessage(err, "failed to login")
|
return nil, errors.WithMessage(err, "failed to login")
|
||||||
}
|
}
|
||||||
@ -91,31 +93,50 @@ func (a *Arlo) Logout() error {
|
|||||||
return checkRequest(resp, err, "failed to logout")
|
return checkRequest(resp, err, "failed to logout")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Arlo) CheckSession() (session *Session, err error) {
|
||||||
|
msg := "failed to get session"
|
||||||
|
resp, err := a.get(SessionUri, "", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithMessage(err, msg)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var response SessionResponse
|
||||||
|
if err := resp.Decode(&response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Success == false {
|
||||||
|
return nil, errors.WithMessage(errors.New(response.Reason), msg)
|
||||||
|
}
|
||||||
|
return &response.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetDevices returns an array of all devices.
|
// GetDevices returns an array of all devices.
|
||||||
// When you call Login, this method is called and all devices are cached in the arlo object.
|
// When you call Login, this method is called and all devices are cached in the arlo object.
|
||||||
func (a *Arlo) GetDevices() (devices *Devices, err error) {
|
func (a *Arlo) GetDevices() (devices *Devices, err error) {
|
||||||
resp, err := a.get(DevicesUri, "", nil)
|
resp, err := a.get(fmt.Sprintf(DevicesUri, time.Now().Format("20060102")), "", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithMessage(err, "failed to get devices")
|
return nil, errors.WithMessage(err, "failed to get devices")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var deviceResponse DeviceResponse
|
var response DeviceResponse
|
||||||
if err := resp.Decode(&deviceResponse); err != nil {
|
if err := resp.Decode(&response); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !deviceResponse.Success {
|
if !response.Success {
|
||||||
return nil, errors.New("failed to get devices")
|
return nil, errors.New("failed to get devices")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(deviceResponse.Data) == 0 {
|
if len(response.Data) == 0 {
|
||||||
return nil, errors.New("no devices found")
|
return nil, errors.New("no devices found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache a pointer to the arlo object with each device.
|
// Cache a pointer to the arlo object with each device.
|
||||||
for i := range deviceResponse.Data {
|
for i := range response.Data {
|
||||||
deviceResponse.Data[i].arlo = a
|
response.Data[i].arlo = a
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect all of the basestations from the EventStream.
|
// Disconnect all of the basestations from the EventStream.
|
||||||
@ -127,8 +148,8 @@ func (a *Arlo) GetDevices() (devices *Devices, err error) {
|
|||||||
|
|
||||||
a.rwmutex.Lock()
|
a.rwmutex.Lock()
|
||||||
// Cache the devices as their respective types.
|
// Cache the devices as their respective types.
|
||||||
a.Cameras = *deviceResponse.Data.GetCameras()
|
a.Cameras = *response.Data.GetCameras()
|
||||||
a.Basestations = *deviceResponse.Data.GetBasestations()
|
a.Basestations = *response.Data.GetBasestations()
|
||||||
a.rwmutex.Unlock()
|
a.rwmutex.Unlock()
|
||||||
|
|
||||||
// subscribe each basestation to the EventStream.
|
// subscribe each basestation to the EventStream.
|
||||||
@ -138,25 +159,45 @@ func (a *Arlo) GetDevices() (devices *Devices, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &deviceResponse.Data, nil
|
return &response.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProfile returns the user profile for the currently logged in user.
|
||||||
|
func (a *Arlo) GetProfile() (profile *UserProfile, err error) {
|
||||||
|
resp, err := a.get(ProfileUri, "", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithMessage(err, "failed to get user profile")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var response UserProfileResponse
|
||||||
|
if err := resp.Decode(&response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.Success {
|
||||||
|
return nil, errors.New("failed to get user profile")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given.
|
// UpdateDisplayOrder sets the display order according to the order defined in the DeviceOrder given.
|
||||||
func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) error {
|
func (a *Arlo) UpdateDisplayOrder(d DeviceOrder) error {
|
||||||
resp, err := a.post(DeviceDisplayOrderUri, "", d, nil)
|
resp, err := a.post(CameraOrderUri, "", d, nil)
|
||||||
return checkRequest(resp, err, "failed to display order")
|
return checkRequest(resp, err, "failed to display order")
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateProfile takes a first and last name, and updates the user profile with that information.
|
// UpdateProfile takes a first and last name, and updates the user profile with that information.
|
||||||
func (a *Arlo) UpdateProfile(firstName, lastName string) error {
|
func (a *Arlo) UpdateProfile(firstName, lastName string) error {
|
||||||
body := map[string]string{"firstName": firstName, "lastName": lastName}
|
body := map[string]string{"firstName": firstName, "lastName": lastName}
|
||||||
resp, err := a.put(UserProfileUri, "", body, nil)
|
resp, err := a.put(ProfileUri, "", body, nil)
|
||||||
return checkRequest(resp, err, "failed to update profile")
|
return checkRequest(resp, err, "failed to update profile")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Arlo) UpdatePassword(pass string) error {
|
func (a *Arlo) UpdatePassword(pass string) error {
|
||||||
body := map[string]string{"currentPassword": a.pass, "newPassword": pass}
|
body := map[string]string{"currentPassword": a.pass, "newPassword": pass}
|
||||||
resp, err := a.post(UserChangePasswordUri, "", body, nil)
|
resp, err := a.post(UpdatePasswordUri, "", body, nil)
|
||||||
if err := checkRequest(resp, err, "failed to update password"); err != nil {
|
if err := checkRequest(resp, err, "failed to update password"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -167,6 +208,6 @@ func (a *Arlo) UpdatePassword(pass string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Arlo) UpdateFriends(f Friend) error {
|
func (a *Arlo) UpdateFriends(f Friend) error {
|
||||||
resp, err := a.put(UserFriendsUri, "", f, nil)
|
resp, err := a.put(FriendsUri, "", f, nil)
|
||||||
return checkRequest(resp, err, "failed to update friends")
|
return checkRequest(resp, err, "failed to update friends")
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package arlo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -53,7 +54,9 @@ func (b *Basestation) makeEventStreamRequest(payload EventStreamPayload, msg str
|
|||||||
payload.TransId = transId
|
payload.TransId = transId
|
||||||
|
|
||||||
if err := b.IsConnected(); err != nil {
|
if err := b.IsConnected(); err != nil {
|
||||||
return nil, errors.WithMessage(err, msg)
|
//if err := b.Subscribe(); err != nil {
|
||||||
|
return nil, errors.WithMessage(errors.WithMessage(err, msg), "failed to reconnect to event stream")
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriber := make(subscriber)
|
subscriber := make(subscriber)
|
||||||
@ -100,7 +103,7 @@ func (b *Basestation) IsConnected() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Basestation) Subscribe() error {
|
func (b *Basestation) Subscribe() error {
|
||||||
b.eventStream = newEventStream(BaseUrl+fmt.Sprintf(SubscribeUri, b.arlo.Account.Token), b.arlo.client.HttpClient)
|
b.eventStream = newEventStream(BaseUrl+fmt.Sprintf(NotifyResponsesPushServiceUri, b.arlo.Account.Token), &http.Client{Jar: b.arlo.client.HttpClient.Jar})
|
||||||
|
|
||||||
forLoop:
|
forLoop:
|
||||||
for {
|
for {
|
||||||
@ -137,6 +140,11 @@ forLoop:
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Basestation) Unsubscribe() error {
|
||||||
|
resp, err := b.arlo.get(UnsubscribeUri, b.XCloudId, nil)
|
||||||
|
return checkRequest(resp, err, "failed to unsubscribe from event stream")
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Basestation) Disconnect() error {
|
func (b *Basestation) Disconnect() error {
|
||||||
// disconnect channel to stop event stream.
|
// disconnect channel to stop event stream.
|
||||||
if b.eventStream != nil {
|
if b.eventStream != nil {
|
||||||
|
301
camera.go
301
camera.go
@ -19,6 +19,7 @@ package arlo
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -225,7 +226,177 @@ func (c *Camera) DisableAudioAlerts(sensitivity int) (response *EventStreamRespo
|
|||||||
// FIXME: This feature requires more API calls to make it actually work, and I haven't figure out how to fully implement it.
|
// FIXME: This feature requires more API calls to make it actually work, and I haven't figure out how to fully implement it.
|
||||||
// It appears that the audio stream is Real-Time Transport Protocol (RTP), which requires a player (ffmpeg?) to consume the stream.
|
// It appears that the audio stream is Real-Time Transport Protocol (RTP), which requires a player (ffmpeg?) to consume the stream.
|
||||||
func (c *Camera) PushToTalk() error {
|
func (c *Camera) PushToTalk() error {
|
||||||
resp, err := c.arlo.get(fmt.Sprintf(PushToTalkUri, c.UniqueId), c.XCloudId, nil)
|
/*
|
||||||
|
processResponse: function(e) {
|
||||||
|
if (g.pc)
|
||||||
|
if (e.properties && "answerSdp" == e.properties.type) {
|
||||||
|
var t = e.properties.data
|
||||||
|
, i = {
|
||||||
|
type: "answer",
|
||||||
|
sdp: t
|
||||||
|
};
|
||||||
|
r.debug(i),
|
||||||
|
g.pc.setRemoteDescription(new g.SessionDescription(i), u, d)
|
||||||
|
} else if (e.properties && "answerCandidate" == e.properties.type)
|
||||||
|
if (g.candidateCache)
|
||||||
|
g.candidateCache.push(e.properties.data);
|
||||||
|
else {
|
||||||
|
var n = e.properties.data
|
||||||
|
, a = window.mozRTCIceCandidate || window.RTCIceCandidate
|
||||||
|
, o = new a({
|
||||||
|
candidate: n,
|
||||||
|
sdpMLineIndex: 0
|
||||||
|
});
|
||||||
|
r.debug(o),
|
||||||
|
g.pc.addIceCandidate(o)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startConnection: function(t) {
|
||||||
|
g.loading = !0,
|
||||||
|
g.error = !1,
|
||||||
|
g.candidateCache = [];
|
||||||
|
var i = t.deviceId
|
||||||
|
, o = t.parentId
|
||||||
|
, u = t.uniqueId;
|
||||||
|
g.device = t;
|
||||||
|
var p = {
|
||||||
|
method: "GET",
|
||||||
|
url: l.getPttUrl(u),
|
||||||
|
data: "",
|
||||||
|
headers: {
|
||||||
|
Authorization: s.ssoToken,
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Data-Type": "json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
r.debug("getting ptt data: " + JSON.stringify(p));
|
||||||
|
n(p).then(function(u) {
|
||||||
|
if (!u.data.success)
|
||||||
|
return e.$broadcast("show_error", u.data),
|
||||||
|
void (g.error = u.data.data.message || !0);
|
||||||
|
var m = u.data.data.data;
|
||||||
|
g.uSessionId = u.data.data.uSessionId,
|
||||||
|
_.each(m, function(e) {
|
||||||
|
e.url && (e.urls = e.url,
|
||||||
|
delete e.url)
|
||||||
|
});
|
||||||
|
var f = new g.PeerConnection({
|
||||||
|
iceServers: m,
|
||||||
|
iceCandidatePoolSize: 0
|
||||||
|
});
|
||||||
|
f.onicecandidate = function(e) {
|
||||||
|
if (null != e.candidate) {
|
||||||
|
r.debug(e.candidate);
|
||||||
|
var a = {
|
||||||
|
action: "pushToTalk",
|
||||||
|
from: t.userId,
|
||||||
|
publishResponse: !1,
|
||||||
|
resource: "cameras/" + i,
|
||||||
|
responseUrl: "",
|
||||||
|
to: o,
|
||||||
|
transId: "web!98b0c88b!1429756137177",
|
||||||
|
properties: {
|
||||||
|
uSessionId: g.uSessionId,
|
||||||
|
type: "offerCandidate",
|
||||||
|
data: e.candidate.candidate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
p = {
|
||||||
|
method: "POST",
|
||||||
|
url: l.getPttNotifyUrl(o),
|
||||||
|
data: a,
|
||||||
|
headers: {
|
||||||
|
xcloudId: t.xCloudId,
|
||||||
|
Authorization: s.ssoToken
|
||||||
|
}
|
||||||
|
},
|
||||||
|
n(p)
|
||||||
|
} else
|
||||||
|
r.debug("Failed to get any more candidate")
|
||||||
|
}
|
||||||
|
,
|
||||||
|
f.oniceconnectionstatechange = function(e) {
|
||||||
|
r.debug("ICE Connection State Change:" + f.iceConnectionState),
|
||||||
|
"connected" == f.iceConnectionState || "completed" == f.iceConnectionState ? g.loading = !1 : "disconnected" != f.iceConnectionState && "failed" != f.iceConnectionState || (g.stopConnection(),
|
||||||
|
g.error = a("i18n")("camera_label_ptt_failed_to_connect"))
|
||||||
|
}
|
||||||
|
,
|
||||||
|
g.pc = f,
|
||||||
|
(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia).call(navigator, {
|
||||||
|
audio: !0,
|
||||||
|
video: !1
|
||||||
|
}, function(e) {
|
||||||
|
g.stream = e,
|
||||||
|
g.stream.getAudioTracks()[0].enabled = !1,
|
||||||
|
f.addStream(e),
|
||||||
|
f.createOffer(function(e) {
|
||||||
|
f.setLocalDescription(e, c, d),
|
||||||
|
r.debug(e.sdp);
|
||||||
|
var a = {
|
||||||
|
action: "pushToTalk",
|
||||||
|
from: t.userId,
|
||||||
|
publishResponse: !0,
|
||||||
|
resource: "cameras/" + i,
|
||||||
|
responseUrl: "",
|
||||||
|
to: o,
|
||||||
|
transId: "web!98b0c88b!1429756137177",
|
||||||
|
properties: {
|
||||||
|
uSessionId: g.uSessionId,
|
||||||
|
type: "offerSdp",
|
||||||
|
data: e.sdp
|
||||||
|
}
|
||||||
|
};
|
||||||
|
p = {
|
||||||
|
method: "POST",
|
||||||
|
url: l.getPttNotifyUrl(o),
|
||||||
|
data: a,
|
||||||
|
headers: {
|
||||||
|
xcloudId: t.xCloudId,
|
||||||
|
Authorization: s.ssoToken
|
||||||
|
}
|
||||||
|
},
|
||||||
|
n(p)
|
||||||
|
}, d)
|
||||||
|
}, d)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
stopConnection: function() {
|
||||||
|
if (g.pc) {
|
||||||
|
var e = {
|
||||||
|
action: "pushToTalk",
|
||||||
|
from: g.device.userId,
|
||||||
|
publishResponse: !1,
|
||||||
|
resource: "cameras/" + g.device.deviceId,
|
||||||
|
responseUrl: "",
|
||||||
|
to: g.device.deviceId,
|
||||||
|
transId: "web!98b0c88b!1429756137177",
|
||||||
|
properties: {
|
||||||
|
uSessionId: g.uSessionId,
|
||||||
|
type: "endSession"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, t = {
|
||||||
|
method: "POST",
|
||||||
|
url: l.getPttNotifyUrl(g.device.deviceId),
|
||||||
|
data: e,
|
||||||
|
headers: {
|
||||||
|
xcloudId: g.device.xCloudId,
|
||||||
|
Authorization: s.ssoToken
|
||||||
|
}
|
||||||
|
};
|
||||||
|
n(t);
|
||||||
|
try {
|
||||||
|
g.stream.getAudioTracks()[0].stop(),
|
||||||
|
g.stream = null
|
||||||
|
} catch (e) {}
|
||||||
|
g.pc.close(),
|
||||||
|
g.pc = null,
|
||||||
|
g.loading = !0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
resp, err := c.arlo.get(fmt.Sprintf(PttUri, c.UniqueId), c.XCloudId, nil)
|
||||||
return checkRequest(resp, err, "failed to enable push to talk")
|
return checkRequest(resp, err, "failed to enable push to talk")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,10 +433,10 @@ func (c *Camera) SetAlertNotificationMethods(action string, email, push bool) (r
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StartStream returns a json object containing the rtmps url to the requested video stream.
|
// StartStream returns a json object containing the rtmps url to the requested video stream.
|
||||||
// You will need the to install a library to handle streaming of this protocol: https://pypi.python.org/pypi/python-librtmp
|
// You will need something like ffmpeg to read the rtmps stream.
|
||||||
//
|
|
||||||
// The request to /users/devices/startStream returns:
|
// If you call StartStream(), you have to start reading data from the stream, or streaming will be cancelled
|
||||||
// NOTE: { "url":"rtsp://vzwow09-z2-prod.vz.netgear.com:80/vzmodulelive?egressToken=b1b4b675_ac03_4182_9844_043e02a44f71&userAgent=web&cameraId=48B4597VD8FF5_1473010750131" }
|
// and taking a snapshot may fail (since it requires the stream to be active).
|
||||||
func (c *Camera) StartStream() (url string, err error) {
|
func (c *Camera) StartStream() (url string, err error) {
|
||||||
payload := EventStreamPayload{
|
payload := EventStreamPayload{
|
||||||
Action: "set",
|
Action: "set",
|
||||||
@ -282,7 +453,7 @@ func (c *Camera) StartStream() (url string, err error) {
|
|||||||
|
|
||||||
msg := "failed to start stream"
|
msg := "failed to start stream"
|
||||||
|
|
||||||
resp, err := c.arlo.post(DeviceStartStreamUri, c.XCloudId, payload, nil)
|
resp, err := c.arlo.post(StartStreamUri, c.XCloudId, payload, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithMessage(err, msg)
|
return "", errors.WithMessage(err, msg)
|
||||||
}
|
}
|
||||||
@ -297,27 +468,79 @@ func (c *Camera) StartStream() (url string, err error) {
|
|||||||
return "", errors.WithMessage(errors.New("status was false"), msg)
|
return "", errors.WithMessage(errors.New("status was false"), msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
response.URL = strings.Replace(response.URL, "rtsp://", "rtsps://", 1)
|
response.Data.URL = strings.Replace(response.Data.URL, "rtsp://", "rtsps://", 1)
|
||||||
|
|
||||||
return response.URL, nil
|
return response.Data.URL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TakeSnapshot causes the camera to record a snapshot.
|
// TakeSnapshot causes the camera to snapshot while recording.
|
||||||
func (c *Camera) TakeSnapshot() (url string, err error) {
|
// NOTE: You MUST call StartStream() before calling this function.
|
||||||
|
// If you call StartStream(), you have to start reading data from the stream, or streaming will be cancelled
|
||||||
|
// and taking a snapshot may fail (since it requires the stream to be active).
|
||||||
|
|
||||||
|
// NOTE: You should not use this function is you just want a snapshot and aren't intending to stream.
|
||||||
|
// Use TriggerFullFrameSnapshot() instead.
|
||||||
|
//
|
||||||
|
// NOTE: Use DownloadSnapshot() to download the actual image file.
|
||||||
|
// TODO: Need to refactor the even stream code to allow handling of events whose transIds don't correlate. :/
|
||||||
|
func (c *Camera) TakeSnapshot() (response *EventStreamResponse, err error) {
|
||||||
|
|
||||||
|
return nil, errors.New("TakeSnapshot not implemented")
|
||||||
|
/*
|
||||||
msg := "failed to take snapshot"
|
msg := "failed to take snapshot"
|
||||||
|
|
||||||
url, err = c.StartStream()
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.WithMessage(err, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
|
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
|
||||||
resp, err := c.arlo.post(DeviceTakeSnapshotUri, c.XCloudId, body, nil)
|
resp, err := c.arlo.post(TakeSnapshotUri, c.XCloudId, body, nil)
|
||||||
if err := checkRequest(resp, err, "failed to update device name"); err != nil {
|
if err := checkRequest(resp, err, msg); err != nil {
|
||||||
return "", errors.WithMessage(err, msg)
|
return nil, errors.WithMessage(err, msg)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Need to write the code to handle the event stream message.
|
||||||
|
/*
|
||||||
|
def callback(self, event):
|
||||||
|
if event.get("deviceId") == camera.get("deviceId") and event.get("resource") == "mediaUploadNotification":
|
||||||
|
presigned_content_url = event.get("presignedContentUrl")
|
||||||
|
if presigned_content_url is not None:
|
||||||
|
r return presigned_content_url
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
return url, nil
|
// TriggerFullFrameSnapshot causes the camera to record a full-frame snapshot.
|
||||||
|
// The presignedFullFrameSnapshotUrl url is returned.
|
||||||
|
// Use DownloadSnapshot() to download the actual image file.
|
||||||
|
// TODO: Need to refactor the even stream code to allow handling of events whose transIds don't correlate. :/
|
||||||
|
func (c *Camera) TriggerFullFrameSnapshot() (response *EventStreamResponse, err error) {
|
||||||
|
|
||||||
|
return nil, errors.New("TriggerFullFrameSnapshot not implemented")
|
||||||
|
/*
|
||||||
|
payload := EventStreamPayload{
|
||||||
|
Action: "set",
|
||||||
|
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
|
||||||
|
PublishResponse: true,
|
||||||
|
Properties: map[string]string{
|
||||||
|
"activityState": "fullFrameSnapshot",
|
||||||
|
},
|
||||||
|
TransId: genTransId(),
|
||||||
|
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
|
||||||
|
To: c.ParentId,
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := "failed to trigger full-frame snapshot"
|
||||||
|
|
||||||
|
b := c.arlo.Basestations.Find(c.ParentId)
|
||||||
|
if b == nil {
|
||||||
|
err := fmt.Errorf("basestation (%s) not found for camera (%s)", c.ParentId, c.DeviceId)
|
||||||
|
return nil, errors.WithMessage(err, msg)
|
||||||
|
}
|
||||||
|
return b.makeEventStreamRequest(payload, msg)
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
def callback(self, event):
|
||||||
|
if event.get("from") == basestation.get("deviceId") and event.get("resource") == "cameras/"+camera.get("deviceId") and event.get("action") == "fullFrameSnapshotAvailable":
|
||||||
|
return event.get("properties", {}).get("presignedFullFrameSnapshotUrl")
|
||||||
|
return None
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartRecording causes the camera to start recording and returns a url that you must start reading from using ffmpeg
|
// StartRecording causes the camera to start recording and returns a url that you must start reading from using ffmpeg
|
||||||
@ -331,10 +554,46 @@ func (c *Camera) StartRecording() (url string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
|
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
|
||||||
resp, err := c.arlo.post(DeviceStartRecordUri, c.XCloudId, body, nil)
|
resp, err := c.arlo.post(StartRecordUri, c.XCloudId, body, nil)
|
||||||
if err := checkRequest(resp, err, "failed to update device name"); err != nil {
|
if err := checkRequest(resp, err, msg); err != nil {
|
||||||
return "", errors.WithMessage(err, msg)
|
return "", errors.WithMessage(err, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return url, nil
|
return url, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StopRecording causes the camera to stop recording.
|
||||||
|
func (c *Camera) StopRecording() error {
|
||||||
|
msg := "failed to stop recording"
|
||||||
|
|
||||||
|
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
|
||||||
|
resp, err := c.arlo.post(StopRecordUri, c.XCloudId, body, nil)
|
||||||
|
if err := checkRequest(resp, err, msg); err != nil {
|
||||||
|
return errors.WithMessage(err, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function downloads a Cvr Playlist file for the period fromDate to toDate.
|
||||||
|
func (c *Camera) GetCvrPlaylist(fromDate, toDate time.Time) (playlist *CvrPlaylist, err error) {
|
||||||
|
msg := "failed to get cvr playlist"
|
||||||
|
|
||||||
|
resp, err := c.arlo.get(fmt.Sprintf(PlaylistUri, c.UniqueId, fromDate.Format("20060102"), toDate.Format("20060102")), c.XCloudId, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithMessage(err, msg)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
response := new(CvrPlaylistResponse)
|
||||||
|
if err := resp.Decode(&response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.Success {
|
||||||
|
return nil, errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response.Data, nil
|
||||||
|
}
|
||||||
|
157
const.go
157
const.go
@ -17,35 +17,136 @@
|
|||||||
package arlo
|
package arlo
|
||||||
|
|
||||||
const (
|
const (
|
||||||
BaseUrl = "https://arlo.netgear.com/hmsweb"
|
DeviceTypeArloQ = "arloq"
|
||||||
LoginUri = "/login/v2"
|
DeviceTypeArloBridge = "arlobridge"
|
||||||
LogoutUri = "/logout"
|
|
||||||
SubscribeUri = "/client/subscribe?token=%s"
|
|
||||||
UnsubscribeUri = "/client/unsubscribe"
|
|
||||||
NotifyUri = "/users/devices/notify/%s"
|
|
||||||
ServiceLevelUri = "/users/serviceLevel"
|
|
||||||
OffersUri = "/users/payment/offers"
|
|
||||||
UserProfileUri = "/users/profile"
|
|
||||||
PushToTalkUri = "/users/devices/%s/pushtotalk"
|
|
||||||
UserChangePasswordUri = "/users/changePassword"
|
|
||||||
UserSessionUri = "/users/session"
|
|
||||||
UserFriendsUri = "/users/friends"
|
|
||||||
UserLocationsUri = "/users/locations"
|
|
||||||
UserLocationUri = "/users/locations/%s"
|
|
||||||
LibraryUri = "/users/library"
|
|
||||||
LibraryMetadataUri = "/users/library/metadata"
|
|
||||||
LibraryRecycleUri = "/users/library/recycle"
|
|
||||||
LibraryResetUri = "/users/library/reset"
|
|
||||||
DevicesUri = "/users/devices"
|
|
||||||
DeviceRenameUri = "/users/devices/renameDevice"
|
|
||||||
DeviceDisplayOrderUri = "/users/devices/displayOrder"
|
|
||||||
DeviceTakeSnapshotUri = "/users/devices/takeSnapshot"
|
|
||||||
DeviceStartRecordUri = "/users/devices/startRecord"
|
|
||||||
DeviceStopRecordUri = "/users/devices/stopRecord"
|
|
||||||
DeviceStartStreamUri = "/users/devices/startStream"
|
|
||||||
|
|
||||||
DeviceTypeBasestation = "basestation"
|
DeviceTypeBasestation = "basestation"
|
||||||
DeviceTypeCamera = "camera"
|
DeviceTypeCamera = "camera"
|
||||||
DeviceTypeArloQ = "arloq"
|
DeviceTypeLights = "lights"
|
||||||
|
DeviceTypeSiren = "siren"
|
||||||
|
|
||||||
TransIdPrefix = "web"
|
TransIdPrefix = "web"
|
||||||
|
BaseUrl = "https://arlo.netgear.com/hmsweb"
|
||||||
|
|
||||||
|
// TODO: Implement all of the following urls. There are many here I don't have devices for. :/
|
||||||
|
ActiveAutomationUri = "/users/devices/automation/active"
|
||||||
|
AllMediaUri = "/users/music/all"
|
||||||
|
AnalyticFeedbackUri = "/users/library/%s/feedback"
|
||||||
|
AssignBetaPlanUri = "/users/assign/smartfeatures"
|
||||||
|
AssignDeviceToServicePlanUri = "/users/devices/provision"
|
||||||
|
AutomationDefinitionsUri = "/users/automation/definitions?uniqueIds=all"
|
||||||
|
AutomationModeUri = "/users/locations/%uniqueId/modes"
|
||||||
|
AutomationScheduleUri = "/users/locations/%uniqueId/schedules"
|
||||||
|
BuyUri = "http:/www.arlo.com/en-us/products/default.aspx?utm_source=app_desktop&p=all&cid=app"
|
||||||
|
CameraOrderUri = "/users/devices/v2/displayOrder"
|
||||||
|
CancelPlanUri = "/users/payment/plans/%paymentId/cancel"
|
||||||
|
CancelQuotationUri = "/users/payment/quotations/%paymentId/cancel/v1"
|
||||||
|
CapabilitiesUri = "/resources/capabilities/en/en_%t|i1000.json?t=%s"
|
||||||
|
ChangeMobileOffersUri = "/users/payment/offers/dataplans/change/v5"
|
||||||
|
ChangePlanUri = "/users/%paymentId/changeplan"
|
||||||
|
CheckAccountUri = "/checkAccountUsage"
|
||||||
|
CheckEmailUri = "/checkEmailUsage"
|
||||||
|
ClaimDeviceUri = "/users/devices/claimDevice"
|
||||||
|
CommunityUri = "http:/www.arlo.com/community?utm_source=app_desktop&locale=en"
|
||||||
|
ConfirmUserIdUri = "/users/resend/confirm/email"
|
||||||
|
CountryCodesUri = "/static/countrycodes"
|
||||||
|
CreatePaymentAccountUri = "/users/payment/accounts"
|
||||||
|
CreatePlanUri = "/users/payment/plans/%paymentId"
|
||||||
|
DeleteAccountUri = "/users/closeAccount"
|
||||||
|
DeviceFeaturesUri = "/users/devices/updateFeatures/feature/%uniqueId"
|
||||||
|
DeviceProvisioningUri = "/users/devices/states"
|
||||||
|
DeviceStatisticsUri = "/users/devices/%uniqueId/data/statistics?t=%s"
|
||||||
|
DeviceSupportUri = "/devicesupport"
|
||||||
|
DeviceSupportV2Uri = "/devicesupport/v2"
|
||||||
|
DeviceUri = "/users/devices/%deviceId"
|
||||||
|
DeviceZoneUri = "/users/devices/%uniqueId/activityzones/%zoneId"
|
||||||
|
DeviceZonesUri = "/users/devices/%uniqueId/activityzones"
|
||||||
|
DevicesUpdateFeaturesUri = "/users/devices/updateFeatures/feature"
|
||||||
|
DevicesUri = "/users/devices/?t=%s"
|
||||||
|
DonateRecordUri = "/users/library/%uniqueId/donate"
|
||||||
|
EditAutomationModeUri = "/users/locations/%uniqueId/modes/%mode/devices/%i"
|
||||||
|
EditUri = "/users/media"
|
||||||
|
EmergencyCallDetailsUri = "/users/emergency/%emergencyId/call"
|
||||||
|
EmergencyLocationSaveUri = "/users/emergency/locations/%emergencyId"
|
||||||
|
EmergencyLocationsUri = "/users/emergency/locations"
|
||||||
|
EventsUri = "/users/devices/%uniqueId/events?fromDate=%s&toDate=%s"
|
||||||
|
FavoriteUri = "/users/library/favorite"
|
||||||
|
FieldLengthsUri = "/static/fieldLengths"
|
||||||
|
FriendsDeleteUri = "/users/friends/remove"
|
||||||
|
FriendsUri = "/users/friends"
|
||||||
|
FullFrameSnapshotUri = "/users/devices/fullFrameSnapshot"
|
||||||
|
GPLUri = "https:/vzs3-prod-common.s3.amazonaws.com/license/GPLv1.html"
|
||||||
|
HtmlChangeOffersUri = "/users/payment/offers/html/v5/change"
|
||||||
|
HtmlOffersUri = "/users/payment/offers/html/v5"
|
||||||
|
HtmlPrelimQuotationUri = "/users/payment/offers/quotation/html/v5"
|
||||||
|
HtmlQuotationUri = "/users/payment/confirmation/%paymentId"
|
||||||
|
LibFeedbackUri = "/library/feedback"
|
||||||
|
LibraryStateUri = "/users/library/state/v1"
|
||||||
|
LocateDevicesUri = "/locateDevice?discoveryToken=%s"
|
||||||
|
LocationByZipUri = "/users/payment/postalcodelookup"
|
||||||
|
LocationUri = "/users/locations"
|
||||||
|
LoginUri = "/login"
|
||||||
|
LoginV2Uri = "/login/v2"
|
||||||
|
LogoutUri = "/logout"
|
||||||
|
MetadataUri = "/users/library/metadata/v2"
|
||||||
|
MigrateZonesUri = "/users/devices/%uniqueId/activityzones/migrate"
|
||||||
|
MobileOffersUri = "/users/payment/offers/dataplans/v5"
|
||||||
|
ModifyBillingUri = "/users/payment/billing/%paymentId"
|
||||||
|
NotifyResponsesPushServiceUri = "/client/subscribe?token=%s"
|
||||||
|
NotifyUri = "/users/devices/notify/%s"
|
||||||
|
OffersDetailsUri = "/users/payment/offersdetail"
|
||||||
|
OffersDvrChangeUri = "/users/payment/offers/arloq/html/v5/change"
|
||||||
|
OffersDvrUri = "/users/payment/offers/arloq/html/v5"
|
||||||
|
OffersUri = "/users/payment/offers/v3"
|
||||||
|
PaymentBillingUri = "/users/payment/billing/%paymentId"
|
||||||
|
PaymentRenewUri = "/users/payment/autoRenew/%paymentId"
|
||||||
|
PaymentTermsLinkUri = "/paymentTermsAndConditions?t=%s"
|
||||||
|
PlacemeterUri = ""
|
||||||
|
PlaylistMetaUri = "/users/devices/%uniqueId/playlist/metadata"
|
||||||
|
PlaylistUri = "/users/devices/%s/playlist?fromDate=%s&toDate=%s"
|
||||||
|
PolicyUri = "/policy/v1/?t=%s"
|
||||||
|
PreferencesUri = "/users/preferences"
|
||||||
|
ProfileUri = "/users/profile"
|
||||||
|
PttNotifyUri = "/users/devices/notify/%parentId"
|
||||||
|
PttUri = "/users/devices/%s/pushtotalk"
|
||||||
|
RMAValidationUri = "/users/devices/%restrictedDevice/apvalidation"
|
||||||
|
RecordingsUri = "/users/library"
|
||||||
|
RecycleUri = "/users/library/recycle"
|
||||||
|
RegisterUserUri = "/register"
|
||||||
|
RemoveDeviceUri = "/users/devices/v2/removeDevice"
|
||||||
|
RenameDeviceUri = "/users/devices/v2/renameDevice"
|
||||||
|
RenewPlanUri = "/users/payment/plans/%paymentId/renew"
|
||||||
|
RenewQuotationUri = "/users/payment/quotations/%?/renew"
|
||||||
|
RequestPasswordResetUri = "/requestPasswordReset"
|
||||||
|
ResetCountUri = "/users/library/reset?uniqueId=%s"
|
||||||
|
ResetPasswordUri = "/resetPassword"
|
||||||
|
RestartDeviceUri = "/users/devices/restart"
|
||||||
|
SSORegisterUri = "/ssoregister"
|
||||||
|
SecretQuestionsUri = "/static/secretquestions"
|
||||||
|
ServicePlanUri = "/users/serviceLevel/v3"
|
||||||
|
SessionUri = "/users/session"
|
||||||
|
SetAutomationModeUri = "/users/locations/%uniqueId/modes/%mode"
|
||||||
|
ShareUri = "/users/library/share"
|
||||||
|
SmartAlertsUri = "/users/devices/%uniqueId/smartalerts"
|
||||||
|
SmartConfigUri = "/user/smarthome/config"
|
||||||
|
StartRecordUri = "/users/devices/startRecord"
|
||||||
|
StartStreamUri = "/users/devices/startStream"
|
||||||
|
StatesCodesUri = "/static/usstatescodes"
|
||||||
|
StopRecordUri = "/users/devices/stopRecord"
|
||||||
|
StopStreamUri = "/users/devices/stopStream"
|
||||||
|
StorageQuotaUri = "/users/quota"
|
||||||
|
SupportUri = "http:/www.arlo.com/support?utm_source=app_desktop&cc=en"
|
||||||
|
TakeSnapshotUri = "/users/devices/takeSnapshot"
|
||||||
|
TempUnitUri = "/users/devices/%uniqueId/tempUnit"
|
||||||
|
TermsLinkUri = "/termsAndConditionsLink?t=%s"
|
||||||
|
TermsUri = "/termsAndConditions/?t=%s"
|
||||||
|
TimeZonesUri = "/static/timezones"
|
||||||
|
UnsubscribeUri = "/client/unsubscribe"
|
||||||
|
UpdateNameUri = "/user"
|
||||||
|
UpdatePasswordUri = "/users/changePassword"
|
||||||
|
UpdateUserIdUri = "/users/changeEmail"
|
||||||
|
UserFrameSnapshotUri = "/users/devices/userSnapshot"
|
||||||
|
UsersEmailsUri = "/users/emails"
|
||||||
|
ValidateCouponUri = "/users/payment/coupondetails"
|
||||||
|
ValidateResetUri = "/validatePasswordReset/%?"
|
||||||
|
WakeupUri = "/users/devices/wakeup/%deviceId?t=%s"
|
||||||
)
|
)
|
||||||
|
@ -135,8 +135,7 @@ func (ds Devices) GetCameras() *Cameras {
|
|||||||
|
|
||||||
// UpdateDeviceName sets the name of the given device to the name argument.
|
// UpdateDeviceName sets the name of the given device to the name argument.
|
||||||
func (d *Device) UpdateDeviceName(name string) error {
|
func (d *Device) UpdateDeviceName(name string) error {
|
||||||
|
|
||||||
body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId}
|
body := map[string]string{"deviceId": d.DeviceId, "deviceName": name, "parentId": d.ParentId}
|
||||||
resp, err := d.arlo.put(DeviceRenameUri, d.XCloudId, body, nil)
|
resp, err := d.arlo.put(RenameDeviceUri, d.XCloudId, body, nil)
|
||||||
return checkRequest(resp, err, "failed to update device name")
|
return checkRequest(resp, err, "failed to update device name")
|
||||||
}
|
}
|
||||||
|
@ -75,10 +75,13 @@ func (e *eventStream) disconnect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *eventStream) listen() (connected chan bool) {
|
func (e *eventStream) listen() (connected chan bool) {
|
||||||
|
|
||||||
connected = make(chan bool)
|
connected = make(chan bool)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
e.SSEClient.OnDisconnect(func(c *sse.Client) {
|
||||||
|
e.disconnect()
|
||||||
|
// fmt.Printf("\n\n\n\nClIENT DISCONNECTED!!!!!\n\n\n\n")
|
||||||
|
})
|
||||||
err := e.SSEClient.SubscribeChanRaw(e.Events)
|
err := e.SSEClient.SubscribeChanRaw(e.Events)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.Error <- FAILED_TO_SUBSCRIBE
|
e.Error <- FAILED_TO_SUBSCRIBE
|
||||||
@ -87,13 +90,14 @@ func (e *eventStream) listen() (connected chan bool) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event := <-e.Events:
|
case event := <-e.Events:
|
||||||
|
//fmt.Println("Got event message.")
|
||||||
/*
|
/*
|
||||||
fmt.Println("Got event message.")
|
fmt.Print(".")
|
||||||
fmt.Printf("EVENT: %s\n", event.Event)
|
fmt.Printf("EVENT: %s\n", event.Event)
|
||||||
fmt.Printf("DATA: %s\n", event.URL)
|
fmt.Printf("DATA: %s\n", event.Data)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if event.Data != nil {
|
if event != nil && event.Data != nil {
|
||||||
notifyResponse := &EventStreamResponse{}
|
notifyResponse := &EventStreamResponse{}
|
||||||
b := bytes.NewBuffer(event.Data)
|
b := bytes.NewBuffer(event.Data)
|
||||||
err := json.NewDecoder(b).Decode(notifyResponse)
|
err := json.NewDecoder(b).Decode(notifyResponse)
|
||||||
@ -102,6 +106,7 @@ func (e *eventStream) listen() (connected chan bool) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: This is a shitty way to handle this. It's potentially leaking a chan.
|
||||||
if notifyResponse.Status == "connected" {
|
if notifyResponse.Status == "connected" {
|
||||||
connected <- true
|
connected <- true
|
||||||
} else if notifyResponse.Status == "disconnected" {
|
} else if notifyResponse.Status == "disconnected" {
|
||||||
|
@ -20,11 +20,11 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ func NewClient(baseURL string, baseHeaders http.Header) (*Client, error) {
|
|||||||
return &Client{
|
return &Client{
|
||||||
BaseURL: u,
|
BaseURL: u,
|
||||||
BaseHeaders: &header,
|
BaseHeaders: &header,
|
||||||
HttpClient: &http.Client{Jar: jar},
|
HttpClient: &http.Client{Jar: jar, Timeout: 30 * time.Second},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ func (c *Client) newRequest(method string, uri string, body interface{}, header
|
|||||||
return nil, errors.Wrap(err, "failed to create request object")
|
return nil, errors.Wrap(err, "failed to create request object")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("\n\nBODY (%s): %s\n\n", uri, buf)
|
// log.Printf("\n\nBODY (%s): %s\n\n", uri, buf)
|
||||||
|
|
||||||
u := c.BaseURL.String() + uri
|
u := c.BaseURL.String() + uri
|
||||||
req, err := http.NewRequest(method, u, buf)
|
req, err := http.NewRequest(method, u, buf)
|
||||||
@ -145,8 +145,8 @@ func (c *Client) newResponse(resp *http.Response) (*Response, error) {
|
|||||||
|
|
||||||
func (c *Client) do(req *Request) (*Response, error) {
|
func (c *Client) do(req *Request) (*Response, error) {
|
||||||
|
|
||||||
log.Printf("\n\nCOOKIES (%s): %v\n\n", req.URL, c.HttpClient.Jar.Cookies(req.URL))
|
// log.Printf("\n\nCOOKIES (%s): %v\n\n", req.URL, c.HttpClient.Jar.Cookies(req.URL))
|
||||||
log.Printf("\n\nHEADERS (%s): %v\n\n", req.URL, req.Header)
|
// log.Printf("\n\nHEADERS (%s): %v\n\n", req.URL, req.Header)
|
||||||
|
|
||||||
resp, err := c.HttpClient.Do(&req.Request)
|
resp, err := c.HttpClient.Do(&req.Request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
107
library.go
107
library.go
@ -57,7 +57,7 @@ func (a *Arlo) GetLibraryMetaData(fromDate, toDate time.Time) (libraryMetaData *
|
|||||||
msg := "failed to get library metadata"
|
msg := "failed to get library metadata"
|
||||||
|
|
||||||
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
|
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
|
||||||
resp, err := a.post(LibraryMetadataUri, "", body, nil)
|
resp, err := a.post(MetadataUri, "", body, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithMessage(err, msg)
|
return nil, errors.WithMessage(err, msg)
|
||||||
}
|
}
|
||||||
@ -79,7 +79,7 @@ func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (library *Library, err err
|
|||||||
msg := "failed to get library"
|
msg := "failed to get library"
|
||||||
|
|
||||||
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
|
body := map[string]string{"dateFrom": fromDate.Format("20060102"), "dateTo": toDate.Format("20060102")}
|
||||||
resp, err := a.post(LibraryUri, "", body, nil)
|
resp, err := a.post(RecordingsUri, "", body, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithMessage(err, msg)
|
return nil, errors.WithMessage(err, msg)
|
||||||
}
|
}
|
||||||
@ -105,9 +105,8 @@ func (a *Arlo) GetLibrary(fromDate, toDate time.Time) (library *Library, err err
|
|||||||
NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
|
NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
|
||||||
*/
|
*/
|
||||||
func (a *Arlo) DeleteRecording(r *Recording) error {
|
func (a *Arlo) DeleteRecording(r *Recording) error {
|
||||||
|
|
||||||
body := map[string]Library{"data": {*r}}
|
body := map[string]Library{"data": {*r}}
|
||||||
resp, err := a.post(LibraryRecycleUri, "", body, nil)
|
resp, err := a.post(RecycleUri, "", body, nil)
|
||||||
return checkRequest(resp, err, "failed to delete recording")
|
return checkRequest(resp, err, "failed to delete recording")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,8 +118,104 @@ func (a *Arlo) DeleteRecording(r *Recording) error {
|
|||||||
NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
|
NOTE: {"data": [{"createdDate": r.CreatedDate, "utcCreatedDate": r.UtcCreatedDate, "deviceId": r.DeviceId}]} is all that's really required.
|
||||||
*/
|
*/
|
||||||
func (a *Arlo) BatchDeleteRecordings(l *Library) error {
|
func (a *Arlo) BatchDeleteRecordings(l *Library) error {
|
||||||
|
|
||||||
body := map[string]Library{"data": *l}
|
body := map[string]Library{"data": *l}
|
||||||
resp, err := a.post(LibraryRecycleUri, "", body, nil)
|
resp, err := a.post(RecycleUri, "", body, nil)
|
||||||
return checkRequest(resp, err, "failed to delete recordings")
|
return checkRequest(resp, err, "failed to delete recordings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendAnalyticFeedback is only really used by the GUI. It is a response to a prompt asking you whether an object which
|
||||||
|
// was tagged by it's AI in your recording was tagged correctly.
|
||||||
|
func (a *Arlo) SendAnalyticFeedback(r *Recording) error {
|
||||||
|
category := "Person" // Other
|
||||||
|
body := map[string]map[string]interface{}{"data": {"utcCreatedDate": r.UtcCreatedDate, "category": category, "createdDate": r.CreatedDate}}
|
||||||
|
resp, err := a.put(AnalyticFeedbackUri, "", body, nil)
|
||||||
|
return checkRequest(resp, err, "failed to send analytic feedback about recording")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveAutomationDefinitions gets the mode metadata (this API replaces the older GetModes(), which still works).
|
||||||
|
func (a *Arlo) GetActiveAutomationDefinitions() error {
|
||||||
|
resp, err := a.get(ActiveAutomationUri, "", nil)
|
||||||
|
return checkRequest(resp, err, "failed to get active automation definitions")
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func (a *Arlo) SetActiveAutomationMode() error {
|
||||||
|
|
||||||
|
body := struct{}{} //map[string]map[string]interface{}{"data": {"utcCreatedDate": r.UtcCreatedDate, "category": category, "createdDate": r.CreatedDate}}
|
||||||
|
resp, err := a.put(AnalyticFeedbackUri, "", body, nil)
|
||||||
|
return checkRequest(resp, err, "failed to send analytic feedback about recording")
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"activeModes": [
|
||||||
|
"mode1"
|
||||||
|
],
|
||||||
|
"activeSchedules": [],
|
||||||
|
"gatewayId": "48935B7SA9847",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"timestamp": 1536781758034,
|
||||||
|
"type": "activeAutomations",
|
||||||
|
"uniqueId": "336-4764296_48935B7SA9847"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
setActiveAutomationMode: function(r, a) {
|
||||||
|
var s = {
|
||||||
|
activeAutomations: [{
|
||||||
|
deviceId: a.gatewayId,
|
||||||
|
timestamp: _.now(),
|
||||||
|
activeModes: [r],
|
||||||
|
activeSchedules: []
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
, l = {
|
||||||
|
method: "POST",
|
||||||
|
data: s,
|
||||||
|
url: d.getActiveAutomationUrl(a.gatewayId),
|
||||||
|
headers: {
|
||||||
|
Authorization: o.ssoToken,
|
||||||
|
schemaVersion: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return n.debug("calling set active automation mode with config:" + JSON.stringify(l)),
|
||||||
|
i(l).then(function(i) {
|
||||||
|
if (n.debug("got set active automation mode result:" + JSON.stringify(i)),
|
||||||
|
i && i.data && !i.data.success)
|
||||||
|
return e.$broadcast(c.appEvents.SHOW_ERROR, i.data),
|
||||||
|
t.reject(i.data)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setActiveAutomationSchedule: function(r) {
|
||||||
|
var r = {
|
||||||
|
activeAutomations: [{
|
||||||
|
deviceId: r.deviceId,
|
||||||
|
timestamp: _.now(),
|
||||||
|
activeModes: [],
|
||||||
|
activeSchedules: [r.scheduleId]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
, a = {
|
||||||
|
method: "POST",
|
||||||
|
data: r,
|
||||||
|
url: d.getActiveAutomationUrl(r.deviceId),
|
||||||
|
headers: {
|
||||||
|
Authorization: o.ssoToken,
|
||||||
|
schemaVersion: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, s = this;
|
||||||
|
return n.debug("calling set active automation schedule with config:" + JSON.stringify(a)),
|
||||||
|
i(a).then(function(i) {
|
||||||
|
return n.debug("got set active automation schedule result:" + JSON.stringify(i)),
|
||||||
|
i && i.data && !i.data.success ? (e.$broadcast(c.appEvents.SHOW_ERROR, i.data),
|
||||||
|
t.reject(i.data)) : i && i.data && i.data.success ? (_.filter(s.activeAutomationDefinitions, function(e) {
|
||||||
|
e.gatewayId == i.config.data.activeAutomations[0].deviceId && (e.activeModes = i.config.data.activeAutomations[0].activeModes,
|
||||||
|
e.activeSchedules = i.config.data.activeAutomations[0].activeSchedules)
|
||||||
|
}),
|
||||||
|
i.config.data.activeAutomations[0]) : void 0
|
||||||
|
})
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
19
lights.go
Normal file
19
lights.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018 Jeffrey Walter <jeffreydwalter@gmail.com>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||||||
|
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
||||||
|
* Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||||
|
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package arlo
|
||||||
|
|
||||||
|
// TODO: Add support for "lights" type of devices.
|
37
responses.go
37
responses.go
@ -16,12 +16,36 @@
|
|||||||
|
|
||||||
package arlo
|
package arlo
|
||||||
|
|
||||||
|
// URL is part of the Status message fragment returned by most calls to the Arlo API.
|
||||||
|
// URL is only populated when Success is false.
|
||||||
|
type Data struct {
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status is the message fragment returned from most http calls to the Arlo API.
|
||||||
|
type Status struct {
|
||||||
|
Data `json:"URL,omitempty"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
// LoginResponse is an intermediate struct used when parsing data from the Login() call.
|
// LoginResponse is an intermediate struct used when parsing data from the Login() call.
|
||||||
type LoginResponse struct {
|
type LoginResponse struct {
|
||||||
Data Account
|
Data Account
|
||||||
Status
|
Status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SessionResponse struct {
|
||||||
|
Data Session
|
||||||
|
Status
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserProfileResponse struct {
|
||||||
|
Data UserProfile
|
||||||
|
Status
|
||||||
|
}
|
||||||
|
|
||||||
// DeviceResponse is an intermediate struct used when parsing data from the GetDevices() call.
|
// DeviceResponse is an intermediate struct used when parsing data from the GetDevices() call.
|
||||||
type DeviceResponse struct {
|
type DeviceResponse struct {
|
||||||
Data Devices
|
Data Devices
|
||||||
@ -39,13 +63,22 @@ type LibraryResponse struct {
|
|||||||
Status
|
Status
|
||||||
}
|
}
|
||||||
|
|
||||||
type StreamResponse struct {
|
type CvrPlaylistResponse struct {
|
||||||
|
Data CvrPlaylist
|
||||||
|
Status
|
||||||
|
}
|
||||||
|
|
||||||
|
type Stream struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StreamResponse struct {
|
||||||
|
Data Stream
|
||||||
Status
|
Status
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingResponse struct {
|
type RecordingResponse struct {
|
||||||
URL string `json:"url"`
|
Data Stream
|
||||||
Status
|
Status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
47
types.go
47
types.go
@ -59,6 +59,29 @@ type Account struct {
|
|||||||
DateCreated int64 `json:"dateCreated"`
|
DateCreated int64 `json:"dateCreated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppStore struct {
|
||||||
|
Enforce bool `json:"enforce"`
|
||||||
|
LatestVersion string `json:"latestVersion"`
|
||||||
|
updateLink string `json:"updateLink"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
AccountStatus string `json:"accountStatus"`
|
||||||
|
|
||||||
|
AppStore `json:"appStore"`
|
||||||
|
Account
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserProfile struct {
|
||||||
|
Type string `json:"_type"`
|
||||||
|
AcceptedPolicy int `json:"acceptedPolicy"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
CurrentPolicy int `json:"currentPolicy"`
|
||||||
|
FirstName string `json:"firstName"`
|
||||||
|
LastName string `json:"lastName"`
|
||||||
|
ValidEmail bool `json:"validEmail"`
|
||||||
|
}
|
||||||
|
|
||||||
// Friend is the account data for non-primary account holders designated as friends.
|
// Friend is the account data for non-primary account holders designated as friends.
|
||||||
type Friend struct {
|
type Friend struct {
|
||||||
FirstName string `json:"firstName"`
|
FirstName string `json:"firstName"`
|
||||||
@ -252,16 +275,20 @@ type EventStreamPayload struct {
|
|||||||
To string `json:"to"`
|
To string `json:"to"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL is part of the Status message fragment returned by most calls to the Arlo API.
|
type PlaylistItem struct {
|
||||||
// URL is only populated when Success is false.
|
TZ string `json:"tz"`
|
||||||
type Data struct {
|
D string `json:"d"`
|
||||||
Message string `json:"message,omitempty"`
|
URL string `json:"url"`
|
||||||
Reason string `json:"reason,omitempty"`
|
SURL string `json:"sUrl"`
|
||||||
Error string `json:"error,omitempty"`
|
S string `json:"s"`
|
||||||
|
U int64 `json:"u"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status is the message fragment returned from most http calls to the Arlo API.
|
type CvrPlaylist struct {
|
||||||
type Status struct {
|
ToDate string `json:"toDate"`
|
||||||
Data `json:"URL,omitempty"`
|
Playlist map[string]map[string][]PlaylistItem `json:"playlist"`
|
||||||
Success bool `json:"success"`
|
UserId string `json:"userId"`
|
||||||
|
FromDate string `json:"fromDate"`
|
||||||
|
DeviceId string `json:"deviceId"`
|
||||||
|
UniqueId string `json:"uniqueId"`
|
||||||
}
|
}
|
||||||
|
43
util.go
43
util.go
@ -22,13 +22,12 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jeffreydwalter/arlo-golang/internal/request"
|
"github.com/jeffreydwalter/arlo-go/internal/request"
|
||||||
"github.com/jeffreydwalter/arlo-golang/internal/util"
|
"github.com/jeffreydwalter/arlo-go/internal/util"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -78,6 +77,7 @@ func (a *Arlo) post(uri, xCloudId string, body interface{}, header http.Header)
|
|||||||
return a.client.Post(uri, body, header)
|
return a.client.Post(uri, body, header)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
func (a *Arlo) DownloadFile(url, to string) error {
|
func (a *Arlo) DownloadFile(url, to string) error {
|
||||||
msg := fmt.Sprintf("failed to download file (%s) => (%s)", url, to)
|
msg := fmt.Sprintf("failed to download file (%s) => (%s)", url, to)
|
||||||
resp, err := a.get(url, "", nil)
|
resp, err := a.get(url, "", nil)
|
||||||
@ -99,3 +99,40 @@ func (a *Arlo) DownloadFile(url, to string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (a *Arlo) DownloadFile(url string, w io.Writer) error {
|
||||||
|
msg := fmt.Sprintf("failed to download file (%s)", url)
|
||||||
|
resp, err := a.get(url, "", nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithMessage(err, msg)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(w, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithMessage(err, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnixMicro(t time.Time) int64 {
|
||||||
|
ns := t.UnixNano()
|
||||||
|
if ns < 0 {
|
||||||
|
return (ns - 999) / 1000
|
||||||
|
}
|
||||||
|
return ns / 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnixMilli(t time.Time) int64 {
|
||||||
|
ns := t.UnixNano()
|
||||||
|
if ns < 0 {
|
||||||
|
return (ns - 999999) / 1000000
|
||||||
|
}
|
||||||
|
return ns / 1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromUnixMicro(µs int64) time.Time { return time.Unix(0, 1000*µs) }
|
||||||
|
|
||||||
|
func FromUnixMilli(ms int64) time.Time { return time.Unix(0, 1000000*ms) }
|
||||||
|
Loading…
Reference in New Issue
Block a user