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)
|
||||
> 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
|
||||
```bash
|
||||
# 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.
|
||||
```
|
||||
@ -52,7 +52,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jeffreydwalter/arlo-golang"
|
||||
"github.com/jeffreydwalter/arlo-go"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jeffreydwalter/arlo-golang/internal/request"
|
||||
"github.com/jeffreydwalter/arlo-go/internal/request"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -57,7 +59,7 @@ func Login(user string, pass string) (arlo *Arlo, err error) {
|
||||
arlo = newArlo(user, 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 {
|
||||
return nil, errors.WithMessage(err, "failed to login")
|
||||
}
|
||||
@ -91,31 +93,50 @@ func (a *Arlo) Logout() error {
|
||||
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.
|
||||
// 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) {
|
||||
resp, err := a.get(DevicesUri, "", nil)
|
||||
resp, err := a.get(fmt.Sprintf(DevicesUri, time.Now().Format("20060102")), "", nil)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to get devices")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var deviceResponse DeviceResponse
|
||||
if err := resp.Decode(&deviceResponse); err != nil {
|
||||
var response DeviceResponse
|
||||
if err := resp.Decode(&response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !deviceResponse.Success {
|
||||
if !response.Success {
|
||||
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")
|
||||
}
|
||||
|
||||
// Cache a pointer to the arlo object with each device.
|
||||
for i := range deviceResponse.Data {
|
||||
deviceResponse.Data[i].arlo = a
|
||||
for i := range response.Data {
|
||||
response.Data[i].arlo = a
|
||||
}
|
||||
|
||||
// Disconnect all of the basestations from the EventStream.
|
||||
@ -127,8 +148,8 @@ func (a *Arlo) GetDevices() (devices *Devices, err error) {
|
||||
|
||||
a.rwmutex.Lock()
|
||||
// Cache the devices as their respective types.
|
||||
a.Cameras = *deviceResponse.Data.GetCameras()
|
||||
a.Basestations = *deviceResponse.Data.GetBasestations()
|
||||
a.Cameras = *response.Data.GetCameras()
|
||||
a.Basestations = *response.Data.GetBasestations()
|
||||
a.rwmutex.Unlock()
|
||||
|
||||
// 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.
|
||||
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")
|
||||
}
|
||||
|
||||
// UpdateProfile takes a first and last name, and updates the user profile with that information.
|
||||
func (a *Arlo) UpdateProfile(firstName, lastName string) error {
|
||||
body := map[string]string{"firstName": firstName, "lastName": lastName}
|
||||
resp, err := a.put(UserProfileUri, "", body, nil)
|
||||
resp, err := a.put(ProfileUri, "", body, nil)
|
||||
return checkRequest(resp, err, "failed to update profile")
|
||||
}
|
||||
|
||||
func (a *Arlo) UpdatePassword(pass string) error {
|
||||
body := map[string]string{"currentPassword": a.pass, "newPassword": pass}
|
||||
resp, err := a.post(UserChangePasswordUri, "", body, nil)
|
||||
resp, err := a.post(UpdatePasswordUri, "", body, nil)
|
||||
if err := checkRequest(resp, err, "failed to update password"); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -167,6 +208,6 @@ func (a *Arlo) UpdatePassword(pass string) 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")
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ package arlo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@ -53,7 +54,9 @@ func (b *Basestation) makeEventStreamRequest(payload EventStreamPayload, msg str
|
||||
payload.TransId = transId
|
||||
|
||||
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)
|
||||
@ -100,7 +103,7 @@ func (b *Basestation) IsConnected() 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:
|
||||
for {
|
||||
@ -137,6 +140,11 @@ forLoop:
|
||||
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 {
|
||||
// disconnect channel to stop event stream.
|
||||
if b.eventStream != nil {
|
||||
|
305
camera.go
305
camera.go
@ -19,6 +19,7 @@ package arlo
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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.
|
||||
// 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 {
|
||||
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")
|
||||
}
|
||||
|
||||
@ -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.
|
||||
// You will need the to install a library to handle streaming of this protocol: https://pypi.python.org/pypi/python-librtmp
|
||||
//
|
||||
// The request to /users/devices/startStream returns:
|
||||
// NOTE: { "url":"rtsp://vzwow09-z2-prod.vz.netgear.com:80/vzmodulelive?egressToken=b1b4b675_ac03_4182_9844_043e02a44f71&userAgent=web&cameraId=48B4597VD8FF5_1473010750131" }
|
||||
// You will need something like ffmpeg to read the rtmps stream.
|
||||
|
||||
// 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).
|
||||
func (c *Camera) StartStream() (url string, err error) {
|
||||
payload := EventStreamPayload{
|
||||
Action: "set",
|
||||
@ -282,7 +453,7 @@ func (c *Camera) StartStream() (url string, err error) {
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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.
|
||||
func (c *Camera) TakeSnapshot() (url string, err error) {
|
||||
msg := "failed to take snapshot"
|
||||
// TakeSnapshot causes the camera to snapshot while recording.
|
||||
// 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).
|
||||
|
||||
url, err = c.StartStream()
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, msg)
|
||||
}
|
||||
// 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) {
|
||||
|
||||
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
|
||||
resp, err := c.arlo.post(DeviceTakeSnapshotUri, c.XCloudId, body, nil)
|
||||
if err := checkRequest(resp, err, "failed to update device name"); err != nil {
|
||||
return "", errors.WithMessage(err, msg)
|
||||
}
|
||||
return nil, errors.New("TakeSnapshot not implemented")
|
||||
/*
|
||||
msg := "failed to take snapshot"
|
||||
|
||||
return url, nil
|
||||
body := map[string]string{"deviceId": c.DeviceId, "parentId": c.ParentId, "xcloudId": c.XCloudId, "olsonTimeZone": c.Properties.OlsonTimeZone}
|
||||
resp, err := c.arlo.post(TakeSnapshotUri, c.XCloudId, body, nil)
|
||||
if err := checkRequest(resp, err, msg); err != nil {
|
||||
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
|
||||
*/
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -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}
|
||||
resp, err := c.arlo.post(DeviceStartRecordUri, c.XCloudId, body, nil)
|
||||
if err := checkRequest(resp, err, "failed to update device name"); err != nil {
|
||||
resp, err := c.arlo.post(StartRecordUri, c.XCloudId, body, nil)
|
||||
if err := checkRequest(resp, err, msg); err != nil {
|
||||
return "", errors.WithMessage(err, msg)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
159
const.go
159
const.go
@ -17,35 +17,136 @@
|
||||
package arlo
|
||||
|
||||
const (
|
||||
BaseUrl = "https://arlo.netgear.com/hmsweb"
|
||||
LoginUri = "/login/v2"
|
||||
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"
|
||||
|
||||
DeviceTypeArloQ = "arloq"
|
||||
DeviceTypeArloBridge = "arlobridge"
|
||||
DeviceTypeBasestation = "basestation"
|
||||
DeviceTypeCamera = "camera"
|
||||
DeviceTypeArloQ = "arloq"
|
||||
TransIdPrefix = "web"
|
||||
DeviceTypeLights = "lights"
|
||||
DeviceTypeSiren = "siren"
|
||||
|
||||
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.
|
||||
func (d *Device) UpdateDeviceName(name string) error {
|
||||
|
||||
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")
|
||||
}
|
||||
|
@ -75,10 +75,13 @@ func (e *eventStream) disconnect() {
|
||||
}
|
||||
|
||||
func (e *eventStream) listen() (connected chan bool) {
|
||||
|
||||
connected = make(chan bool)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
e.Error <- FAILED_TO_SUBSCRIBE
|
||||
@ -87,13 +90,14 @@ func (e *eventStream) listen() (connected chan bool) {
|
||||
for {
|
||||
select {
|
||||
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("DATA: %s\n", event.URL)
|
||||
fmt.Printf("DATA: %s\n", event.Data)
|
||||
*/
|
||||
|
||||
if event.Data != nil {
|
||||
if event != nil && event.Data != nil {
|
||||
notifyResponse := &EventStreamResponse{}
|
||||
b := bytes.NewBuffer(event.Data)
|
||||
err := json.NewDecoder(b).Decode(notifyResponse)
|
||||
@ -102,6 +106,7 @@ func (e *eventStream) listen() (connected chan bool) {
|
||||
break
|
||||
}
|
||||
|
||||
// FIXME: This is a shitty way to handle this. It's potentially leaking a chan.
|
||||
if notifyResponse.Status == "connected" {
|
||||
connected <- true
|
||||
} else if notifyResponse.Status == "disconnected" {
|
||||
|
@ -20,11 +20,11 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@ -61,7 +61,7 @@ func NewClient(baseURL string, baseHeaders http.Header) (*Client, error) {
|
||||
return &Client{
|
||||
BaseURL: u,
|
||||
BaseHeaders: &header,
|
||||
HttpClient: &http.Client{Jar: jar},
|
||||
HttpClient: &http.Client{Jar: jar, Timeout: 30 * time.Second},
|
||||
}, 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")
|
||||
}
|
||||
}
|
||||
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
|
||||
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) {
|
||||
|
||||
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\nCOOKIES (%s): %v\n\n", req.URL, c.HttpClient.Jar.Cookies(req.URL))
|
||||
// log.Printf("\n\nHEADERS (%s): %v\n\n", req.URL, req.Header)
|
||||
|
||||
resp, err := c.HttpClient.Do(&req.Request)
|
||||
if err != nil {
|
||||
|
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"
|
||||
|
||||
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 {
|
||||
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"
|
||||
|
||||
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 {
|
||||
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.
|
||||
*/
|
||||
func (a *Arlo) DeleteRecording(r *Recording) error {
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
func (a *Arlo) BatchDeleteRecordings(l *Library) error {
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// 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.
|
||||
type LoginResponse struct {
|
||||
Data Account
|
||||
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.
|
||||
type DeviceResponse struct {
|
||||
Data Devices
|
||||
@ -39,13 +63,22 @@ type LibraryResponse struct {
|
||||
Status
|
||||
}
|
||||
|
||||
type StreamResponse struct {
|
||||
type CvrPlaylistResponse struct {
|
||||
Data CvrPlaylist
|
||||
Status
|
||||
}
|
||||
|
||||
type Stream struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type StreamResponse struct {
|
||||
Data Stream
|
||||
Status
|
||||
}
|
||||
|
||||
type RecordingResponse struct {
|
||||
URL string `json:"url"`
|
||||
Data Stream
|
||||
Status
|
||||
}
|
||||
|
||||
|
47
types.go
47
types.go
@ -59,6 +59,29 @@ type Account struct {
|
||||
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.
|
||||
type Friend struct {
|
||||
FirstName string `json:"firstName"`
|
||||
@ -252,16 +275,20 @@ type EventStreamPayload struct {
|
||||
To string `json:"to"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
type PlaylistItem struct {
|
||||
TZ string `json:"tz"`
|
||||
D string `json:"d"`
|
||||
URL string `json:"url"`
|
||||
SURL string `json:"sUrl"`
|
||||
S string `json:"s"`
|
||||
U int64 `json:"u"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
type CvrPlaylist struct {
|
||||
ToDate string `json:"toDate"`
|
||||
Playlist map[string]map[string][]PlaylistItem `json:"playlist"`
|
||||
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/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jeffreydwalter/arlo-golang/internal/request"
|
||||
"github.com/jeffreydwalter/arlo-golang/internal/util"
|
||||
"github.com/jeffreydwalter/arlo-go/internal/request"
|
||||
"github.com/jeffreydwalter/arlo-go/internal/util"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
||||
/*
|
||||
func (a *Arlo) DownloadFile(url, to string) error {
|
||||
msg := fmt.Sprintf("failed to download file (%s) => (%s)", url, to)
|
||||
resp, err := a.get(url, "", nil)
|
||||
@ -99,3 +99,40 @@ func (a *Arlo) DownloadFile(url, to string) error {
|
||||
|
||||
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