From 982b21dffe707e88fcaac5a56d62b41c71371992 Mon Sep 17 00:00:00 2001 From: Al DAmico Date: Thu, 8 Dec 2022 20:18:35 -0500 Subject: [PATCH] [script.module.arlo] 1.2.62 --- script.module.arlo/addon.xml | 7 +- script.module.arlo/lib/arlo.py | 319 +++++++++++++++++++------- script.module.arlo/lib/eventstream.py | 2 +- script.module.arlo/lib/request.py | 14 ++ 4 files changed, 255 insertions(+), 87 deletions(-) diff --git a/script.module.arlo/addon.xml b/script.module.arlo/addon.xml index 3f966a796..cde35d73e 100644 --- a/script.module.arlo/addon.xml +++ b/script.module.arlo/addon.xml @@ -1,11 +1,16 @@ + + + + + diff --git a/script.module.arlo/lib/arlo.py b/script.module.arlo/lib/arlo.py index 258f0963a..34ee1ffc8 100644 --- a/script.module.arlo/lib/arlo.py +++ b/script.module.arlo/lib/arlo.py @@ -19,37 +19,42 @@ # Import helper classes that are part of this library. -import sys - try: - from request import Request - from eventstream import EventStream import Queue as queue except ImportError: - from request import Request - from eventstream import EventStream import queue as queue - + +from request import Request +from eventstream import EventStream + # Import all of the other stuff. from six import string_types, text_type from datetime import datetime +import sys import base64 import calendar import json #import logging import math import os +import pickle import random +import re import requests import signal import time +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build + #logging.basicConfig(level=logging.DEBUG,format='[%(levelname)s] (%(threadName)-10s) %(message)s',) class Arlo(object): + BASE_URL = 'my.arlo.com' + AUTH_URL = 'ocapi-app.arlo.com' TRANSID_PREFIX = 'web' - def __init__(self, username, password): + def __init__(self, username, password, google_credential_file=None): # signals only work in main thread try: @@ -60,7 +65,10 @@ def __init__(self, username, password): self.event_stream = None self.request = None - self.Login(username, password) + if google_credential_file: + self.LoginMFA(username, password, google_credential_file) + else: + self.Login(username, password) def interrupt_handler(self, signum, frame): print("Caught Ctrl-C, exiting.") @@ -127,14 +135,17 @@ def Login(self, username, password): } """ self.username = username - self.password = base64.b64encode(password.encode()).decode() - + self.password = password self.request = Request() headers = { + 'Access-Control-Request-Headers': 'content-type,source,x-user-device-id,x-user-device-name,x-user-device-type', + 'Access-Control-Request-Method': 'POST', + 'Origin': f'https://{self.BASE_URL}', + 'Referer': f'https://{self.BASE_URL}/', 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_2 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Mobile/15B202 NETGEAR/v1 (iOS Vuezone)', } - self.request.options('https://ocapi-app.arlo.com/api/auth', headers=headers) + self.request.options(f'https://{self.AUTH_URL}/api/auth', headers=headers) headers = { 'DNT': '1', @@ -142,24 +153,139 @@ def Login(self, username, password): 'Auth-Version': '2', 'Content-Type': 'application/json; charset=UTF-8', 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_2 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Mobile/15B202 NETGEAR/v1 (iOS Vuezone)', - 'Origin': 'https://my.arlo.com', - 'Referer': 'https://my.arlo.com/', + 'Origin': f'https://{self.BASE_URL}', + 'Referer': f'https://{self.BASE_URL}/', 'Source': 'arloCamWeb', } - #body = self.request.post('https://my.arlo.com/hmsweb/login/v2', {'email': self.username, 'password': self.password}, headers=headers) - body = self.request.post('https://ocapi-app.arlo.com/api/auth', {'email': self.username, 'password': self.password, 'EnvSource': 'prod', 'language': 'en'}, headers=headers) - + #body = self.request.post(f'https://{self.BASE_URL}/hmsweb/login/v2', {'email': self.username, 'password': self.password}, headers=headers) + body = self.request.post( + f'https://{self.AUTH_URL}/api/auth', + params={ + 'email': self.username, + 'password': str(base64.b64encode(self.password.encode('utf-8')), 'utf-8'), + 'language': 'en', + 'EnvSource': 'prod' + }, + headers=headers + ) headers['Authorization'] = body['token'] - + self.request.session.headers.update(headers) self.user_id = body['userId'] return body + def LoginMFA(self, username, password, google_credential_file): + self.username = username + self.password = password + self.google_credentials = pickle.load(open(google_credential_file, 'rb')) + self.request = Request() + + # request MFA token + request_start_time = int(time.time()) + + headers = { + 'DNT': '1', + 'schemaVersion': '1', + 'Auth-Version': '2', + 'Content-Type': 'application/json; charset=UTF-8', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_2 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Mobile/15B202 NETGEAR/v1 (iOS Vuezone)', + 'Origin': f'https://{self.BASE_URL}', + 'Referer': f'https://{self.BASE_URL}/', + 'Source': 'arloCamWeb', + 'TE': 'Trailers', + } + + # Authenticate + auth_body = self.request.post( + f'https://{self.AUTH_URL}/api/auth', + params={ + 'email': self.username, + 'password': str(base64.b64encode(self.password.encode('utf-8')), 'utf-8'), + 'language': 'en', + 'EnvSource': 'prod' + }, + headers=headers, + raw=True + ) + self.user_id = auth_body['data']['userId'] + self.request.session.headers.update({'Authorization': base64.b64encode(auth_body['data']['token'].encode('utf-8'))}) + + # Retrieve email factor id + factors_body = self.request.get( + f'https://{self.AUTH_URL}/api/getFactors', + params={'data': auth_body['data']['issued']}, + headers=headers, + raw=True + ) + email_factor_id = next(i for i in factors_body['data']['items'] if i['factorType'] == 'EMAIL' and i['factorRole'] == "PRIMARY")['factorId'] + if email_factor_id is None: + email_factor_id = next(i for i in factors_body['data']['items'] if i['factorType'] == 'EMAIL' and i['factorRole'] == "SECONDARY")['factorId'] + + # Start factor auth + start_auth_body = self.request.post( + f'https://{self.AUTH_URL}/api/startAuth', + {'factorId': email_factor_id}, + headers=headers, + raw=True + ) + factor_auth_code = start_auth_body['data']['factorAuthCode'] + + # search for MFA token in latest emails + pattern = r'\d{6}' + code = None + service = build('gmail', 'v1', credentials = self.google_credentials) + + for i in range(0, 10): + time.sleep(5) + messages = service.users().messages().list( + userId='me', + q=f'from:do_not_reply@arlo.com after:{request_start_time}' + ).execute() + + if messages['resultSizeEstimate'] == 0: + print('no matching emails found') + continue + + # only check the latest message + message = service.users().messages().get(userId='me', id=messages['messages'][0]['id']).execute() + search = re.search(pattern, message['snippet']) + if not search: + print('no matching code in email found') + continue + + code = search.group(0) + break + + """ + code = input("Enter MFA code:\n") + print("CODE", factor_auth_code) + """ + + # Complete auth + finish_auth_body = self.request.post( + f'https://{self.AUTH_URL}/api/finishAuth', + { + 'factorAuthCode': factor_auth_code, + 'otp': code + }, + headers=headers, + raw=True + ) + + # Update Authorization code with new code + headers = { + 'Auth-Version': '2', + 'Authorization': finish_auth_body['data']['token'].encode('utf-8'), + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_2 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Mobile/15B202 NETGEAR/v1 (iOS Vuezone)', + } + self.request.session.headers.update(headers) + self.BASE_URL = 'myapi.arlo.com' + def Logout(self): self.Unsubscribe() - return self.request.put('https://my.arlo.com/hmsweb/logout') + return self.request.put(f'https://{self.BASE_URL}/hmsweb/logout') def Subscribe(self, basestation): """ @@ -223,7 +349,7 @@ def Heartbeat(self, stop_event): def Unsubscribe(self): """ This method stops the EventStream subscription and removes it from the event_stream collection. """ if self.event_stream and self.event_stream.connected: - self.request.get('https://my.arlo.com/hmsweb/client/unsubscribe') + self.request.get(f'https://{self.BASE_URL}/hmsweb/client/unsubscribe') self.event_stream.Disconnect() self.event_stream = None @@ -274,7 +400,7 @@ def Notify(self, basestation, body): body['from'] = self.user_id+'_web' body['to'] = basestation_id - self.request.post('https://my.arlo.com/hmsweb/users/devices/notify/'+body['to'], body, headers={"xcloudId":basestation.get('xCloudId')}) + self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/notify/'+body['to'], body, headers={"xcloudId":basestation.get('xCloudId')}) return body.get('transId') def NotifyAndGetResponse(self, basestation, body, timeout=120): @@ -378,16 +504,16 @@ def GetRules(self, basestation): return self.NotifyAndGetResponse(basestation, {"action":"get","resource":"rules","publishResponse":False}) def GetSmartFeatures(self): - return self.request.get('https://my.arlo.com/hmsweb/users/subscription/smart/features') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/subscription/smart/features') def GetSmartAlerts(self, camera): - return self.request.get('https://my.arlo.com/hmsweb/users/devices/'+camera.get('uniqueId')+'/smartalerts') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/devices/'+camera.get('uniqueId')+'/smartalerts') def GetAutomationActivityZones(self, camera): - return self.request.get('https://my.arlo.com/hmsweb/users/devices/'+camera.get('uniqueId')+'/activityzones') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/devices/'+camera.get('uniqueId')+'/activityzones') def RestartBasestation(self, basestation): - return self.request.post('https://my.arlo.com/hmsweb/users/devices/restart', {"deviceId":basestation.get('deviceId')}) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/restart', {"deviceId":basestation.get('deviceId')}) def SetAutomationActivityZones(self, camera, zone, coords, color): """ @@ -398,10 +524,10 @@ def SetAutomationActivityZones(self, camera, zone, coords, color): coords: [{"x":0.37946943483275664,"y":0.3790983606557377},{"x":0.8685121107266436,"y":0.3790983606557377},{"x":0.8685121107266436,"y":1},{"x":0.37946943483275664,"y":1}] - these coordinates are the bonding box for the activity zone. color: 45136 - the color for your bounding box. """ - return self.request.post('https://my.arlo.com/hmsweb/users/devices/'+camera.get('uniqueId')+'/activityzones', {"name": zone,"coords": coords, "color": color}) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/'+camera.get('uniqueId')+'/activityzones', {"name": zone,"coords": coords, "color": color}) def GetAutomationDefinitions(self): - return self.request.get('https://my.arlo.com/hmsweb/users/automation/definitions', {'uniqueIds':'all'}) + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/automation/definitions', {'uniqueIds':'all'}) def GetCalendar(self, basestation): return self.NotifyAndGetResponse(basestation, {"action":"get","resource":"schedule","publishResponse":False}) @@ -409,12 +535,12 @@ def GetCalendar(self, basestation): def DeleteMode(self, device, mode): """ device can be any object that has parentId == deviceId. i.e., not a camera """ parentId = device.get('parentId', None) - if device['deviceType'] == 'arlobridge': - return self.request.delete('https://my.arlo.com/hmsweb/users/locations/'+device.get('uniqueId')+'/modes/'+mode) + if device.get('deviceType') == 'arlobridge': + return self.request.delete(f'https://{self.BASE_URL}/hmsweb/users/locations/'+device.get('uniqueId')+'/modes/'+mode) elif not parentId or device.get('deviceId') == parentId: return self.NotifyAndGetResponse(device, {"action":"delete","resource":"modes/"+mode,"publishResponse":True}) else: - raise Exception('Only parent device modes and schedules can be deleted.'); + raise Exception('Only parent device modes and schedules can be deleted.') def GetModes(self, basestation): """ DEPRECATED: This is the older API for getting the "mode". It still works, but GetModesV2 is the way the Arlo software does it these days. """ @@ -426,14 +552,17 @@ def GetModesV2(self): Set a non-schedule mode to be active: {"activeAutomations":[{"deviceId":"XXXXXXXXXXXXX","timestamp":1532015622105,"activeModes":["mode1"],"activeSchedules":[]}]} Set a schedule to be active: {"activeAutomations":[{"deviceId":"XXXXXXXXXXXXX","timestamp":1532015790139,"activeModes":[],"activeSchedules":["schedule.1"]}]} """ - return self.request.get('https://my.arlo.com/hmsweb/users/devices/automation/active') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/devices/automation/active') def CustomMode(self, device, mode, schedules=[]): """ device can be any object that has parentId == deviceId. i.e., not a camera """ - if(device["deviceType"].startswith("arloq")): + parentId = device.get('parentId', None) + if device.get('deviceType') == 'arlobridge': + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/automation/active', {'activeAutomations':[{'deviceId':device.get('deviceId'),'timestamp':self.to_timestamp(datetime.now()),'activeModes':[mode],'activeSchedules':schedules}]}) + elif not parentId or device.get('deviceId') == parentId: return self.NotifyAndGetResponse(device, {"from":self.user_id+"_web", "to": device.get("parentId"), "action":"set","resource":"modes", "transId": self.genTransId(),"publishResponse":True,"properties":{"active":mode}}) else: - return self.request.post('https://my.arlo.com/hmsweb/users/devices/automation/active', {'activeAutomations':[{'deviceId':device.get('deviceId'),'timestamp':self.to_timestamp(datetime.now()),'activeModes':[mode],'activeSchedules':schedules}]}) + raise Exception('Only parent device modes and schedules can be modified.') def Arm(self, device): return self.CustomMode(device, "mode1") @@ -546,7 +675,7 @@ def SetSchedule(self, basestation, schedule): "enabled": true } """ - return self.request.post('https://my.arlo.com/hmsweb/users/locations/'+basestation.get('uniqueId')+'/schedules', ) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/locations/'+basestation.get('uniqueId')+'/schedules', ) def AdjustBrightness(self, basestation, camera, brightness=0): """ @@ -575,7 +704,7 @@ def ToggleCamera(self, basestation, camera, active=True): return self.NotifyAndGetResponse(basestation, {"action":"set","resource":"cameras/"+camera.get('deviceId'),"publishResponse":True,"properties":{"privacyActive":active}}) def PushToTalk(self, camera): - return self.request.get('https://my.arlo.com/hmsweb/users/devices/'+camera.get('uniqueId')+'/pushtotalk') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/devices/'+camera.get('uniqueId')+'/pushtotalk') """ General alert toggles """ def SetMotionAlertsOn(self, basestation, sensitivity=5): @@ -609,7 +738,7 @@ def PauseTrack(self, basestation): def UnPauseTrack(self, basestation): return self.Notify(basestation, {"action":"play","resource":"audioPlayback/player"}) - + def SkipTrack(self, basestation): return self.Notify(basestation, {"action":"nextTrack","resource":"audioPlayback/player"}) @@ -719,7 +848,7 @@ def SetTempRecordingOff(self, basestation): return self.NotifyAndGetResponse(basestation, {"action":"set","resource":"cameras/"+basestation.get('deviceId')+"/ambientSensors/config","publishResponse":True,"properties":{"temperature":{"recordingEnabled":False}}}) def SetTempUnit(self, uniqueId, unit="C"): - return self.request.post('https://my.arlo.com/hmsweb/users/devices/'+uniqueId+'/tempUnit', {"tempUnit":unit}) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/'+uniqueId+'/tempUnit', {"tempUnit":unit}) def SirenOn(self, basestation): return self.NotifyAndGetResponse(basestation, {"action":"set","resource":"siren","publishResponse":True,"properties":{"sirenState":"on","duration":300,"volume":8,"pattern":"alarm"}}) @@ -728,51 +857,51 @@ def SirenOff(self, basestation): return self.NotifyAndGetResponse(basestation, {"action":"set","resource":"siren","publishResponse":True,"properties":{"sirenState":"off","duration":300,"volume":8,"pattern":"alarm"}}) def Reset(self): - return self.request.get('https://my.arlo.com/hmsweb/users/library/reset') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/library/reset') def GetServiceLevelSettings(self): - return self.request.get('https://my.arlo.com/hmsweb/users/serviceLevel/settings') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/serviceLevel/settings') def GetServiceLevel(self): - return self.request.get('https://my.arlo.com/hmsweb/users/serviceLevel') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/serviceLevel') def GetServiceLevelV2(self): """ DEPRECATED: This API still works, but I don't see it being called in the web UI anymore. """ - return self.request.get('https://my.arlo.com/hmsweb/users/serviceLevel/v2') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/serviceLevel/v2') def GetServiceLevelV3(self): """ DEPRECATED: This API still works, but I don't see it being called in the web UI anymore. """ - return self.request.get('https://my.arlo.com/hmsweb/users/serviceLevel/v3') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/serviceLevel/v3') def GetServiceLevelV4(self): - return self.request.get('https://my.arlo.com/hmsweb/users/serviceLevel/v4') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/serviceLevel/v4') def GetUpdateFeatures(self): - return self.request.get('https://my.arlo.com/hmsweb/users/devices/updateFeatures/feature') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/devices/updateFeatures/feature') def GetPaymentBilling(self): - return self.request.get('https://my.arlo.com/hmsweb/users/payment/billing/'+self.user_id) + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/payment/billing/'+self.user_id) def GetPaymentOffers(self): """ DEPRECATED: This API still works, but I don't see it being called in the web UI anymore. """ - return self.request.get('https://my.arlo.com/hmsweb/users/payment/offers') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/payment/offers') def GetPaymentOffersV2(self): """ DEPRECATED: This API still works, but I don't see it being called in the web UI anymore. """ - return self.request.get('https://my.arlo.com/hmsweb/users/payment/offers/v2') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/payment/offers/v2') def GetPaymentOffersV3(self): """ DEPRECATED: This API still works, but I don't see it being called in the web UI anymore. """ - return self.request.get('https://my.arlo.com/hmsweb/users/payment/offers/v3') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/payment/offers/v3') def GetPaymentOffersV4(self): - return self.request.get('https://my.arlo.com/hmsweb/users/payment/offers/v4') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/payment/offers/v4') def SetOCProfile(self, firstName, lastName, country='United States', language='en', spam_me=0): - return self.request.post('https://my.arlo.com/hmsweb/users/ocprofile', {"firstName":"Jeffrey","lastName":"Walter","country":country,"language":language,"mailProgram":spam_me}) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/ocprofile', {"firstName":"Jeffrey","lastName":"Walter","country":country,"language":language,"mailProgram":spam_me}) def GetOCProfile(self): - return self.request.get('https://my.arlo.com/hmsweb/users/ocprofile') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/ocprofile') def GetProfile(self): """ @@ -791,7 +920,7 @@ def GetProfile(self): "success": true } """ - return self.request.get('https://my.arlo.com/hmsweb/users/profile') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/profile') def GetAccount(self): """ @@ -842,7 +971,7 @@ def GetAccount(self): "success": true } """ - return self.request.get('https://my.arlo.com/hmsweb/users/account') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/account') def GetSession(self): """ @@ -862,10 +991,30 @@ def GetSession(self): "dateCreated": 1463975008658 } """ - return self.request.get('https://my.arlo.com/hmsweb/users/session') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/session') + + def GetSessionV2(self): + """ + Returns something like the following: + { + "userId": "XXX-XXXXXXX", + "email": "jeffreydwalter@gmail.com", + "token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "paymentId": "XXXXXXXX", + "accountStatus": "registered", + "serialNumber": "XXXXXXXXXXXXXX", + "countryCode": "US", + "tocUpdate": false, + "policyUpdate": false, + "validEmail": true, + "arlo": true, + "dateCreated": 1463975008658 + } + """ + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/session/v2') def GetFriends(self): - return self.request.get('https://my.arlo.com/hmsweb/users/friends') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/friends') def GetLocations(self): """ @@ -893,10 +1042,10 @@ def GetLocations(self): ] } """ - return self.request.get('https://my.arlo.com/hmsweb/users/locations') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/locations') def GetEmergencyLocations(self): - return self.request.get('https://my.arlo.com/hmsweb/users/emergency/locations') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/emergency/locations') def Geofencing(self, location_id, active=True): """ @@ -904,29 +1053,29 @@ def Geofencing(self, location_id, active=True): NOTE: The Arlo API seems to disable geofencing mode when switching to other modes, if it's enabled. You should probably do the same, although, the UI reflects the switch from calendar mode to say armed mode without explicitly setting calendar mode to inactive. """ - return self.request.put('https://my.arlo.com/hmsweb/users/locations/'+location_id, {'geoEnabled':active}) + return self.request.put(f'https://{self.BASE_URL}/hmsweb/users/locations/'+location_id, {'geoEnabled':active}) def GetDevice(self, device_name): def is_device(device): - return device['deviceName'] == device_name + return device.get('deviceName') == device_name return list(filter(is_device, self.GetDevices()))[0] def GetDevices(self, device_type=None, filter_provisioned=None): """ This method returns an array that contains the basestation, cameras, etc. and their metadata. If you pass in a valid device type, as a string or a list, this method will return an array of just those devices that match that type. An example would be ['basestation', 'camera'] - To filter provisioned or unprovisioned devices pass in a True/False value for filter_provisioned. By default both types are returned. + To filter provisioned or unprovisioned devices pass in a True/False value for filter_provisioned. By default both types are returned. """ - devices = self.request.get('https://my.arlo.com/hmsweb/users/devices') + devices = self.request.get(f'https://{self.BASE_URL}/hmsweb/users/devices') if device_type: - devices = [ device for device in devices if device['deviceType'] in device_type] + devices = [ device for device in devices if device.get('deviceType') in device_type] if filter_provisioned is not None: if filter_provisioned: devices = [ device for device in devices if device.get("state") == 'provisioned'] else: devices = [ device for device in devices if device.get("state") != 'provisioned'] - + return devices def GetDeviceSupport(self): @@ -975,7 +1124,7 @@ def GetDeviceSupport(self): ] } """ - return self.request.get('https://my.arlo.com/hmsweb/devicesupport') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/devicesupport') def GetDeviceSupportv2(self): """ @@ -1164,7 +1313,7 @@ def GetDeviceSupportv2(self): "baseUrl": "https://vzs3-prod-common.s3.amazonaws.com/static/v2/html/en/" } """ - return self.request.get('https://my.arlo.com/hmsweb/devicesupport/v2') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/devicesupport/v2') def GetDeviceSupportV3(self): """ @@ -1327,20 +1476,20 @@ def GetDeviceSupportV3(self): "success":true } """ - return self.request.get('https://my.arlo.com/hmsweb/devicesupport/v3') + return self.request.get(f'https://{self.BASE_URL}/hmsweb/devicesupport/v3') def GetDeviceCapabilities(self, device): model = device.get('modelId').lower() - return self.request.get('https://my.arlo.com/resources/capabilities/'+model+'/'+model+'_'+device.get('interfaceVersion')+'.json', raw=True) + return self.request.get(f'https://{self.BASE_URL}/resources/capabilities/'+model+'/'+model+'_'+device.get('interfaceVersion')+'.json', raw=True) def GetLibraryMetaData(self, from_date, to_date): - return self.request.post('https://my.arlo.com/hmsweb/users/library/metadata', {'dateFrom':from_date, 'dateTo':to_date}) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/library/metadata', {'dateFrom':from_date, 'dateTo':to_date}) def UpdateProfile(self, first_name, last_name): - return self.request.put('https://my.arlo.com/hmsweb/users/profile', {'firstName': first_name, 'lastName': last_name}) + return self.request.put(f'https://{self.BASE_URL}/hmsweb/users/profile', {'firstName': first_name, 'lastName': last_name}) def UpdatePassword(self, password): - r = self.request.post('https://my.arlo.com/hmsweb/users/changePassword', {'currentPassword':self.password,'newPassword':password}) + r = self.request.post(f'https://{self.BASE_URL}/hmsweb/users/changePassword', {'currentPassword':self.password,'newPassword':password}) self.password = password return r @@ -1361,7 +1510,7 @@ def UpdateFriend(self, body): "id":"XXX-XXXXXXX" } """ - return self.request.put('https://my.arlo.com/hmsweb/users/friends', body) + return self.request.put(f'https://{self.BASE_URL}/hmsweb/users/friends', body) def RemoveFriend(self, email): """ @@ -1369,7 +1518,7 @@ def RemoveFriend(self, email): email: email of user you want to revoke access from. """ - return self.request.post('https://my.arlo.com/hmsweb/users/friends/remove', {"email":email}) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/friends/remove', {"email":email}) def AddFriend(self, firstname, lastname, email, devices={}, admin=False): """ @@ -1378,17 +1527,17 @@ def AddFriend(self, firstname, lastname, email, devices={}, admin=False): {adminUser:false,firstName:John,lastName:Doe,email:john.doe@example.com,devices:{XXX-XXXXXXX_XXXXXXXXXXXX:Camera1,XXX-XXXXXXX_XXXXXXXXXXXX:Camera2}} """ - return self.request.post('https://my.arlo.com/hmsweb/users/friends', {"adminUser":admin,"firstName":firstname,"lastName":lastname,"email":email,"devices":devices}) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/friends', {"adminUser":admin,"firstName":firstname,"lastName":lastname,"email":email,"devices":devices}) def ResendFriendInvite(self, friend): """ This API will resend an invitation email to a user that you've AddFriend'd. You will need to get the friend object by calling GetFriend() because it includes a token that must be passed to this API. friend: {"ownerId":"XXX-XXXXXXX","token":"really long string that you get from the GetFriends() API","firstName":"John","lastName":"Doe","devices":{"XXX-XXXXXXX_XXXXXXXXXXXX":"Camera1","XXX-XXXXXXX_XXXXXXXXXXXX":"Camera2"},"lastModified":1548470485419,"adminUser":false,"email":"john.doe@example.com"} """ - return self.request.post('https://my.arlo.com/hmsweb/users/friends', friend) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/friends', friend) def UpdateDeviceName(self, device, name): - return self.request.put('https://my.arlo.com/hmsweb/users/devices/renameDevice', {'deviceId':device.get('deviceId'), 'deviceName':name, 'parentId':device.get('parentId')}) + return self.request.put(f'https://{self.BASE_URL}/hmsweb/users/devices/renameDevice', {'deviceId':device.get('deviceId'), 'deviceName':name, 'parentId':device.get('parentId')}) def UpdateDisplayOrder(self, body): """ @@ -1403,7 +1552,7 @@ def UpdateDisplayOrder(self, body): } } """ - return self.request.post('https://my.arlo.com/hmsweb/users/devices/displayOrder', body) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/displayOrder', body) def GetLibrary(self, from_date, to_date): """ @@ -1432,14 +1581,14 @@ def GetLibrary(self, from_date, to_date): } ] """ - return self.request.post('https://my.arlo.com/hmsweb/users/library', {'dateFrom':from_date, 'dateTo':to_date}) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/library', {'dateFrom':from_date, 'dateTo':to_date}) def DeleteRecording(self, recording): """ Delete a single video recording from Arlo. All of the date info and device id you need to pass into this method are given in the results of the GetLibrary() call. """ - return self.request.post('https://my.arlo.com/hmsweb/users/library/recycle', {'data':[{'createdDate':recording.get('createdDate'),'utcCreatedDate':recording.get('createdDate'),'deviceId':recording.get('deviceId')}]}) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/library/recycle', {'data':[{'createdDate':recording.get('createdDate'),'utcCreatedDate':recording.get('createdDate'),'deviceId':recording.get('deviceId')}]}) def BatchDeleteRecordings(self, recordings): """ @@ -1462,7 +1611,7 @@ def BatchDeleteRecordings(self, recordings): ] """ if recordings: - return self.request.post('https://my.arlo.com/hmsweb/users/library/recycle', {'data':recordings}) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/library/recycle', {'data':recordings}) def GetRecording(self, url, chunk_size=4096): """ Returns the whole video from the presignedContentUrl. """ @@ -1523,7 +1672,7 @@ class nl: stream_url_dict = None def trigger(self): - nl.stream_url_dict = self.request.post('https://my.arlo.com/hmsweb/users/devices/startStream', {"to":camera.get('parentId'),"from":self.user_id+"_web","resource":"cameras/"+camera.get('deviceId'),"action":"set","responseUrl":"", "publishResponse":True,"transId":self.genTransId(),"properties":{"activityState":"startUserStream","cameraId":camera.get('deviceId')}}, headers={"xcloudId":camera.get('xCloudId')}) + nl.stream_url_dict = self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/startStream', {"to":camera.get('parentId'),"from":self.user_id+"_web","resource":"cameras/"+camera.get('deviceId'),"action":"set","responseUrl":"", "publishResponse":True,"transId":self.genTransId(),"properties":{"activityState":"startUserStream","cameraId":camera.get('deviceId')}}, headers={"xcloudId":camera.get('xCloudId')}) def callback(self, event): if event.get("from") == basestation.get("deviceId") and event.get("resource") == "cameras/"+camera.get("deviceId") and event.get("properties", {}).get("activityState") == "userStreamActive": @@ -1540,7 +1689,7 @@ class nl: stream_url_dict = None def trigger(self): - self.request.post('https://my.arlo.com/hmsweb/users/devices/stopStream', {"to":camera.get('parentId'),"from":self.user_id+"_web","resource":"cameras/"+camera. get('deviceId'),"action":"set","responseUrl":"", "publishResponse":True,"transId":self.genTransId(),"properties":{"activityState":"stopUserStream","cameraId":camera.get('deviceId')}}, headers={"xcloudId": camera.get('xCloudId')}) + self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/stopStream', {"to":camera.get('parentId'),"from":self.user_id+"_web","resource":"cameras/"+camera. get('deviceId'),"action":"set","responseUrl":"", "publishResponse":True,"transId":self.genTransId(),"properties":{"activityState":"stopUserStream","cameraId":camera.get('deviceId')}}, headers={"xcloudId": camera.get('xCloudId')}) def callback(self, event): if event.get("from") == basestation.get("deviceId") and event.get("resource") == "cameras/"+camera.get("deviceId") and event.get("properties", {}).get("activityState") == "userStreamActive": @@ -1562,7 +1711,7 @@ def TriggerStreamSnapshot(self, basestation, camera): NOTE: Use DownloadSnapshot() to download the actual image file. """ def trigger(self): - self.request.post('https://my.arlo.com/hmsweb/users/devices/takeSnapshot', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')}) + self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/takeSnapshot', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')}) def callback(self, event): if event.get("deviceId") == camera.get("deviceId") and event.get("resource") == "mediaUploadNotification": @@ -1596,7 +1745,7 @@ def StartRecording(self, basestation, camera): You can get the timezone from GetDevices(). """ stream_url = self.StartStream(basestation, camera) - self.request.post('https://my.arlo.com/hmsweb/users/devices/startRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')}) + self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/startRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')}) return stream_url def StopRecording(self, camera): @@ -1604,8 +1753,8 @@ def StopRecording(self, camera): This function causes the camera to stop recording. You can get the timezone from GetDevices(). """ - return self.request.post('https://my.arlo.com/hmsweb/users/devices/stopRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')}) + return self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/stopRecord', {'xcloudId':camera.get('xCloudId'),'parentId':camera.get('parentId'),'deviceId':camera.get('deviceId'),'olsonTimeZone':camera.get('properties', {}).get('olsonTimeZone')}, headers={"xcloudId":camera.get('xCloudId')}) def GetCvrPlaylist(self, camera, fromDate, toDate): """ This function downloads a Cvr Playlist file for the period fromDate to toDate. """ - return self.request.get('https://my.arlo.com/hmsweb/users/devices/'+camera.get('deviceId')+'/playlist?fromDate='+fromDate+'&toDate='+toDate) + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/devices/'+camera.get('uniqueId')+'/playlist?fromDate='+fromDate+'&toDate='+toDate) diff --git a/script.module.arlo/lib/eventstream.py b/script.module.arlo/lib/eventstream.py index 1504c043b..fe06c21d9 100644 --- a/script.module.arlo/lib/eventstream.py +++ b/script.module.arlo/lib/eventstream.py @@ -72,7 +72,7 @@ def Get(self, block=True, timeout=None): def Start(self): try: - event_stream = sseclient.SSEClient('https://my.arlo.com/hmsweb/client/subscribe?token='+self.arlo.request.session.headers.get('Authorization'), session=self.arlo.request.session) + event_stream = sseclient.SSEClient('https://myapi.arlo.com/hmsweb/client/subscribe?token='+self.arlo.request.session.headers.get('Authorization').decode(), session=self.arlo.request.session) self.event_stream_thread = threading.Thread(name="EventStream", target=self.event_handler, args=(self.arlo, event_stream, self.event_stream_stop_event, )) self.event_stream_thread.setDaemon(True) self.event_stream_thread.start() diff --git a/script.module.arlo/lib/request.py b/script.module.arlo/lib/request.py index b30aff906..f80afa347 100644 --- a/script.module.arlo/lib/request.py +++ b/script.module.arlo/lib/request.py @@ -29,7 +29,21 @@ def __init__(self): self.session = requests.Session() def _request(self, url, method='GET', params={}, headers={}, stream=False, raw=False): + + ## uncomment for debug logging + """ + import logging + import http.client + http.client.HTTPConnection.debuglevel = 1 + logging.basicConfig() + logging.getLogger().setLevel(logging.DEBUG) + req_log = logging.getLogger('requests.packages.urllib3') + req_log.setLevel(logging.DEBUG) + req_log.propagate = True + """ + if method == 'GET': + #print('COOKIES: ', self.session.cookies.get_dict()) r = self.session.get(url, params=params, headers=headers, stream=stream) r.raise_for_status() if stream is True: