Skip to content

Commit 7820719

Browse files
authored
Merge pull request #9 from nahum365/v020
Version 0.02.0 -- get camera stream
2 parents 0c4b4bd + 100f19d commit 7820719

File tree

3 files changed

+133
-22
lines changed

3 files changed

+133
-22
lines changed

docs/index.md

+18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Coming soon:
1212

1313
## Demo
1414

15+
### Access Devices
1516
```
1617
nda = NestDeviceAccess(
1718
project_id="PROJECT_ID",
@@ -23,3 +24,20 @@ nda.login()
2324
for device in nda.devices():
2425
print(device.name)
2526
```
27+
28+
### Access Camera Stream
29+
30+
```
31+
nda = NestDeviceAccess(
32+
project_id="PROJECT_ID",
33+
client_id="CLIENT_ID",
34+
client_secret="CLIENT_SECRET",
35+
code="OAUTH_CODE",
36+
)
37+
nda.login()
38+
for device in nda.get_devices():
39+
camera_stream = nda.get_camera_stream(device)
40+
print(camera_stream.rtsp_stream_url)
41+
print(camera_stream.stream_token)
42+
print(camera_stream.expires_at)
43+
```

nestdeviceaccess/nestdeviceaccess.py

+57-16
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
from datetime import datetime
2+
13
import requests
24
import pickle
35
import os
46

57
AUTH_URI = "https://www.googleapis.com/oauth2/v4/token"
8+
REFRESH_URI = "https://www.googleapis.com/oauth2/v4/token?client_id={client_id}" \
9+
"&client_secret={client_secret}&refresh_token={refresh_token}" \
10+
"&grant_type=refresh_token"
611
DEFAULT_BASE_URI = "https://smartdevicemanagement.googleapis.com/v1"
712
DEFAULT_REDIRECT_URI = "https://www.google.com"
813
DEVICES_URI = (
914
"https://smartdevicemanagement.googleapis.com/v1/enterprises/{project_id}/devices"
1015
)
16+
EXECUTE_COMMAND_URI = "https://smartdevicemanagement.googleapis.com/v1/enterprises/" \
17+
"{project_id}/devices/{device_id}:executeCommand"
1118

1219

1320
class ArgumentsMissingError(Exception):
@@ -18,6 +25,10 @@ class AuthorizationError(Exception):
1825
pass
1926

2027

