arlo-go/camera.go

600 lines
21 KiB
Go
Raw Normal View History

2018-09-22 19:22:42 +00:00
/*
* 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
import (
"fmt"
"strings"
"time"
"github.com/pkg/errors"
)
// A Camera is a Device of type "camera".
// This type is here just for semantics. Some methods explicitly require a device of a certain type.
type Camera Device
2018-09-22 19:22:42 +00:00
// Cameras is a slice of Camera objects.
type Cameras []Camera
// Find returns a camera with the device id passed in.
func (cs *Cameras) Find(deviceId string) *Camera {
for _, c := range *cs {
if c.DeviceId == deviceId {
return &c
}
}
return nil
}
// On turns a camera on; meaning it will detect and record events.
func (c *Camera) On() (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: CameraProperties{
PrivacyActive: false,
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to turn camera on"
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)
}
// On turns a camera off; meaning it won't detect and record events.
func (c *Camera) Off() (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: CameraProperties{
PrivacyActive: true,
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to turn camera off"
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)
}
// SetBrightness sets the camera brightness.
// NOTE: Brightness is between -2 and 2 in increments of 1 (-2, -1, 0, 1, 2).
// Setting it to an invalid value has no effect.
func (c *Camera) SetBrightness(brightness int) (response *EventStreamResponse, err error) {
// Sanity check; if the values are above or below the allowed limits, set them to their limit.
if brightness < -2 {
brightness = -2
} else if brightness > 2 {
brightness = 2
}
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: CameraProperties{
Brightness: brightness,
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to set camera brightness"
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)
}
func (c *Camera) EnableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: MotionDetectionProperties{
BaseDetectionProperties: BaseDetectionProperties{
Armed: true,
Sensitivity: sensitivity,
Zones: zones,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to enable motion alerts"
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)
}
func (c *Camera) DisableMotionAlerts(sensitivity int, zones []string) (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: MotionDetectionProperties{
BaseDetectionProperties: BaseDetectionProperties{
Armed: false,
Sensitivity: sensitivity,
Zones: zones,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to enable motion alerts"
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)
}
func (c *Camera) EnableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: AudioDetectionProperties{
BaseDetectionProperties: BaseDetectionProperties{
Armed: true,
Sensitivity: sensitivity,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to enable audio alerts"
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)
}
func (c *Camera) DisableAudioAlerts(sensitivity int) (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: AudioDetectionProperties{
BaseDetectionProperties: BaseDetectionProperties{
Armed: false,
Sensitivity: sensitivity,
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to disable audio alerts"
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)
}
// PushToTalk starts a push-to-talk session.
// 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 {
/*
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")
}
// action: disabled OR recordSnapshot OR recordVideo
func (c *Camera) SetAlertNotificationMethods(action string, email, push bool) (response *EventStreamResponse, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: EventActionProperties{
BaseEventActionProperties: BaseEventActionProperties{
ActionType: action,
StopType: "timeout",
Timeout: 15,
EmailNotification: EmailNotification{
Enabled: email,
EmailList: []string{"__OWNER_EMAIL__"},
PushNotification: push,
},
},
},
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to set alert notification methods"
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)
}
2018-09-22 19:22:42 +00:00
// StartStream returns a json object containing the rtmps url to the requested video stream.
// 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).
2018-09-22 19:22:42 +00:00
func (c *Camera) StartStream() (url string, err error) {
payload := EventStreamPayload{
Action: "set",
Resource: fmt.Sprintf("cameras/%s", c.DeviceId),
PublishResponse: true,
Properties: map[string]string{
"activityState": "startUserStream",
"cameraId": c.DeviceId,
},
TransId: genTransId(),
From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix),
To: c.ParentId,
}
msg := "failed to start stream"
resp, err := c.arlo.post(StartStreamUri, c.XCloudId, payload, nil)
2018-09-22 19:22:42 +00:00
if err != nil {
return "", errors.WithMessage(err, msg)
}
defer resp.Body.Close()
response := new(StreamResponse)
if err := resp.Decode(response); err != nil {
return "", err
}
if !response.Success {
return "", errors.WithMessage(errors.New("status was false"), msg)
}
response.Data.URL = strings.Replace(response.Data.URL, "rtsp://", "rtsps://", 1)
2018-09-22 19:22:42 +00:00
return response.Data.URL, nil
2018-09-22 19:22:42 +00:00
}
// 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).
2018-09-22 19:22:42 +00:00
// 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"
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
*/
}
2018-09-22 19:22:42 +00:00
// 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,
}
2018-09-22 19:22:42 +00:00
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
*/
2018-09-22 19:22:42 +00:00
}
// StartRecording causes the camera to start recording and returns a url that you must start reading from using ffmpeg
// or something similar.
func (c *Camera) StartRecording() (url string, err error) {
msg := "failed to start recording"
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}
resp, err := c.arlo.post(StartRecordUri, c.XCloudId, body, nil)
if err := checkRequest(resp, err, msg); err != nil {
2018-09-22 19:22:42 +00:00
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
}