28+
class InvalidActionError(Exception):
29+
pass
30+
31+
2132
class NestDeviceAccessAuth(requests.auth.AuthBase):
2233
def __init__(
2334
self,
@@ -34,16 +45,16 @@ def __init__(
3445
self.project_id = project_id
3546
self.client_id = client_id
3647
self.client_secret = client_secret
48+
self._res = {}
49+
self.access_token = None
50+
self.refresh_token = None
3751

3852
if not code or code == "":
3953
self.invalid_token()
4054
raise ArgumentsMissingError()
4155

4256
self.code = code
4357

44-
self._res = {}
45-
self.access_token = None
46-
self.refresh_token = None
4758
self.redirect_uri = redirect_uri
4859
self._session = session
4960

@@ -53,6 +64,7 @@ def login(self):
5364
creds = pickle.load(token)
5465
self.access_token = creds["access"]
5566
self.refresh_token = creds["refresh"]
67+
self.refresh()
5668
return
5769

5870
data = {
@@ -86,20 +98,44 @@ def __call__(self, r):
8698
return r
8799

88100
def invalid_token(self):
101+
if self.refresh_token:
102+
self.refresh()
103+
return
89104
print(
90105
f"Go to this link to get OAuth token: https://nestservices.google.com/partnerconnections/{self.project_id}"
91106
f"/auth?redirect_uri=https://www.google.com&access_type=offline&prompt=consent&client_id={self.client_id}"
92107
f"&response_type=code&scope=https://www.googleapis.com/auth/sdm.service"
93108
)
94109

110+
def refresh(self):
111+
if not self.refresh_token:
112+
self.invalid_token()
113+
return
114+
response = requests.post(REFRESH_URI.format(client_id=self.client_id,
115+
client_secret=self.client_secret,
116+
refresh_token=self.refresh_token))
117+
if response.status_code != 200:
118+
if response.status_code == 400:
119+
self.invalid_token()
120+
raise AuthorizationError(response)
121+
self.access_token = response.json()["access_token"]
122+
95123

96124
class Device(object):
97125
def __init__(self, dict):
98126
self.name = dict["name"]
127+
self.device_id = self.name.split('/')[3]
99128
self.type = dict["type"]
100129
self.traits = dict["traits"]
101130

102131

132+
class CameraStream(object):
133+
def __init__(self, dict):
134+
self.rtsp_stream_url = dict["results"]["streamUrls"]["rtspUrl"]
135+
self.stream_token = dict["results"]["streamToken"]
136+
self.expires_at = datetime.strptime(dict["results"]["expiresAt"], "%Y-%m-%dT%H:%M:%S.%fZ")
137+
138+
103139
class NestDeviceAccess(object):
104140
def __init__(
105141
self,
@@ -126,7 +162,7 @@ def login(self):
126162
print("Authorization Error")
127163
pass
128164

129-
def devices(self):
165+
def get_devices(self):
130166
if not self.auth.access_token:
131167
raise AuthorizationError()
132168

@@ -139,18 +175,23 @@ def devices(self):
139175
devices_dict = response.json()
140176

141177
devices = []
142-
for device in devices_dict["devices"]:
143-
devices.append(Device(device))
178+
for device_dict in devices_dict["devices"]:
179+
device = Device(device_dict)
180+
devices.append(device)
144181
return devices
145182

183+
def get_camera_stream(self, device):
184+
if device.type not in ["sdm.devices.types.DOORBELL", "sdm.devices.types.CAMERA"]:
185+
raise InvalidActionError()
186+
if not self.auth.access_token:
187+
raise AuthorizationError()
146188

147-
if __name__ == "__main__":
148-
nda = NestDeviceAccess(
149-
project_id="2a7ad63f-af0f-414a-b218-23dd6b39d0c5",
150-
client_id="484808906646-a9tche4b03q56u47fiojh04tbf7r56m8.apps.googleusercontent.com",
151-
client_secret="uS-rH6Fqcr_d_vsTHpZOZd6l",
152-
code="4/0AY0e-g6INJsCbfeHVxZV_Eg1jSEdWaxI22DgTfxUFTbPSBMXAEexjT_4VY9Rf1H5jht-hQ",
153-
)
154-
nda.login()
155-
for device in nda.devices():
156-
print(device.name)
189+
data = {
190+
"command": "sdm.devices.commands.CameraLiveStream.GenerateRtspStream",
191+
"params": {}
192+
}
193+
response = requests.post(EXECUTE_COMMAND_URI.format(project_id=self.project_id,
194+
device_id=device.device_id),
195+
json=data,
196+
auth=self.auth)
197+
return CameraStream(response.json())

nestdeviceaccess/test_nestdeviceaccess.py

+58-6
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import pytest
22
import nestdeviceaccess
3-
4-
5-
def callback():
6-
pass
3+
from datetime import datetime
74

85

96
def test_require_all_arguments():
107
with pytest.raises(nestdeviceaccess.ArgumentsMissingError):
118
_ = nestdeviceaccess.NestDeviceAccess(project_id="", client_secret="", client_id="", code="")
129

1310

14-
def test_url_printed_when_auth_code_not_present(capsys):
11+
def test_auth_code_not_present_prints_url(capsys):
1512
try:
1613
_ = nestdeviceaccess.NestDeviceAccess(project_id="123", client_secret="123", client_id="123", code="")
1714
except nestdeviceaccess.ArgumentsMissingError:
@@ -26,11 +23,66 @@ def test_url_printed_when_auth_code_not_present(capsys):
2623
def test_devices_call_without_login_fails():
2724
with pytest.raises(nestdeviceaccess.AuthorizationError):
2825
nda = nestdeviceaccess.NestDeviceAccess(project_id="123", client_secret="123", client_id="123", code="123")
29-
nda.devices()
26+
nda.get_devices()
3027

3128

3229
def test_failed_login_prints_error(capsys):
3330
nda = nestdeviceaccess.NestDeviceAccess(project_id="123", client_secret="123", client_id="123", code="123")
3431
nda.login()
3532
captured = capsys.readouterr()
3633
assert captured.out == "Authorization Error\n"
34+
35+
36+
def test_failed_refresh_prints_url(capsys):
37+
nda = nestdeviceaccess.NestDeviceAccess(project_id="123", client_secret="123", client_id="123", code="123")
38+
nda.auth.refresh()
39+
captured = capsys.readouterr()
40+
assert captured.out == "Go to this link to get OAuth token: " \
41+
"https://nestservices.google.com/partnerconnections/123/auth?redirect_uri=https://www" \
42+
".google.com&access_type=offline&prompt=consent&client_id=123&response_type=code&scope" \
43+
"=https://www.googleapis.com/auth/sdm.service\n"
44+
45+
46+
def test_create_camera_stream():
47+
response = {
48+
"results": {
49+
"streamUrls": {
50+
"rtspUrl": "rtsp://123.com"
51+
},
52+
"streamToken": "token",
53+
"expiresAt": "2020-01-01T00:00:01.000Z",
54+
}
55+
}
56+
57+
camera_stream = nestdeviceaccess.CameraStream(response)
58+
assert camera_stream.rtsp_stream_url == "rtsp://123.com"
59+
assert camera_stream.stream_token == "token"
60+
assert camera_stream.expires_at == datetime.strptime("2020-01-01T00:00:01.000Z", "%Y-%m-%dT%H:%M:%S.%fZ")
61+
62+
63+
def test_create_device():
64+
response = {
65+
"name": "0/1/2/device_id",
66+
"type": "type",
67+
"traits": {"trait": "trait"}
68+
}
69+
70+
device = nestdeviceaccess.Device(response)
71+
assert device.name == "0/1/2/device_id"
72+
assert device.device_id == "device_id"
73+
assert device.type == "type"
74+
assert device.traits == {"trait": "trait"}
75+
76+
77+
def test_create_camera_stream_with_non_camera():
78+
response = {
79+
"name": "0/1/2/device_id",
80+
"type": "type",
81+
"traits": {"trait": "trait"}
82+
}
83+
device = nestdeviceaccess.Device(response)
84+
85+
nda = nestdeviceaccess.NestDeviceAccess(project_id="123", client_secret="123", client_id="123", code="123")
86+
87+
with pytest.raises(nestdeviceaccess.InvalidActionError):
88+
nda.get_camera_stream(device)

0 commit comments

Comments
 (0)