diff --git a/owlet_api/cli.py b/owlet_api/cli.py index 8e7f242..41621b6 100644 --- a/owlet_api/cli.py +++ b/owlet_api/cli.py @@ -20,7 +20,9 @@ def cli(): parser.add_argument('password', help='Specify Password') parser.add_argument('actions', help='Specify the actions', nargs='+', choices=["token", "devices", "attributes", - "stream", 'download']) + "stream", 'dump', 'download']) + parser.add_argument('--dbname', dest='dbname', + help='Specify filename for dump. (dump saves all data to a sqlite database.)') parser.add_argument('--device', dest='device', help='Specify DSN for device filter') parser.add_argument('--stream', dest='attributes', action='append', @@ -46,7 +48,7 @@ def cli(): try: api.login() except OwletPermanentCommunicationException: - print("Login failed, username or passwort might be wrong") + print("Login failed, username or password might be wrong") sys.exit(1) except OwletTemporaryCommunicationException: print("Login failed, server might be down") @@ -74,6 +76,12 @@ def cli(): (myproperty.name, myproperty.display_name, myproperty.last_update, myproperty.value)) + if "dump" in args.actions: + if args.dbname is None: + api.save_everything_to_db() + else: + api.save_everything_to_db(args.dbname) + if "download" in args.actions: for device in api.get_devices(): if args.device is None or args.device == device.dsn: diff --git a/owlet_api/owlet.py b/owlet_api/owlet.py index 8393161..f29cf78 100644 --- a/owlet_api/owlet.py +++ b/owlet_api/owlet.py @@ -1,6 +1,7 @@ #!/usr/bin/env python """Contains Class Owlet.""" +import json from json.decoder import JSONDecodeError import requests from requests.exceptions import RequestException @@ -15,18 +16,30 @@ class Owlet(): # pylint: disable=R0902 def __init__(self, api, json): """Initialize Owlet with API reference and json object.""" - self.product_name = json['product_name'] - self.model = json['model'] - self.dsn = json['dsn'] - self.sw_version = json['sw_version'] - self.mac = json['mac'] - self.hwsig = json['hwsig'] - self.lan_ip = json['lan_ip'] - self.connected_at = json['connected_at'] - self.connection_status = json['connection_status'] - self.lat = float(json['lat']) - self.lon = float(json['lng']) - self.device_type = json['device_type'] + self.product_name = json['product_name'] if 'product_name' in json else '' + self.product_class = json['product_class'] if 'product_class' in json else '' + self.model = json['model'] if 'model' in json else '' + self.dsn = json['dsn'] if 'dsn' in json else '' + self.sw_version = json['sw_version'] if 'sw_version' in json else '' + self.mac = json['mac'] if 'mac' in json else '' + self.hwsig = json['hwsig'] if 'hwsig' in json else '' + self.lan_ip = json['lan_ip'] if 'lan_ip' in json else '' + self.connected_at = json['connected_at'] if 'connected_at' in json else '' + self.connection_priority = json['connection_priority'] if 'connection_priority' in json else '' + self.connection_status = json['connection_status'] if 'connection_status' in json else '' + self.dealer = json['dealer'] if 'dealer' in json else '' + self.lat = float(json['lat']) if 'lat' in json else '' + self.lon = float(json['lng']) if 'lng' in json else '' + self.locality = json['locality'] if 'locality' in json else '' + self.device_type = json['device_type'] if 'device_type' in json else '' + self.has_properties = json['has_properties'] if 'has_properties' in json else '' + self.key = json['key'] if 'key' in json else '' + self.lan_enabled = json['lan_enabled'] if 'lan_enabled' in json else '' + self.manuf_model = json['manuf_model'] if 'manuf_model' in json else '' + self.oem_model = json['oem_model'] if 'oem_model' in json else '' + self.model = json['model'] if 'model' in json else '' + self.template_id = json['template_id'] if 'template_id' in json else '' + self.unique_hardware_id = json['unique_hardware_id'] if 'unique_hardware_id' in json else '' self.properties = {} self.update_interval = 10 self.owlet_api = api @@ -99,18 +112,250 @@ def update(self): 'Server Request failed - status code') try: - json = result.json() + json_data = result.json() except JSONDecodeError: raise OwletTemporaryCommunicationException( 'Update failed - JSON error') - for myproperty in json: + for myproperty in json_data: property_name = myproperty['property']['name'] + temp_key = myproperty['property']['key'] + if property_name in self.properties: self.properties[property_name].update(myproperty['property']) else: new_property = OwletProperty(myproperty['property']) self.properties[new_property.name] = new_property + + if property_name == 'REAL_TIME_VITALS': + # Convert Dream Sock Data to Smart Sock 3 Format + vitals = json.loads(myproperty['property']['value']) + + # OXYGEN_LEVEL = ox + new_property = OwletProperty(myproperty['property']) + new_property.name = 'OXYGEN_LEVEL' + new_property.display_name = 'Oxygen Level' + new_property.value = vitals['ox'] if 'ox' in vitals else '' + new_property.key = temp_key + 0.01 + new_property.expanded = True + self.properties[new_property.name] = new_property + + #HEART_RATE = hr + new_property = OwletProperty(myproperty['property']) + new_property.name = 'HEART_RATE' + new_property.display_name = 'Heart Rate' + new_property.value = vitals['hr'] if 'hr' in vitals else '' + new_property.key = temp_key + 0.02 + new_property.expanded = True + self.properties[new_property.name] = new_property + + #MOVEMENT = mv + new_property = OwletProperty(myproperty['property']) + new_property.name = 'MOVEMENT' + new_property.display_name = 'Baby Movement' + new_property.value = vitals['mv'] if 'mv' in vitals else '' + new_property.key = temp_key + 0.03 + new_property.expanded = True + self.properties[new_property.name] = new_property + + # SOCK_CONNECTION = sc + new_property = OwletProperty(myproperty['property']) + new_property.name = 'SOCK_CONNECTION' + new_property.display_name = 'Sock Connection' + new_property.value = vitals['sc'] if 'sc' in vitals else '' + new_property.key = temp_key + 0.04 + new_property.expanded = True + self.properties[new_property.name] = new_property + + """ + # ??? = st + new_property = OwletProperty(myproperty['property']) + new_property.name = '???' + new_property.display_name = '???' + new_property.value = vitals['st'] if 'st' in vitals else '' + new_property.key = temp_key + 0.05 + new_property.expanded = True + self.properties[new_property.name] = new_property + """ + + # BASE_STAT_ON = bso + new_property = OwletProperty(myproperty['property']) + new_property.name = 'BASE_STAT_ON' + new_property.display_name = 'Base Station On' + new_property.value = vitals['bso'] if 'bso' in vitals else '' + new_property.key = temp_key + 0.06 + new_property.expanded = True + self.properties[new_property.name] = new_property + + #BATT_LEVEL = bat + new_property = OwletProperty(myproperty['property']) + new_property.name = 'BATT_LEVEL' + new_property.display_name = 'Battery Level (%)' + new_property.value = vitals['bat'] if 'bat' in vitals else '' + new_property.key = temp_key + 0.07 + new_property.expanded = True + self.properties[new_property.name] = new_property + + # BAT_TIME = btt + new_property = OwletProperty(myproperty['property']) + new_property.name = 'BAT_TIME' + new_property.display_name = 'Sock Battery (Minutes)' + new_property.value = vitals['btt'] if 'btt' in vitals else '' + new_property.key = temp_key + 0.08 + new_property.expanded = True + self.properties[new_property.name] = new_property + + # CHARGE_STATUS = chg + # 1 = Charged + # 2 = Charging + new_property = OwletProperty(myproperty['property']) + new_property.name = 'CHARGE_STATUS' + new_property.display_name = 'Charge Status' + new_property.value = vitals['chg'] if 'chg' in vitals else '' + new_property.key = temp_key + 0.09 + new_property.expanded = True + self.properties[new_property.name] = new_property + + # ALRTS_DISABLED = aps + new_property = OwletProperty(myproperty['property']) + new_property.name = 'ALRTS_DISABLED' + new_property.display_name = 'Disable Alerts' + new_property.value = vitals['aps'] if 'aps' in vitals else '' + new_property.key = temp_key + 0.10 + new_property.expanded = True + self.properties[new_property.name] = new_property + + # ALERT = alrt + # 16 = Disconnected + # 32 & 64 = Placement + new_property = OwletProperty(myproperty['property']) + new_property.name = 'ALERT' + new_property.display_name = 'Alert Status' + new_property.value = vitals['alrt'] if 'alrt' in vitals else '' + new_property.key = temp_key + 0.11 + new_property.expanded = True + self.properties[new_property.name] = new_property + + # OTA_STATUS = ota + # 0 = None + # 1 = Firmware being sent + # 2 = Waiting for sock to be plugged in + # 3 = Installing + # 4 = Installing Critical + # 5 = Unknown + new_property = OwletProperty(myproperty['property']) + new_property.name = 'OTA_STATUS' + new_property.display_name = 'OTA Status' + new_property.value = vitals['ota'] if 'ota' in vitals else '' + new_property.key = temp_key + 0.12 + new_property.expanded = True + self.properties[new_property.name] = new_property + + # SOCK_STATUS = srf + # 1 = Checking On + # 2 (When sc also = 2) = Kicking + # 3 = Recently Placed + new_property = OwletProperty(myproperty['property']) + new_property.name = 'SOCK_STATUS' + new_property.display_name = 'Sock Status' + new_property.value = vitals['srf'] if 'srf' in vitals else '' + new_property.key = temp_key + 0.13 + new_property.expanded = True + self.properties[new_property.name] = new_property + + #BLE_RSSI = rsi + new_property = OwletProperty(myproperty['property']) + new_property.name = 'BLE_RSSI' + new_property.display_name = 'BLE RSSI' + new_property.value = vitals['rsi'] if 'rsi' in vitals else '' + new_property.key = temp_key + 0.14 + new_property.expanded = True + self.properties[new_property.name] = new_property + + """ + # ??? = sb + new_property = OwletProperty(myproperty['property']) + new_property.name = '???' + new_property.display_name = '???' + new_property.value = vitals['sb'] if 'sb' in vitals else '' + new_property.key = temp_key + 0.15 + new_property.expanded = True + self.properties[new_property.name] = new_property + """ + + """ + # ??? = ss + new_property = OwletProperty(myproperty['property']) + new_property.name = '???' + new_property.display_name = '???' + new_property.value = vitals['ss'] if 'ss' in vitals else '' + new_property.key = temp_key + 0.16 + new_property.expanded = True + self.properties[new_property.name] = new_property + """ + """ + + # ??? = mvb + new_property = OwletProperty(myproperty['property']) + new_property.name = '???' + new_property.display_name = '???' + new_property.value = vitals['mvb'] if 'mvb' in vitals else '' + new_property.key = temp_key + 0.17 + new_property.expanded = True + self.properties[new_property.name] = new_property + """ + """ + + # ??? = mst + new_property = OwletProperty(myproperty['property']) + new_property.name = '???' + new_property.display_name = '???' + new_property.value = vitals['mst'] if 'mst' in vitals else '' + new_property.key = temp_key + 0.18 + new_property.expanded = True + self.properties[new_property.name] = new_property + """ + # OXYGEN_TEN_MIN = oxta + new_property = OwletProperty(myproperty['property']) + new_property.name = 'OXYGEN_TEN_MIN' + new_property.display_name = 'Oxygen Ten Minute Average' + new_property.value = vitals['oxta'] if 'oxta' in vitals else '' + new_property.key = temp_key + 0.19 + new_property.expanded = True + self.properties[new_property.name] = new_property + """ + + # ??? = onm + new_property = OwletProperty(myproperty['property']) + new_property.name = '???' + new_property.display_name = '???' + new_property.value = vitals['onm'] if 'onm' in vitals else '' + new_property.key = temp_key + 0.20 + new_property.expanded = True + self.properties[new_property.name] = new_property + """ + """ + + # ??? = bsb + new_property = OwletProperty(myproperty['property']) + new_property.name = '???' + new_property.display_name = '???' + new_property.value = vitals['bsb'] if 'bsb' in vitals else '' + new_property.key = temp_key + 0.21 + new_property.expanded = True + self.properties[new_property.name] = new_property + """ + """ + + # ??? = hw + new_property = OwletProperty(myproperty['property']) + new_property.name = '???' + new_property.display_name = '???' + new_property.value = vitals['hw'] if 'hw' in vitals else '' + new_property.key = temp_key + 0.22 + new_property.expanded = True + self.properties[new_property.name] = new_property + """ for name, myproperty in self.properties.items(): if name == "APP_ACTIVE": @@ -132,11 +377,14 @@ def download_logged_data(self): raise OwletNotInitializedException( 'Initialize first - no properties') - if 'LOGGED_DATA_CACHE' not in self.properties: + if 'LOGGED_DATA_CACHE' not in self.properties and \ + 'VITALS_LOG_FILE' not in self.properties: raise OwletNotInitializedException( 'Initialize first - missing property') - - download_url = self.properties['LOGGED_DATA_CACHE'].value + if 'LOGGED_DATA_CACHE' in self.properties: + download_url = self.properties['LOGGED_DATA_CACHE'].value + if 'VITALS_LOG_FILE' in self.properties: + download_url = self.properties['VITALS_LOG_FILE'].value download_header = self.owlet_api.get_request_headers() try: diff --git a/owlet_api/owletapi.py b/owlet_api/owletapi.py index 708a624..e0e1926 100644 --- a/owlet_api/owletapi.py +++ b/owlet_api/owletapi.py @@ -1,11 +1,15 @@ #!/usr/bin/env python """Handles Owlet API stuff.""" +import json from json.decoder import JSONDecodeError import time +import calendar import requests from requests.exceptions import RequestException +import sqlite3 from .owlet import Owlet +from .owletpropertydatapoint import OwletPropertyDatapoint from .owletexceptions import OwletTemporaryCommunicationException from .owletexceptions import OwletPermanentCommunicationException from .owletexceptions import OwletNotInitializedException @@ -14,19 +18,65 @@ class OwletAPI(): """Handles Owlet API stuff.""" - base_user_url = 'https://user-field.aylanetworks.com/users/' - base_properties_url = 'https://ads-field.aylanetworks.com/apiv1/' + google_API_key = 'AIzaSyCBJ_5TRcPz_cQA4Xdqpcuo9PE5lR8Cc7k' + app_id = 'owa-rg-id' + app_secret = 'owa-dx85qljgtR6hmVflyrL6LasCxA8' + owlet_login_url = 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=' + owlet_login_token_provider_url = 'https://ayla-sso.owletdata.com/mini/' + base_user_url = 'https://ads-owlue1.aylanetworks.com/api/v1/token_sign_in.json' + base_properties_url = 'https://ads-owlue1.aylanetworks.com/apiv1/' + + #By default vital data is returned in 10 minute increments. Change here if different time is desired + vital_data_resolution = 600 def __init__(self, email=None, password=None): """Initialize OwletAPI, with email and password as opt. arguments.""" self._email = email self._password = password + self._owlet_id_token = None + self._owlet_local_id = None + self._owlet_refresh_token = None + self._owlet_id_token_expiry_time = None + self._owlet_mini_token = None self._auth_token = None + self._refresh_token = None self._expiry_time = None self._devices = [] + def set_vital_data_resolution(self, time): + """Set vital data time resolution.""" + self.vital_data_resolution = time + + def set_api_key(self, key): + """Set google API key.""" + self.google_API_key = key + + def set_app_id(self, app_id): + """Set app_id.""" + self.app_id = app_id + + def set_app_secret(self, app_secret): + """Set app_secret.""" + self.app_secret = app_secret + + def set_owlet_login_url(self, owlet_login_url): + """Set owlet_login_url.""" + self.owlet_login_url = owlet_login_url + + def set_owlet_login_token_provider_url(self, owlet_login_token_provider_url): + """Set owlet_login_token_provider_url.""" + self.owlet_login_token_provider_url = owlet_login_token_provider_url + + def set_base_user_url(self, base_user_url): + """Set base_user_url.""" + self.base_user_url = base_user_url + + def set_base_properties_url(self, base_properties_url): + """Set base_properties_url.""" + self.base_properties_url = base_properties_url + def set_email(self, email): - """Set Emailadress aka Username.""" + """Set Email address aka Username.""" self._email = email def set_password(self, password): @@ -34,23 +84,26 @@ def set_password(self, password): self._password = password def login(self): - """Login to Owlet Cloud Service and obtain Auth Token.""" + """Login is currently a three step process + 1. Login to Google Identity Toolkit to get owlet_authorization + 2. Use owlet_authorization to get Ayla login token + 3. Use Ayla login token to login to Ayla cloud database + """ + + """Step 1: Login to Google Identity Toolkit to get authorization.""" login_headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' } - login_url = self.base_user_url + 'sign_in.json' + login_url = self.owlet_login_url + self.google_API_key + + login_payload = { - 'user': { - 'email': self._email, - 'password': self._password, - 'application': { - 'app_id': 'OWL-id', - 'app_secret': 'OWL-4163742' - } - } + 'returnSecureToken': True, + 'email': self._email, + 'password': self._password } try: @@ -65,26 +118,130 @@ def login(self): 'Login request failed - no response') # Login failed - if result.status_code == 401: + if result.status_code == 400: + if result.text.__contains__('EMAIL_NOT_FOUND'): + raise OwletPermanentCommunicationException( + 'Login failed, bad username', result) + if result.text.__contains__('INVALID_PASSWORD'): + raise OwletPermanentCommunicationException( + 'Login failed, bad password', result) + if result.text.__contains__('API_KEY_INVALID'): + raise OwletPermanentCommunicationException( + 'Login failed, bad API key.', result) raise OwletPermanentCommunicationException( - 'Login failed, check username and password') + 'Login failed, check username and password', result) if result.status_code != 200: raise OwletTemporaryCommunicationException( - 'Login request failed - status code') + 'Login request failed - status code (' + str(result.status_code) + ') - (Step 2 of 3)', result) # Login seems to be ok, extract json try: json_result = result.json() except JSONDecodeError: raise OwletTemporaryCommunicationException( - 'Server did not send valid json') + 'Server did not send valid json (Step 1 of 3)') + + if ('idToken' not in json_result) or \ + ('localId' not in json_result) or \ + ('refreshToken' not in json_result) or \ + ('expiresIn' not in json_result): + raise OwletTemporaryCommunicationException( + 'Server did not send id token (Step 1 of 3)', json_result) + + self._owlet_id_token = json_result['idToken'] + self._owlet_local_id = json_result['localId'] + self._owlet_refresh_token = json_result['refreshToken'] + self._owlet_id_token_expiry_time = time.time() + int(json_result['expiresIn']) + + """Step 2: Retrieve Ayla Login Token.""" + login_headers = { + 'Accept': 'application/json', + 'authorization': self._owlet_id_token + } + + login_url = self.owlet_login_token_provider_url + + try: + result = requests.get( + login_url, + headers=login_headers, + timeout=5 + ) + except RequestException: + raise OwletTemporaryCommunicationException( + 'Login request failed - no response (Step 2 of 3)') + + # Login failed + if result.status_code != 200: + raise OwletTemporaryCommunicationException( + 'Login request failed - status code (' + str(result.status_code) + ') - (Step 2 of 3)', result) + + # Login seems to be ok, extract json + try: + json_result = result.json() + except JSONDecodeError: + raise OwletTemporaryCommunicationException( + 'Server did not send valid json (Step 2 of 3)') + + if ('mini_token' not in json_result): + raise OwletTemporaryCommunicationException( + 'Server did not send mini token (Step 2 of 3)', json_result) + + self._owlet_mini_token = json_result['mini_token'] + + """Step 3: Login to Ayla and obtain Auth Token.""" + login_headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + login_url = self.base_user_url + + login_payload = { + 'token': self._owlet_mini_token, + 'app_id': self.app_id, + 'app_secret': self.app_secret, + 'headers': { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + } + + try: + result = requests.post( + login_url, + json=login_payload, + headers=login_headers, + timeout=5 + ) + except RequestException: + raise OwletTemporaryCommunicationException( + 'Login request failed - no response (Step 3 of 3)', result) + + # Login failed + if result.status_code != 200: + if result.status_code == 404 and\ + result.text.__contains__('Could not find application'): + raise OwletPermanentCommunicationException( + 'login request failed - app_id or app_secret is bad (Step 3 of 3)', result) + raise OwletTemporaryCommunicationException( + 'Login request failed - status code (' + str(result.status_code) + ') - (Step 3 of 3)', result) + + # Login seems to be ok, extract json + try: + json_result = result.json() + except JSONDecodeError: + raise OwletTemporaryCommunicationException( + 'Server did not send valid json (Step 3 of 3)') if ('access_token' not in json_result) or \ + ('refresh_token' not in json_result) or \ ('expires_in' not in json_result): raise OwletTemporaryCommunicationException( - 'Server did not send access token') + 'Server did not send access token (Step 3 of 3)', json_result) self._auth_token = json_result['access_token'] + self._refresh_token = json_result['refresh_token'] self._expiry_time = time.time() + json_result['expires_in'] def get_auth_token(self): @@ -134,7 +291,7 @@ def update_devices(self): if result.status_code != 200: raise OwletTemporaryCommunicationException( - 'Server request failed - status code') + 'Server request failed - status code', result) try: json_result = result.json() @@ -168,3 +325,1058 @@ def get_update_interval(self): update_interval = device.get_update_interval() return update_interval + + def create_db_structure(self, con, cur): + #Setup Database if it isn't already setup + cur.execute("CREATE TABLE IF NOT EXISTS device(connected_at,connection_priority,connection_status,dealer,device_type,dsn,has_properties,hwsig,key,lan_enabled,lan_ip,lat,lng,locality,mac,manuf_model,model,oem_model,product_class,product_name,sw_version,template_id,unique_hardware_id, PRIMARY KEY(key), UNIQUE(dsn))") + cur.execute("CREATE TABLE IF NOT EXISTS device_properties(type,name,base_type,read_only,direction,scope,data_updated_at,key,device_key,product_name,track_only_changes,display_name,host_sw_version,time_series,derived,app_type,recipe,value,generated_from,generated_at,denied_roles,ack_enabled,retention_days,ack_status,ack_message,acked_at, PRIMARY KEY(key,data_updated_at))") + cur.execute("CREATE TABLE IF NOT EXISTS device_property_datapoints(device_dsn,property_name,id,updated_at,created_at,created_at_from_device,echo,metadata,generated_at,generated_from,value,acked_at,ack_status,ack_message, PRIMARY KEY(id))") + cur.execute("CREATE TABLE IF NOT EXISTS events (createTime,deviceType,eventType,isDiscrete,isUserModified,name,profile,profileType,service,serviceType,startTime,updateTime, PRIMARY KEY(name))") + cur.execute("CREATE TABLE IF NOT EXISTS sleep_state_summary(endTime,longestSleepSegmentMinutes,sessionType,sleepOnsetMinutes,sleepQuality,sleepStateDurationsMinutes,startTime,wakingsCount,awakeStateDurationsMinutes,lightSleepStateDurationsMinutes,deepSleepStateDurationsMinutes, PRIMARY KEY(startTime,endTime))") + cur.execute("CREATE TABLE IF NOT EXISTS sleep_state_detail(summary_startTime, summary_endTime, timeWindowStartTime, sleepState, PRIMARY KEY(summary_startTime, summary_endTime, timeWindowStartTime))") + cur.execute("CREATE TABLE IF NOT EXISTS vital_data(event_startTime,validSampleCount,firstReadingTime,heartRate_avg,heartRate_max,heartRate_min,lastReadingTime,movement_avg,oxygen_avg,oxygen_max,oxygen_min,timeWindowStartTime, PRIMARY KEY(event_startTime,timeWindowStartTime))") + con.commit() + + def save_device_to_db(self, con, cur, device): + cur.execute('INSERT into device (\ + connected_at,\ + connection_priority,\ + connection_status,\ + dealer,\ + device_type,\ + dsn,\ + has_properties,\ + hwsig,\ + key,\ + lan_enabled,\ + lan_ip,\ + lat,\ + lng,\ + locality,\ + mac,\ + manuf_model,\ + model,\ + oem_model,\ + product_class,\ + product_name,\ + sw_version,\ + template_id,\ + unique_hardware_id\ + )\ + VALUES (\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?\ + )\ + on conflict ("key") do \ + UPDATE\ + SET\ + connected_at = ?,\ + connection_priority = ?,\ + connection_status = ?,\ + dealer = ?,\ + device_type = ?,\ + dsn = ?,\ + has_properties = ?,\ + hwsig = ?,\ + key = ?,\ + lan_enabled = ?,\ + lan_ip = ?,\ + lat = ?,\ + lng = ?,\ + locality = ?,\ + mac = ?,\ + manuf_model = ?,\ + model = ?,\ + oem_model = ?,\ + product_class = ?,\ + product_name = ?,\ + sw_version = ?,\ + template_id = ?,\ + unique_hardware_id = ?\ + ',(device.connected_at,\ + json.dumps(device.connection_priority),\ + device.connection_status,\ + device.dealer,\ + device.device_type,\ + device.dsn,\ + device.has_properties,\ + device.hwsig,\ + device.key,\ + device.lan_enabled,\ + device.lan_ip,\ + device.lat,\ + device.lon,\ + device.locality,\ + device.mac,\ + device.manuf_model,\ + device.model,\ + device.oem_model,\ + device.product_class,\ + device.product_name,\ + device.sw_version,\ + device.template_id,\ + device.unique_hardware_id,\ + \ + device.connected_at,\ + json.dumps(device.connection_priority),\ + device.connection_status,\ + device.dealer,\ + device.device_type,\ + device.dsn,\ + device.has_properties,\ + device.hwsig,\ + device.key,\ + device.lan_enabled,\ + device.lan_ip,\ + device.lat,\ + device.lon,\ + device.locality,\ + device.mac,\ + device.manuf_model,\ + device.model,\ + device.oem_model,\ + device.product_class,\ + device.product_name,\ + device.sw_version,\ + device.template_id,\ + device.unique_hardware_id) + ) + con.commit() + + def save_device_property_to_db(self, con, cur, property): + cur.execute('INSERT into device_properties (\ + type,\ + name,\ + base_type,\ + read_only,\ + direction,\ + scope,\ + data_updated_at,\ + key,\ + device_key,\ + product_name,\ + track_only_changes,\ + display_name,\ + host_sw_version,\ + time_series,\ + derived,\ + app_type,\ + recipe,\ + value,\ + generated_from,\ + generated_at,\ + denied_roles,\ + ack_enabled,\ + retention_days,\ + ack_status,\ + ack_message,\ + acked_at\ + )\ + VALUES (\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?\ + )\ + on conflict ("key", "data_updated_at") do \ + UPDATE\ + SET\ + type=?,\ + name=?,\ + base_type=?,\ + read_only=?,\ + direction=?,\ + scope=?,\ + data_updated_at=?,\ + key=?,\ + device_key=?,\ + product_name=?,\ + track_only_changes=?,\ + display_name=?,\ + host_sw_version=?,\ + time_series=?,\ + derived=?,\ + app_type=?,\ + recipe=?,\ + value=?,\ + generated_from=?,\ + generated_at=?,\ + denied_roles=?,\ + ack_enabled=?,\ + retention_days=?,\ + ack_status=?,\ + ack_message=?,\ + acked_at=?\ + ',(property.type,\ + property.name,\ + property.base_type,\ + property.read_only,\ + property.direction,\ + property.scope,\ + property.data_updated_at,\ + property.key,\ + property.device_key,\ + property.product_name,\ + property.track_only_changes,\ + property.display_name,\ + property.host_sw_version,\ + property.time_series,\ + property.derived,\ + property.app_type,\ + property.recipe,\ + property.value,\ + property.generated_from,\ + property.generated_at,\ + json.dumps(property.denied_roles),\ + property.ack_enabled,\ + property.retention_days,\ + property.ack_status,\ + property.ack_message,\ + property.acked_at,\ + \ + property.type,\ + property.name,\ + property.base_type,\ + property.read_only,\ + property.direction,\ + property.scope,\ + property.data_updated_at,\ + property.key,\ + property.device_key,\ + property.product_name,\ + property.track_only_changes,\ + property.display_name,\ + property.host_sw_version,\ + property.time_series,\ + property.derived,\ + property.app_type,\ + property.recipe,\ + property.value,\ + property.generated_from,\ + property.generated_at,\ + json.dumps(property.denied_roles),\ + property.ack_enabled,\ + property.retention_days,\ + property.ack_status,\ + property.ack_message,\ + property.acked_at) + ) + con.commit() + + def save_device_property_datapoints_to_db(self, con, cur, dsn, property_name): + cur.execute("select MAX(created_at) as max, MIN(created_at) as min FROM device_property_datapoints WHERE device_dsn = '{}' and property_name = '{}'".format(dsn,property_name)) + max_date,min_date = cur.fetchone() + max_date = "&filter[created_at_since_date]="+max_date if max_date != None else '' + min_date = "&filter[created_at_end_date]="+min_date if min_date != None else '' + #Get datapoints newer than those already in DB + self.save_device_property_datapoints_to_db_by_range(con, cur, dsn, property_name, max_date) + + #No need to run this the earlier run already collected them all. + if max_date != "": + #Get datapoints older than those already in DB + self.save_device_property_datapoints_to_db_by_range(con, cur, dsn, property_name, min_date) + + def save_device_property_datapoints_to_db_by_range(self, con, cur, dsn, property_name, filter): + next_page = "" + while True: + cur.execute("begin") + """Get the Properties Datapoints.""" + datapoints_url = self.base_properties_url + \ + 'dsns/{}/properties/{}/datapoints.json?paginated=true&is_forward_page=true{}{}'.format(dsn,property_name,next_page,filter) + + properties_header = self.get_request_headers() + + try: + result = requests.get( + datapoints_url, + headers=properties_header + ) + except RequestException: + raise OwletTemporaryCommunicationException( + 'Server Request failed - no response') + + if result.status_code != 200: + raise OwletTemporaryCommunicationException( + 'Server Request failed - status code') + + try: + json_data = result.json() + except JSONDecodeError: + raise OwletTemporaryCommunicationException( + 'Update failed - JSON error') + next_page = "&next="+json_data["meta"]["next_page"] if json_data["meta"]["next_page"] != None else None + for mydatapoint in json_data["datapoints"]: + new_property = OwletPropertyDatapoint(mydatapoint['datapoint']) + self.save_individual_device_property_datapoint_to_db(cur,con,new_property,dsn,property_name) + + #Expand new format datapoints to old format + if property_name == 'REAL_TIME_VITALS': + # Convert Dream Sock Data to Smart Sock 3 Format + vitals = json.loads(new_property.value) + + # OXYGEN_LEVEL = ox + temp_property = new_property + temp_property.value = vitals['ox'] if 'ox' in vitals else '' + temp_property.id = new_property.id + '0.01' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'OXYGEN_LEVEL') + + #HEART_RATE = hr + temp_property = new_property + new_property.value = vitals['hr'] if 'hr' in vitals else '' + temp_property.id = new_property.id + '0.02' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'HEART_RATE') + + #MOVEMENT = mv + temp_property = new_property + new_property.value = vitals['mv'] if 'mv' in vitals else '' + temp_property.id = new_property.id + '0.03' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'MOVEMENT') + + # SOCK_CONNECTION = sc + temp_property = new_property + new_property.value = vitals['sc'] if 'sc' in vitals else '' + temp_property.id = new_property.id + '0.04' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'SOCK_CONNECTION') + + """ + # ??? = st + temp_property = new_property + new_property.value = vitals['st'] if 'st' in vitals else '' + temp_property.id = new_property.id + '0.05' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'???') + """ + + # BASE_STAT_ON = bso + temp_property = new_property + new_property.value = vitals['bso'] if 'bso' in vitals else '' + temp_property.id = new_property.id + '0.06' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'BASE_STAT_ON') + + #BATT_LEVEL = bat + temp_property = new_property + new_property.value = vitals['bat'] if 'bat' in vitals else '' + temp_property.id = new_property.id + '0.07' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'BATT_LEVEL') + + # BAT_TIME = btt + temp_property = new_property + new_property.value = vitals['btt'] if 'btt' in vitals else '' + temp_property.id = new_property.id + '0.08' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'BAT_TIME') + + # CHARGE_STATUS = chg + # 1 = Charged + # 2 = Charging + temp_property = new_property + new_property.value = vitals['chg'] if 'chg' in vitals else '' + temp_property.id = new_property.id + '0.09' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'CHARGE_STATUS') + + # ALRTS_DISABLED = aps + temp_property = new_property + new_property.value = vitals['aps'] if 'aps' in vitals else '' + temp_property.id = new_property.id + '0.10' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'ALRTS_DISABLED') + + # ALERT = alrt + # 16 = Disconnected + # 32 & 64 = Placement + temp_property = new_property + new_property.value = vitals['alrt'] if 'alrt' in vitals else '' + temp_property.id = new_property.id + '0.11' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'ALERT') + + # OTA_STATUS = ota + # 0 = None + # 1 = Firmware being sent + # 2 = Waiting for sock to be plugged in + # 3 = Installing + # 4 = Installing Critical + # 5 = Unknown + temp_property = new_property + new_property.value = vitals['ota'] if 'ota' in vitals else '' + temp_property.id = new_property.id + '0.12' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'OTA_STATUS') + + # SOCK_STATUS = srf + # 1 = Checking On + # 2 (When sc also = 2) = Kicking + # 3 = Recently Placed + temp_property = new_property + new_property.value = vitals['srf'] if 'srf' in vitals else '' + temp_property.id = new_property.id + '0.13' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'SOCK_STATUS') + + #BLE_RSSI = rsi + temp_property = new_property + new_property.value = vitals['rsi'] if 'rsi' in vitals else '' + temp_property.id = new_property.id + '0.14' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'BLE_RSSI') + + """ + # ??? = sb + temp_property = new_property + new_property.value = vitals['sb'] if 'sb' in vitals else '' + temp_property.id = new_property.id + '0.15' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'???') + """ + + """ + # ??? = ss + temp_property = new_property + new_property.value = vitals['ss'] if 'ss' in vitals else '' + temp_property.id = new_property.id + '0.16' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'???') + """ + """ + + # ??? = mvb + temp_property = new_property + new_property.value = vitals['mvb'] if 'mvb' in vitals else '' + temp_property.id = new_property.id + '0.17' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'???') + """ + """ + + # ??? = mst + temp_property = new_property + new_property.value = vitals['mst'] if 'mst' in vitals else '' + temp_property.id = new_property.id + '0.18' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'???') + """ + # OXYGEN_TEN_MIN = oxta + temp_property = new_property + new_property.value = vitals['oxta'] if 'oxta' in vitals else '' + temp_property.id = new_property.id + '0.19' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'OXYGEN_TEN_MIN') + """ + + # ??? = onm + temp_property = new_property + new_property.value = vitals['onm'] if 'onm' in vitals else '' + temp_property.id = new_property.id + '0.20' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'???') + """ + """ + + # ??? = bsb + temp_property = new_property + new_property.value = vitals['bsb'] if 'bsb' in vitals else '' + temp_property.id = new_property.id + '0.21' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'???') + """ + """ + + # ??? = hw + temp_property = new_property + new_property.value = vitals['hw'] if 'hw' in vitals else '' + temp_property.id = new_property.id + '0.22' + self.save_individual_device_property_datapoint_to_db(cur,con,temp_property,dsn,'???') + """ + cur.execute("commit") + con.commit() + if next_page == None: + break + print(".", end ="") + + + def save_individual_device_property_datapoint_to_db(self, cur, con, new_property, dsn, property_name): + #Add data to database + cur.execute('INSERT into device_property_datapoints (\ + device_dsn,\ + property_name,\ + id,\ + updated_at,\ + created_at,\ + created_at_from_device,\ + echo,\ + metadata,\ + generated_at,\ + generated_from,\ + value,\ + acked_at,\ + ack_status,\ + ack_message\ + )\ + VALUES (\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?\ + )\ + on conflict ("id") do \ + UPDATE\ + SET\ + device_dsn=?,\ + property_name=?,\ + id=?,\ + updated_at=?,\ + created_at=?,\ + created_at_from_device=?,\ + echo=?,\ + metadata=?,\ + generated_at=?,\ + generated_from=?,\ + value=?,\ + acked_at=?,\ + ack_status=?,\ + ack_message=?\ + ',(dsn,\ + property_name,\ + new_property.id,\ + new_property.updated_at,\ + new_property.created_at,\ + new_property.created_at_from_device,\ + new_property.echo,\ + json.dumps(new_property.metadata),\ + new_property.generated_at,\ + new_property.generated_from,\ + new_property.value,\ + new_property.acked_at,\ + new_property.ack_status,\ + new_property.ack_message,\ + \ + dsn,\ + property_name,\ + new_property.id,\ + new_property.updated_at,\ + new_property.created_at,\ + new_property.created_at_from_device,\ + new_property.echo,\ + json.dumps(new_property.metadata),\ + new_property.generated_at,\ + new_property.generated_from,\ + new_property.value,\ + new_property.acked_at,\ + new_property.ack_status,\ + new_property.ack_message) + ) + + def save_events_to_db(self, con, cur, event_type = ""): + #potential SQL injection error here that I am currently ignoring + secondary_filter_sql = " where eventType = '{}'".format(event_type) if event_type != '' else '' + secondary_filter_web = "eventType%20%3D%20{}%20AND%20".format(event_type) if event_type != '' else '' + + count = limit = 100 + + while count > 0: + cur.execute("begin") + + cur.execute("select MAX(startTime) as max, MIN(startTime) as min FROM events{}".format(secondary_filter_sql)) + max_date,min_date = cur.fetchone() + filter = "&filter={}(startTime%20%3E%20{}Z%20OR%20startTime%20%3C%20{}Z)".format(secondary_filter_web, max_date[0:19],min_date[0:19]) if max_date != None else '' + if filter == "" and secondary_filter_web != "": + filter = "&filter={}".format(secondary_filter_web[0:-9]) + + #Get Events + events_url = 'https://accounts.owletdata.com/v2/accounts/{}/events?totalSize={}{}'.format(self._owlet_local_id, limit, filter) + + """Get the Events.""" + properties_header = { + 'Accept': 'application/json', + 'Authorization': 'Bearer {}'.format(self._owlet_id_token) + } + + try: + result = requests.get( + events_url, + headers=properties_header + ) + except RequestException: + raise OwletTemporaryCommunicationException( + 'Server Request failed - no response') + if result.status_code == 598: + #Temporary read error wait 30 seconds and try again + time.sleep(30) + + #Close out DB transactions + cur.execute("commit") + con.commit() + + self.save_events_to_db(con, cur, event_type) + #Exit loop since error ocurred + break + if result.status_code == 401: + #Close out DB transactions + cur.execute("commit") + con.commit() + + print("401 Unauthorized. Loging in and trying again.") + + #Login appears to have timedout, so login again + self.login(); + + #Retry collecting data + self.save_events_to_db(con, cur, event_type) + #Exit loop since error ocurred + break + if result.status_code == 429: + #To many requests + wait_time = result.raw.retries.DEFAULT_BACKOFF_MAX or 120 + print("To many requests waiting "+wait_time+" to try again.") + time.sleep(wait_time) #wait the recommended time and try again + + #Close out DB transactions + cur.execute("commit") + con.commit() + + self.save_events_to_db(con, cur, event_type) + #Exit loop since error ocurred + return + if result.status_code != 200: + raise OwletTemporaryCommunicationException( + 'Server Request failed - status code') + + try: + json_data = result.json() + count = len(json_data["events"]) + except JSONDecodeError: + raise OwletTemporaryCommunicationException( + 'Update failed - JSON error') + for mydatapoint in json_data["events"]: + #Lookup details first to allow for resuming upon error + if mydatapoint["eventType"] == "EVENT_TYPE_PROFILE_SLEEP": + self.save_sleep_summary_data_to_db(con, cur, mydatapoint["profile"], mydatapoint["startTime"]) + self.save_vital_summary_data_to_db(con, cur, mydatapoint["profile"], mydatapoint["startTime"]) + + #Add data to database + cur.execute('INSERT into events (\ + createTime,\ + deviceType,\ + eventType,\ + isDiscrete,\ + isUserModified,\ + name,\ + profile,\ + profileType,\ + service,\ + serviceType,\ + startTime,\ + updateTime\ + )\ + VALUES (\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?\ + )\ + on conflict ("name") do \ + UPDATE\ + SET\ + createTime=?,\ + deviceType=?,\ + eventType=?,\ + isDiscrete=?,\ + isUserModified=?,\ + name=?,\ + profile=?,\ + profileType=?,\ + service=?,\ + serviceType=?,\ + startTime=?,\ + updateTime=?\ + ',(mydatapoint["createTime"] if 'createTime' in mydatapoint else '',\ + mydatapoint["deviceType"] if 'deviceType' in mydatapoint else '',\ + mydatapoint["eventType"] if 'eventType' in mydatapoint else '',\ + mydatapoint["isDiscrete"] if 'isDiscrete' in mydatapoint else '',\ + mydatapoint["isUserModified"] if 'isUserModified' in mydatapoint else '',\ + mydatapoint["name"] if 'name' in mydatapoint else '',\ + mydatapoint["profile"] if 'profile' in mydatapoint else '',\ + mydatapoint["profileType"] if 'profileType' in mydatapoint else '',\ + mydatapoint["service"] if 'service' in mydatapoint else '',\ + mydatapoint["serviceType"] if 'serviceType' in mydatapoint else '',\ + mydatapoint["startTime"] if 'startTime' in mydatapoint else '',\ + mydatapoint["updateTime"] if 'updateTime' in mydatapoint else '',\ + \ + mydatapoint["createTime"] if 'createTime' in mydatapoint else '',\ + mydatapoint["deviceType"] if 'deviceType' in mydatapoint else '',\ + mydatapoint["eventType"] if 'eventType' in mydatapoint else '',\ + mydatapoint["isDiscrete"] if 'isDiscrete' in mydatapoint else '',\ + mydatapoint["isUserModified"] if 'isUserModified' in mydatapoint else '',\ + mydatapoint["name"] if 'name' in mydatapoint else '',\ + mydatapoint["profile"] if 'profile' in mydatapoint else '',\ + mydatapoint["profileType"] if 'profileType' in mydatapoint else '',\ + mydatapoint["service"] if 'service' in mydatapoint else '',\ + mydatapoint["serviceType"] if 'serviceType' in mydatapoint else '',\ + mydatapoint["startTime"] if 'startTime' in mydatapoint else '',\ + mydatapoint["updateTime"] if 'updateTime' in mydatapoint else '') + ) + cur.execute("commit") + con.commit() + con.commit() + + def save_sleep_summary_data_to_db(self, con, cur, profile, start_time): + #Convert start_time to timestamp + temp_time = time.strptime(start_time, "%Y-%m-%dT%H:%M:%S%z") + start_timestamp = calendar.timegm(temp_time) + #Calculate end_time + end_timestamp = start_timestamp + 86400 + + #Get Sleep Summary Data + events_url = 'https://sleep-data.owletdata.com/v1/{}/sleep?endTime={}&startTime={}&timeZone=GMT&version=smartSock3Sleep'.format(profile,end_timestamp,start_timestamp) + """Get the Events.""" + properties_header = { + 'Accept': 'application/json', + 'Authorization': '{}'.format(self._owlet_id_token) + } + + try: + result = requests.get( + events_url, + headers=properties_header + ) + except RequestException: + raise OwletTemporaryCommunicationException( + 'Server Request failed - no response') + if result.status_code == 598: + #Temporary read error try again + self.save_sleep_summary_data_to_db(con, cur, profile, start_time) + #Exit loop since error ocurred + return + if result.status_code == 401: + #Login error, try logging in again + print("401 Unauthorized. Loging in and trying again.") + self.login() + #Try collecting data again + self.save_sleep_summary_data_to_db(con, cur, profile, start_time) + #Exit loop since error ocurred + return + if result.status_code == 429: + #To many requests + wait_time = result.raw.retries.DEFAULT_BACKOFF_MAX or 120 + print("To many requests waiting "+wait_time+" to try again.") + time.sleep(wait_time) #wait the recommended 2 minutes and try again + self.save_sleep_summary_data_to_db(con, cur, profile, start_time) + #Exit loop since error ocurred + return + if result.status_code != 200: + raise OwletTemporaryCommunicationException( + 'Server Request failed - status code') + + try: + json_data = result.json() + except JSONDecodeError: + raise OwletTemporaryCommunicationException( + 'Update failed - JSON error') + for mydatapoint in json_data["sessions"]: + #Add data to database + cur.execute('INSERT into sleep_state_summary (\ + endTime,\ + longestSleepSegmentMinutes,\ + sessionType,\ + sleepOnsetMinutes,\ + sleepQuality,\ + sleepStateDurationsMinutes,\ + startTime,\ + wakingsCount,\ + awakeStateDurationsMinutes,\ + lightSleepStateDurationsMinutes,\ + deepSleepStateDurationsMinutes\ + )\ + VALUES (\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?\ + )\ + on conflict ("startTime", "endTime") do \ + UPDATE\ + SET\ + endTime=?,\ + longestSleepSegmentMinutes=?,\ + sessionType=?,\ + sleepOnsetMinutes=?,\ + sleepQuality=?,\ + sleepStateDurationsMinutes=?,\ + startTime=?,\ + wakingsCount=?,\ + awakeStateDurationsMinutes=?,\ + lightSleepStateDurationsMinutes=?,\ + deepSleepStateDurationsMinutes=?\ + ',(mydatapoint["endTime"] if 'endTime' in mydatapoint else '',\ + mydatapoint["longestSleepSegmentMinutes"] if 'longestSleepSegmentMinutes' in mydatapoint else '',\ + mydatapoint["sessionType"] if 'sessionType' in mydatapoint else '',\ + mydatapoint["sleepOnsetMinutes"] if 'sleepOnsetMinutes' in mydatapoint else '',\ + mydatapoint["sleepQuality"] if 'sleepQuality' in mydatapoint else '',\ + json.dumps(mydatapoint["sleepStateDurationsMinutes"]) if 'sleepStateDurationsMinutes' in mydatapoint else '',\ + mydatapoint["startTime"] if 'startTime' in mydatapoint else '',\ + mydatapoint["wakingsCount"] if 'wakingsCount' in mydatapoint else '',\ + mydatapoint["sleepStateDurationsMinutes"]['1'] if '1' in mydatapoint["sleepStateDurationsMinutes"] else '',\ + mydatapoint["sleepStateDurationsMinutes"]['8'] if '8' in mydatapoint["sleepStateDurationsMinutes"] else '',\ + mydatapoint["sleepStateDurationsMinutes"]['15'] if '15' in mydatapoint["sleepStateDurationsMinutes"] else '',\ + \ + mydatapoint["endTime"] if 'endTime' in mydatapoint else '',\ + mydatapoint["longestSleepSegmentMinutes"] if 'longestSleepSegmentMinutes' in mydatapoint else '',\ + mydatapoint["sessionType"] if 'sessionType' in mydatapoint else '',\ + mydatapoint["sleepOnsetMinutes"] if 'sleepOnsetMinutes' in mydatapoint else '',\ + mydatapoint["sleepQuality"] if 'sleepQuality' in mydatapoint else '',\ + json.dumps(mydatapoint["sleepStateDurationsMinutes"]) if 'sleepStateDurationsMinutes' in mydatapoint else '',\ + mydatapoint["startTime"] if 'startTime' in mydatapoint else '',\ + mydatapoint["wakingsCount"] if 'wakingsCount' in mydatapoint else '',\ + mydatapoint["sleepStateDurationsMinutes"]['1'] if '1' in mydatapoint["sleepStateDurationsMinutes"] else '',\ + mydatapoint["sleepStateDurationsMinutes"]['8'] if '8' in mydatapoint["sleepStateDurationsMinutes"] else '',\ + mydatapoint["sleepStateDurationsMinutes"]['15'] if '15' in mydatapoint["sleepStateDurationsMinutes"] else '') + ) + count = len(json_data["data"]["timeWindowStartTimes"]) + for x in range(count): + summary_startTime = json_data["sessions"][0]["startTime"] + summary_endTime = json_data["sessions"][0]["endTime"] + timeWindowStartTime = json_data["data"]["timeWindowStartTimes"][x] + sleepState = json_data["data"]["sleepStates"][x] + cur.execute('INSERT into sleep_state_detail (\ + summary_startTime,\ + summary_endTime,\ + timeWindowStartTime,\ + sleepState\ + )\ + VALUES (\ + ?,\ + ?,\ + ?,\ + ?\ + )\ + on conflict (summary_startTime, summary_endTime, timeWindowStartTime) do \ + UPDATE\ + SET\ + summary_startTime=?,\ + summary_endTime=?,\ + timeWindowStartTime=?,\ + sleepState=?\ + ',(summary_startTime if summary_startTime != "" else '',\ + summary_endTime if summary_startTime != "" else '',\ + timeWindowStartTime if timeWindowStartTime != "" else '',\ + sleepState if sleepState != "" else '',\ + \ + summary_startTime if summary_startTime != "" else '',\ + summary_endTime if summary_startTime != "" else '',\ + timeWindowStartTime if timeWindowStartTime != "" else '',\ + sleepState if sleepState != "" else '') + ) + + def save_vital_summary_data_to_db(self, con, cur, profile, start_time): + #Convert start_time to timestamp + temp_time = time.strptime(start_time, "%Y-%m-%dT%H:%M:%S%z") + start_timestamp = calendar.timegm(temp_time) + #Calculate end_time + end_timestamp = start_timestamp + 86400 + + #Get Vitals Data + events_url = 'https://vital-data.owletdata.com/v1/{}/vitals?resolution={}&startTime={}&version=smartSock3Sleep&endTime={}'.format(profile,self.vital_data_resolution,start_timestamp,end_timestamp) + """Get the Events.""" + properties_header = { + 'Accept': 'application/json', + 'Authorization': '{}'.format(self._owlet_id_token) + } + + try: + result = requests.get( + events_url, + headers=properties_header + ) + except RequestException: + raise OwletTemporaryCommunicationException( + 'Server Request failed - no response') + if result.status_code == 598: + #Temporary read error try again + self.save_vital_summary_data_to_db(con, cur, profile, start_time) + #Exit loop since error ocurred + return + if result.status_code != 200: + raise OwletTemporaryCommunicationException( + 'Server Request failed - status code') + + try: + json_data = result.json() + except JSONDecodeError: + raise OwletTemporaryCommunicationException( + 'Update failed - JSON error') + count = len(json_data["data"]["timeWindowStartTimes"]) + for x in range(count): + event_startTime = start_time + validSampleCount = json_data["data"]["counts"]["validSamples"][x] + firstReadingTime = json_data["data"]["firstReadingTimes"][x] + heartRate_avg = json_data["data"]["heartRate"]["avg"][x] + heartRate_max = json_data["data"]["heartRate"]["max"][x] + heartRate_min = json_data["data"]["heartRate"]["min"][x] + lastReadingTime = json_data["data"]["lastReadingTimes"][x] + movement_avg = json_data["data"]["movement"]["avg"][x] + oxygen_avg = json_data["data"]["oxygen"]["avg"][x] + oxygen_max = json_data["data"]["oxygen"]["max"][x] + oxygen_min = json_data["data"]["oxygen"]["min"][x] + timeWindowStartTime = json_data["data"]["timeWindowStartTimes"][x] + cur.execute('INSERT into vital_data (\ + event_startTime,\ + validSampleCount,\ + firstReadingTime,\ + heartRate_avg,\ + heartRate_max,\ + heartRate_min,\ + lastReadingTime,\ + movement_avg,\ + oxygen_avg,\ + oxygen_max,\ + oxygen_min,\ + timeWindowStartTime\ + )\ + VALUES (\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?,\ + ?\ + )\ + on conflict (event_startTime,timeWindowStartTime) do \ + UPDATE\ + SET\ + event_startTime=?,\ + validSampleCount=?,\ + firstReadingTime=?,\ + heartRate_avg=?,\ + heartRate_max=?,\ + heartRate_min=?,\ + lastReadingTime=?,\ + movement_avg=?,\ + oxygen_avg=?,\ + oxygen_max=?,\ + oxygen_min=?,\ + timeWindowStartTime=?\ + ',(event_startTime if event_startTime != "" else '',\ + validSampleCount if validSampleCount != "" else '',\ + firstReadingTime if firstReadingTime != "" else '',\ + heartRate_avg if heartRate_avg != "" else '',\ + heartRate_max if heartRate_max != "" else '',\ + heartRate_min if heartRate_min != "" else '',\ + lastReadingTime if lastReadingTime != "" else '',\ + movement_avg if movement_avg != "" else '',\ + oxygen_avg if oxygen_avg != "" else '',\ + oxygen_max if oxygen_max != "" else '',\ + oxygen_min if oxygen_min != "" else '',\ + timeWindowStartTime if timeWindowStartTime != "" else '',\ + \ + event_startTime if event_startTime != "" else '',\ + validSampleCount if validSampleCount != "" else '',\ + firstReadingTime if firstReadingTime != "" else '',\ + heartRate_avg if heartRate_avg != "" else '',\ + heartRate_max if heartRate_max != "" else '',\ + heartRate_min if heartRate_min != "" else '',\ + lastReadingTime if lastReadingTime != "" else '',\ + movement_avg if movement_avg != "" else '',\ + oxygen_avg if oxygen_avg != "" else '',\ + oxygen_max if oxygen_max != "" else '',\ + oxygen_min if oxygen_min != "" else '',\ + timeWindowStartTime if timeWindowStartTime != "" else '') + ) + + def save_everything_to_db(self, db_name = 'owlet.db'): + con = sqlite3.connect(db_name) + cur = con.cursor() + + #Setup isolation level for performance reasons + con.isolation_level = None + + self.create_db_structure(con, cur) + + # Get/Update device info + for device in self.get_devices(): + print("Saving device {} to DB".format(device.dsn)) + self.save_device_to_db(con, cur, device) + device.update() + + #Turn logging on + try: + device.reactivate() + except OwletTemporaryCommunicationException: + continue + + print("Saving device {}'s current state to DB".format(device.dsn)) + for name, property in device.get_properties().items(): + #Save Current Property Info + self.save_device_property_to_db(con, cur, property) + if property.expanded == False: + #Save Historical Property Datapoints + print("Saving device {}'s historical state for {} to DB".format(device.dsn, name), end ="") + self.save_device_property_datapoints_to_db(con, cur, device.dsn, name) + print("") + + print("Save events (like low O2 Alarm) to DB") + self.save_events_to_db(con, cur) + + print("Save sleeping events to DB") + self.save_events_to_db(con, cur, "EVENT_TYPE_PROFILE_SLEEP") + + con.close() diff --git a/owlet_api/owletproperty.py b/owlet_api/owletproperty.py index 26299c9..f035779 100644 --- a/owlet_api/owletproperty.py +++ b/owlet_api/owletproperty.py @@ -18,6 +18,29 @@ def __init__(self, json): self.last_update = None self.minimum_update_interval = None self.key = None + self.type = None + self.base_type = None + self.read_only = None + self.direction = None + self.scope = None + self.data_updated_at = None + self.device_key = None + self.product_name = None + self.track_only_changes = None + self.host_sw_version = None + self.time_series = None + self.derived = None + self.app_type = None + self.recipe = None + self.generated_from = None + self.generated_at = None + self.denied_roles = None + self.ack_enabled = None + self.retention_days = None + self.ack_status = None + self.ack_message = None + self.acked_at = None + self.expanded = False self._from_json(json) @@ -27,10 +50,32 @@ def update(self, json): def _from_json(self, json): """Parse JSON and update attributes of class.""" - self.name = json['name'] - self.display_name = json['display_name'] - self.value = json['value'] - self.key = json['key'] + self.name = json['name'] if 'name' in json else '' + self.display_name = json['display_name'] if 'display_name' in json else '' + self.value = json['value'] if 'value' in json else '' + self.key = json['key'] if 'key' in json else '' + self.type = json['type'] if 'type' in json else '' + self.base_type = json['base_type'] if 'base_type' in json else '' + self.read_only = json['read_only'] if 'read_only' in json else '' + self.direction = json['direction'] if 'direction' in json else '' + self.scope = json['scope'] if 'scope' in json else '' + self.data_updated_at = json['data_updated_at'] if 'data_updated_at' in json else '' + self.device_key = json['device_key'] if 'device_key' in json else '' + self.product_name = json['product_name'] if 'product_name' in json else '' + self.track_only_changes = json['track_only_changes'] if 'track_only_changes' in json else '' + self.host_sw_version = json['host_sw_version'] if 'host_sw_version' in json else '' + self.time_series = json['time_series'] if 'time_series' in json else '' + self.derived = json['derived'] if 'derived' in json else '' + self.app_type = json['app_type'] if 'app_type' in json else '' + self.recipe = json['recipe'] if 'recipe' in json else '' + self.generated_from = json['generated_from'] if 'generated_from' in json else '' + self.generated_at = json['generated_at'] if 'generated_at' in json else '' + self.denied_roles = json['denied_roles'] if 'denied_roles' in json else '' + self.ack_enabled = json['ack_enabled'] if 'ack_enabled' in json else '' + self.retention_days = json['retention_days'] if 'retention_days' in json else '' + self.ack_status = json['ack_status'] if 'ack_status' in json else '' + self.ack_message = json['ack_message'] if 'ack_message' in json else '' + self.acked_at = json['acked_at'] if 'acked_at' in json else '' if json['data_updated_at'] != "null": new_update = parse(json['data_updated_at']) diff --git a/owlet_api/owletpropertydatapoint.py b/owlet_api/owletpropertydatapoint.py new file mode 100644 index 0000000..af43b6f --- /dev/null +++ b/owlet_api/owletpropertydatapoint.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +"""Class to keep information of one property.""" + +from dateutil.parser import parse + +# We really only have little public methods +# pylint: disable=R0903 + + +class OwletPropertyDatapoint(): + """Class to keep information of one property datapoint.""" + + def __init__(self, json): + """Initialize property datapoint from json object as argument.""" + self.id = None + self.updated_at = None + self.created_at = None + self.created_at_from_device = None + self.echo = None + self.metadata = None + self.generated_at = None + self.generated_from = None + self.value = "" + self.acked_at = None + self.ack_status = None + self.ack_message = None + + self._from_json(json) + + def update(self, json): + """Update property datapoint from JSON.""" + self._from_json(json) + + def _from_json(self, json): + """Parse JSON and update attributes of class.""" + self.id = json['id'] if 'id' in json else '' + self.updated_at = json['updated_at'] if 'updated_at' in json else '' + self.created_at = json['created_at'] if 'created_at' in json else '' + self.created_at_from_device = json['created_at_from_device'] if 'created_at_from_device' in json else '' + self.echo = json['echo'] if 'echo' in json else '' + self.metadata = json['metadata'] if 'metadata' in json else '' + self.generated_at = json['generated_at'] if 'generated_at' in json else '' + self.generated_from = json['generated_from'] if 'generated_from' in json else '' + self.value = json['value'] if 'value' in json else '' + self.acked_at = json['acked_at'] if 'acked_at' in json else '' + self.ack_status = json['ack_status'] if 'ack_status' in json else '' + self.ack_message = json['ack_message'] if 'ack_message' in json else '' diff --git a/setup.py b/setup.py index 249241f..8e2b571 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup, find_packages name='owlet_api' -version='0.1.0' +version='0.2.0' setup(name = name, version = version, diff --git a/tests/test_cli.py b/tests/test_cli.py index 696c871..02fdecb 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -17,7 +17,12 @@ from owlet_api.cli import cli LOGIN_PAYLOAD = { - 'access_token': 'testtoken', + 'access_token': 'test_access_token', + 'idToken': 'test_id_token', + 'refreshToken': 'test_refresh_token', + 'refresh_token': 'test_refresh_token', + 'mini_token': 'test_min_token', + 'expiresIn': '3600', 'expires_in': 86400 } @@ -168,16 +173,20 @@ 'metadata':{ }, - 'value':'https://ads-field.aylanetworks.com/apiv1/devices/24826059/properties/LOGGED_DATA_CACHE/datapoints/76ce9810-5375-11e8-e7a5-6450803806ca.json', + 'value':OwletAPI.base_properties_url + 'devices/24826059/properties/LOGGED_DATA_CACHE/datapoints/76ce9810-5375-11e8-e7a5-6450803806ca.json', 'created_at_from_device':None, - 'file':'https://ayla-device-field-production-1a2039d9.s3.amazonaws.com/X?AWSAccessKeyId=Y&Expires=1234&Signature=Z' + 'file':'https://ayla-device-owlue1-production-1a2039d9.s3.amazonaws.com/X?AWSAccessKeyId=Y&Expires=1234&Signature=Z' } } @responses.activate def test_cli_token(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) with patch('sys.argv', ['cli.py', 'test@test.de', 'moped', 'token']): @@ -185,9 +194,8 @@ def test_cli_token(): @responses.activate def test_cli_login_fail(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', - json=LOGIN_PAYLOAD, status=401) - + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=400) with patch('sys.argv', ['cli.py', 'test@test.de', 'moped', 'token']): with pytest.raises(SystemExit) as info: @@ -207,9 +215,13 @@ def test_cli_server_down(): @responses.activate def test_cli_devices_ok(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/devices.json', + responses.add(responses.GET, OwletAPI.base_properties_url + 'devices.json', json=DEVICES_PAYLOAD, status=200) with patch('sys.argv', ['cli.py', 'test@test.de', 'moped', 'devices']): @@ -217,11 +229,15 @@ def test_cli_devices_ok(): @responses.activate def test_cli_attributes_ok(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/devices.json', + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.base_properties_url + 'devices.json', json=DEVICES_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) with patch('sys.argv', ['cli.py', 'test@test.de', 'moped', 'attributes']): @@ -229,15 +245,19 @@ def test_cli_attributes_ok(): @responses.activate def test_cli_download_ok(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/devices.json', + responses.add(responses.GET, OwletAPI.base_properties_url + 'devices.json', json=DEVICES_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) responses.add(responses.GET, 'http://de.mo/file', json=DOWNLOAD_DATA, status=200) - responses.add(responses.GET, 'https://ayla-device-field-production-1a2039d9.s3.amazonaws.com/X?AWSAccessKeyId=Y&Expires=1234&Signature=Z', + responses.add(responses.GET, 'https://ayla-device-owlue1-production-1a2039d9.s3.amazonaws.com/X?AWSAccessKeyId=Y&Expires=1234&Signature=Z', status=200) with patch('sys.argv', ['cli.py', 'test@test.de', 'moped', 'download']): @@ -247,13 +267,17 @@ def test_cli_download_ok(): @responses.activate def test_cli_stream_ok(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/devices.json', + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.base_properties_url + 'devices.json', json=DEVICES_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) - responses.add(responses.POST, 'https://ads-field.aylanetworks.com/apiv1/properties/42738119/datapoints', + responses.add(responses.POST, OwletAPI.base_properties_url + 'properties/42738119/datapoints', status=201) with patch('sys.argv', ['cli.py', 'test@test.de', 'moped', '--timeout', '10', 'stream']): @@ -261,17 +285,21 @@ def test_cli_stream_ok(): #@pytest.mark.skip(reason="no way of currently testing this") @responses.activate -def test_cli_stream_updatefail(): +def test_cli_stream_update_fail(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/devices.json', + responses.add(responses.GET, OwletAPI.base_properties_url + 'devices.json', json=DEVICES_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) - responses.add(responses.POST, 'https://ads-field.aylanetworks.com/apiv1/properties/42738119/datapoints', + responses.add(responses.POST, OwletAPI.base_properties_url + 'properties/42738119/datapoints', status=201) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=400) with patch('sys.argv', ['cli.py', 'test@test.de', 'moped', '--timeout', '10', 'stream']): @@ -279,15 +307,19 @@ def test_cli_stream_updatefail(): #@pytest.mark.skip(reason="no way of currently testing this") @responses.activate -def test_cli_stream_reactivationfail(): +def test_cli_stream_reactivation_fail(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/devices.json', + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.base_properties_url + 'devices.json', json=DEVICES_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) - responses.add(responses.POST, 'https://ads-field.aylanetworks.com/apiv1/properties/42738119/datapoints', + responses.add(responses.POST, OwletAPI.base_properties_url + 'properties/42738119/datapoints', status=400) with patch('sys.argv', ['cli.py', 'test@test.de', 'moped', '--timeout', '10', 'stream']): @@ -299,13 +331,17 @@ def test_cli_stream_reactivationfail(): def test_cli_stream_ctrlc(sleep_mock): sleep_mock.side_effect = SystemExit - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/devices.json', + responses.add(responses.GET, OwletAPI.base_properties_url + 'devices.json', json=DEVICES_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) - responses.add(responses.POST, 'https://ads-field.aylanetworks.com/apiv1/properties/42738119/datapoints', + responses.add(responses.POST, OwletAPI.base_properties_url + 'properties/42738119/datapoints', status=201) with patch('sys.argv', ['cli.py', 'test@test.de', 'moped', '--timeout', '10', 'stream']): diff --git a/tests/test_owlet.py b/tests/test_owlet.py index 49b92a0..1e54445 100644 --- a/tests/test_owlet.py +++ b/tests/test_owlet.py @@ -15,7 +15,12 @@ from owlet_api.owletexceptions import OwletNotInitializedException LOGIN_PAYLOAD = { - 'access_token': 'testtoken', + 'access_token': 'test_access_token', + 'idToken': 'test_id_token', + 'refreshToken': 'test_refresh_token', + 'refresh_token': 'test_refresh_token', + 'mini_token': 'test_min_token', + 'expiresIn': '3600', 'expires_in': 86400 } @@ -162,7 +167,7 @@ 'metadata':{ }, - 'value':'https://ads-field.aylanetworks.com/apiv1/devices/24826059/properties/LOGGED_DATA_CACHE/datapoints/76ce9810-5375-11e8-e7a5-6450803806ca.json', + 'value':OwletAPI.base_properties_url + 'devices/24826059/properties/LOGGED_DATA_CACHE/datapoints/76ce9810-5375-11e8-e7a5-6450803806ca.json', 'created_at_from_device':None, 'file':'https://ayla-device-field-production-1a2039d9.s3.amazonaws.com/X?AWSAccessKeyId=Y&Expires=1234&Signature=Z' } @@ -194,10 +199,14 @@ def test_owlet_ok(): @responses.activate def test_update_ok(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) # Owlet will pull the properties of this particular device - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) # Initialize OwletAPI @@ -207,7 +216,7 @@ def test_update_ok(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() assert device.get_property('AGE_MONTHS_OLD').value == None @@ -215,8 +224,12 @@ def test_update_ok(): assert device.get_property('APP_ACTIVE').value == 0 @responses.activate -def test_update_noresponse(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_update_no_response(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) # Initialize OwletAPI @@ -226,7 +239,7 @@ def test_update_noresponse(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device with pytest.raises(OwletTemporaryCommunicationException) as info: device.update() @@ -236,9 +249,13 @@ def test_update_noresponse(): @responses.activate def test_update_return_code(): # Owlet will pull the properties of this particular device - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=500) - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) # Initialize OwletAPI @@ -248,7 +265,7 @@ def test_update_return_code(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device with pytest.raises(OwletTemporaryCommunicationException) as info: device.update() @@ -258,10 +275,14 @@ def test_update_return_code(): @responses.activate def test_update_invalid_json(): # Owlet will pull the properties of this particular device - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', body="INVALID", status=200) - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) # Initialize OwletAPI @@ -271,7 +292,7 @@ def test_update_invalid_json(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device with pytest.raises(OwletTemporaryCommunicationException) as info: device.update() @@ -285,11 +306,15 @@ def test_update_repeat(): my_device_attributes[0]['property']['data_updated_at'] = '2018-12-30T09:43:28Z' # Owlet will pull the properties of this particular device - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=my_device_attributes, status=200) - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) # Initialize OwletAPI @@ -299,7 +324,7 @@ def test_update_repeat(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() device.update() @@ -314,11 +339,15 @@ def test_update_repeat(): @responses.activate def test_reactivate_ok(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) - responses.add(responses.POST, 'https://ads-field.aylanetworks.com/apiv1/properties/42738119/datapoints', + responses.add(responses.POST, OwletAPI.base_properties_url + 'properties/42738119/datapoints', status=201) # Initialize OwletAPI @@ -328,14 +357,18 @@ def test_reactivate_ok(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() device.reactivate() @responses.activate -def test_reactivate_fail_noattributes(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_reactivate_fail_no_attributes(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) # Initialize OwletAPI @@ -352,15 +385,19 @@ def test_reactivate_fail_noattributes(): @responses.activate -def test_reactivate_fail_wrongattributes(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_reactivate_fail_wrong_attributes(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) my_device_attributes = copy.deepcopy(DEVICE_ATTRIBUTES) my_device_attributes[0]['property']['name'] = 'DEADBEEF1' my_device_attributes[1]['property']['name'] = 'DEADBEEF2' my_device_attributes[2]['property']['name'] = 'DEADBEEF3' - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=my_device_attributes, status=200) # Initialize OwletAPI @@ -370,7 +407,7 @@ def test_reactivate_fail_wrongattributes(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() with pytest.raises(OwletNotInitializedException) as info: @@ -379,10 +416,14 @@ def test_reactivate_fail_wrongattributes(): assert 'Initialize first - missing property' in str(info.value) @responses.activate -def test_reactivate_fail_noconnection(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_reactivate_fail_no_connection(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.POST, OwletAPI.base_user_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) # Initialize OwletAPI @@ -392,7 +433,7 @@ def test_reactivate_fail_noconnection(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() with pytest.raises(OwletTemporaryCommunicationException) as info: @@ -401,12 +442,16 @@ def test_reactivate_fail_noconnection(): assert 'Server Request failed - no response' in str(info.value) @responses.activate -def test_reactivate_fail_statuscode(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_reactivate_fail_status_code(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.POST, OwletAPI.base_user_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) - responses.add(responses.POST, 'https://ads-field.aylanetworks.com/apiv1/properties/42738119/datapoints', + responses.add(responses.POST, OwletAPI.base_properties_url + 'properties/42738119/datapoints', status=500) # Initialize OwletAPI @@ -416,7 +461,7 @@ def test_reactivate_fail_statuscode(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() with pytest.raises(OwletTemporaryCommunicationException) as info: @@ -426,9 +471,13 @@ def test_reactivate_fail_statuscode(): @responses.activate def test_download_logged_data_ok(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) responses.add(responses.GET, 'http://de.mo/file', json=DOWNLOAD_DATA, status=200) @@ -442,15 +491,19 @@ def test_download_logged_data_ok(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() device.download_logged_data() @responses.activate -def test_download_logged_data_fail_noinit(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_download_logged_data_fail_no_init(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) # Initialize OwletAPI @@ -467,15 +520,19 @@ def test_download_logged_data_fail_noinit(): assert 'Initialize first - no properties' in str(info.value) @responses.activate -def test_download_logged_data_fail_noattribute(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_download_logged_data_fail_no_attribute(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) my_device_attributes = copy.deepcopy(DEVICE_ATTRIBUTES) my_device_attributes[0]['property']['name'] = 'DEADBEEF3' my_device_attributes[1]['property']['name'] = 'DEADBEEF3' my_device_attributes[2]['property']['name'] = 'DEADBEEF3' my_device_attributes[3]['property']['name'] = 'DEADBEEF3' - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=my_device_attributes, status=200) # Initialize OwletAPI @@ -485,7 +542,7 @@ def test_download_logged_data_fail_noattribute(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() with pytest.raises(OwletNotInitializedException) as info: @@ -494,10 +551,14 @@ def test_download_logged_data_fail_noattribute(): assert 'Initialize first - missing property' in str(info.value) @responses.activate -def test_download_logged_data_fail_noconnection(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_download_logged_data_fail_no_connection(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.POST, OwletAPI.base_user_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) # Initialize OwletAPI @@ -507,7 +568,7 @@ def test_download_logged_data_fail_noconnection(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() with pytest.raises(OwletTemporaryCommunicationException) as info: @@ -517,10 +578,14 @@ def test_download_logged_data_fail_noconnection(): @responses.activate -def test_download_logged_data_fail_statuscode(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_download_logged_data_fail_status_code(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.POST, OwletAPI.base_user_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) responses.add(responses.GET, 'http://de.mo/file', json=DOWNLOAD_DATA, status=500) @@ -532,7 +597,7 @@ def test_download_logged_data_fail_statuscode(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() with pytest.raises(OwletTemporaryCommunicationException) as info: @@ -542,10 +607,14 @@ def test_download_logged_data_fail_statuscode(): @responses.activate -def test_download_logged_data_fail_invalidjson(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_download_logged_data_fail_invalid_json(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) responses.add(responses.GET, 'http://de.mo/file', body="INVALID", status=200) @@ -557,7 +626,7 @@ def test_download_logged_data_fail_invalidjson(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() with pytest.raises(OwletTemporaryCommunicationException) as info: @@ -567,10 +636,14 @@ def test_download_logged_data_fail_invalidjson(): @responses.activate -def test_download_logged_data_fail_incompletejson(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_download_logged_data_fail_incomplete_json(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) my_download_data = copy.deepcopy(DOWNLOAD_DATA) my_download_data['datapoint'] = {} @@ -584,7 +657,7 @@ def test_download_logged_data_fail_incompletejson(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() with pytest.raises(OwletTemporaryCommunicationException) as info: @@ -594,10 +667,14 @@ def test_download_logged_data_fail_incompletejson(): @responses.activate -def test_download_logged_data_fail_nodownload(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_download_logged_data_fail_no_download(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) responses.add(responses.GET, 'http://de.mo/file', json=DOWNLOAD_DATA, status=200) @@ -609,7 +686,7 @@ def test_download_logged_data_fail_nodownload(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() with pytest.raises(OwletTemporaryCommunicationException) as info: @@ -619,10 +696,14 @@ def test_download_logged_data_fail_nodownload(): @responses.activate -def test_download_logged_data_fail_nodownloadcode(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_download_logged_data_fail_no_download_code(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/dsns/c/properties', + responses.add(responses.GET, OwletAPI.base_properties_url + 'dsns/c/properties', json=DEVICE_ATTRIBUTES, status=200) responses.add(responses.GET, 'http://de.mo/file', json=DOWNLOAD_DATA, status=200) @@ -636,7 +717,7 @@ def test_download_logged_data_fail_nodownloadcode(): # Instantiate the device device = Owlet(api, DEVICE_PAYLOAD) - # Update the decice + # Update the device device.update() with pytest.raises(OwletTemporaryCommunicationException) as info: diff --git a/tests/test_owlet_api.py b/tests/test_owlet_api.py index 1712d2b..52b3cda 100644 --- a/tests/test_owlet_api.py +++ b/tests/test_owlet_api.py @@ -15,7 +15,12 @@ from owlet_api.owletexceptions import OwletNotInitializedException LOGIN_PAYLOAD = { - 'access_token': 'testtoken', + 'access_token': 'test_access_token', + 'idToken': 'test_id_token', + 'refreshToken': 'test_refresh_token', + 'refresh_token': 'test_refresh_token', + 'mini_token': 'test_min_token', + 'expiresIn': '3600', 'expires_in': 86400 } @@ -49,7 +54,11 @@ @responses.activate def test_login_ok(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) api = OwletAPI() @@ -60,16 +69,16 @@ def test_login_ok(): assert api._email == "test@test.de" assert api._password == "moped" - assert api._auth_token == "testtoken" + assert api._auth_token == "test_access_token" assert api._expiry_time > time.time() + 86400 - 1 assert api._expiry_time < time.time() + 86400 + 1 - assert api.get_auth_token() == "testtoken" + assert api.get_auth_token() == "test_access_token" @responses.activate def test_login_fail(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', - json=LOGIN_PAYLOAD, status=401) + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=400) api = OwletAPI() api.set_email("test@test.de") @@ -83,11 +92,91 @@ def test_login_fail(): assert api._password == "moped" assert api._auth_token == None assert api.get_auth_token() == None + +@responses.activate +def test_login_fail_step_1_api_key_bad(): + login_payload = { + "error": { + "details": [ + { + "reason": "API_KEY_INVALID", + } + ] + } + } + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=login_payload, status=400) + + api = OwletAPI() + api.set_email("test@test.de") + api.set_password("moped") + with pytest.raises(OwletPermanentCommunicationException) as info: + api.login() + + assert 'Login failed, bad API key.' in str(info.value) + assert api._email == "test@test.de" + assert api._password == "moped" + assert api._auth_token == None + assert api.get_auth_token() == None + +@responses.activate +def test_login_fail_step_1_username_bad(): + login_payload = { + "error": { + "details": [ + { + "reason": "EMAIL_NOT_FOUND" + } + ] + } + } + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=login_payload, status=400) + + api = OwletAPI() + api.set_email("test@test.de") + api.set_password("moped") + + with pytest.raises(OwletPermanentCommunicationException) as info: + api.login() + + assert 'Login failed, bad username' in str(info.value) + assert api._email == "test@test.de" + assert api._password == "moped" + assert api._auth_token == None + assert api.get_auth_token() == None + +@responses.activate +def test_login_fail_step_1_password_bad(): + login_payload = { + "error": { + "details": [ + { + "reason": "INVALID_PASSWORD" + } + ] + } + } + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=login_payload, status=400) + + api = OwletAPI() + api.set_email("test@test.de") + api.set_password("moped") + + with pytest.raises(OwletPermanentCommunicationException) as info: + api.login() + + assert 'Login failed, bad password' in str(info.value) + assert api._email == "test@test.de" + assert api._password == "moped" + assert api._auth_token == None + assert api.get_auth_token() == None @responses.activate -def test_login_fail_temporary(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_login_fail_step_1_temporary(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, json=LOGIN_PAYLOAD, status=500) api = OwletAPI() @@ -103,10 +192,9 @@ def test_login_fail_temporary(): assert api._auth_token == None assert api.get_auth_token() == None - @responses.activate -def test_login_fail_invalidjson(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_login_fail_step_1_invalid_json(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, body="broken", status=200) api = OwletAPI() @@ -116,19 +204,18 @@ def test_login_fail_invalidjson(): with pytest.raises(OwletTemporaryCommunicationException) as info: api.login() - assert 'Server did not send valid json' in str(info.value) + assert 'Server did not send valid json (Step 1 of 3)' in str(info.value) assert api._email == "test@test.de" assert api._password == "moped" assert api._auth_token == None assert api.get_auth_token() == None - @responses.activate -def test_login_fail_incompletejson(): +def test_login_fail_step_1_incomplete_json(): login_payload = { 'access_token': 'testtoken' } - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, json=login_payload, status=200) api = OwletAPI() @@ -138,15 +225,14 @@ def test_login_fail_incompletejson(): with pytest.raises(OwletTemporaryCommunicationException) as info: api.login() - assert 'Server did not send access token' in str(info.value) + assert 'Server did not send id token (Step 1 of 3)' in str(info.value) assert api._email == "test@test.de" assert api._password == "moped" assert api._auth_token == None assert api.get_auth_token() == None - @responses.activate -def test_login_fail_noconnection(): +def test_login_fail_step_1_no_connection(): api = OwletAPI() api.set_email("test@test.de") api.set_password("moped") @@ -160,10 +246,207 @@ def test_login_fail_noconnection(): assert api._auth_token == None assert api.get_auth_token() == None +@responses.activate +def test_login_fail_step_2_temporary(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=500) + + api = OwletAPI() + api.set_email("test@test.de") + api.set_password("moped") + + with pytest.raises(OwletTemporaryCommunicationException) as info: + api.login() + + assert 'Login request failed - status code (500) - (Step 2 of 3)' in str(info.value) + assert api._email == "test@test.de" + assert api._password == "moped" + assert api._auth_token == None + assert api.get_auth_token() == None + +@responses.activate +def test_login_fail_step_2_invalid_json(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + body="broken", status=200) + + api = OwletAPI() + api.set_email("test@test.de") + api.set_password("moped") + + with pytest.raises(OwletTemporaryCommunicationException) as info: + api.login() + + assert 'Server did not send valid json (Step 2 of 3)' in str(info.value) + assert api._email == "test@test.de" + assert api._password == "moped" + assert api._auth_token == None + assert api.get_auth_token() == None + +@responses.activate +def test_login_fail_step_2_incomplete_json(): + login_payload = { + } + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=login_payload, status=200) + + api = OwletAPI() + api.set_email("test@test.de") + api.set_password("moped") + + with pytest.raises(OwletTemporaryCommunicationException) as info: + api.login() + + assert 'Server did not send mini token (Step 2 of 3)' in str(info.value) + assert api._email == "test@test.de" + assert api._password == "moped" + assert api._auth_token == None + assert api.get_auth_token() == None + +@responses.activate +def test_login_fail_step_2_no_connection(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + + api = OwletAPI() + api.set_email("test@test.de") + api.set_password("moped") + + with pytest.raises(OwletTemporaryCommunicationException) as info: + api.login() + + assert 'Login request failed - no response (Step 2 of 3)' in str(info.value) + assert api._email == "test@test.de" + assert api._password == "moped" + assert api._auth_token == None + assert api.get_auth_token() == None + +@responses.activate +def test_login_fail_step_3_temporary(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, + json=LOGIN_PAYLOAD, status=500) + + api = OwletAPI() + api.set_email("test@test.de") + api.set_password("moped") + + with pytest.raises(OwletTemporaryCommunicationException) as info: + api.login() + + assert 'Login request failed - status code (500) - (Step 3 of 3)' in str(info.value) + assert api._email == "test@test.de" + assert api._password == "moped" + assert api._auth_token == None + assert api.get_auth_token() == None + +@responses.activate +def test_login_fail_step_3_app_id_or_app_secret_bad(): + login_payload = { + 'error': 'Could not find application' + } + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, + json=login_payload, status=404) + + api = OwletAPI() + api.set_email("test@test.de") + api.set_password("moped") + + with pytest.raises(OwletPermanentCommunicationException) as info: + api.login() + + assert 'login request failed - app_id or app_secret is bad (Step 3 of 3)' in str(info.value) + assert api._email == "test@test.de" + assert api._password == "moped" + assert api._auth_token == None + assert api.get_auth_token() == None + +@responses.activate +def test_login_fail_step_3_invalid_json(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, + body="broken", status=200) + + api = OwletAPI() + api.set_email("test@test.de") + api.set_password("moped") + + with pytest.raises(OwletTemporaryCommunicationException) as info: + api.login() + + assert 'Server did not send valid json (Step 3 of 3)' in str(info.value) + assert api._email == "test@test.de" + assert api._password == "moped" + assert api._auth_token == None + assert api.get_auth_token() == None + +@responses.activate +def test_login_fail_step_3_incomplete_json(): + login_payload = { + 'access_token': 'testtoken' + } + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, + json=login_payload, status=200) + + api = OwletAPI() + api.set_email("test@test.de") + api.set_password("moped") + + with pytest.raises(OwletTemporaryCommunicationException) as info: + api.login() + + assert 'Server did not send access token (Step 3 of 3)' in str(info.value) + assert api._email == "test@test.de" + assert api._password == "moped" + assert api._auth_token == None + assert api.get_auth_token() == None + +@responses.activate +def test_login_fail_step_3_no_connection(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + + api = OwletAPI() + api.set_email("test@test.de") + api.set_password("moped") + + with pytest.raises(OwletTemporaryCommunicationException) as info: + api.login() + + assert 'Login request failed - no response (Step 3 of 3)' in str(info.value) + assert api._email == "test@test.de" + assert api._password == "moped" + assert api._auth_token == None + assert api.get_auth_token() == None @responses.activate def test_get_auth_token_ok(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) api = OwletAPI() @@ -172,17 +455,25 @@ def test_get_auth_token_ok(): api.login() # If no exception occurred, everything seems to be fine - assert api.get_auth_token() == "testtoken" + assert api.get_auth_token() == "test_access_token" @responses.activate def test_get_auth_token_relogin(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) login_payload2 = copy.deepcopy(LOGIN_PAYLOAD) login_payload2['access_token'] = 'newtoken' - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=login_payload2, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=login_payload2, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=login_payload2, status=200) api = OwletAPI() @@ -192,7 +483,7 @@ def test_get_auth_token_relogin(): # Login happens at 2018-12-30 and lasts 1 day with freeze_time("2018-12-30"): api.login() - assert api.get_auth_token() == "testtoken" + assert api.get_auth_token() == "test_access_token" with freeze_time("2019-12-30"): @@ -206,7 +497,11 @@ def test_get_auth_token_fail(): @responses.activate def test_get_request_headers_ok(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) api = OwletAPI() @@ -216,7 +511,7 @@ def test_get_request_headers_ok(): assert api.get_request_headers()['Content-Type'] == "application/json" assert api.get_request_headers()['Accept'] == "application/json" - assert api.get_request_headers()['Authorization'] == "testtoken" + assert api.get_request_headers()['Authorization'] == "test_access_token" def test_get_request_headers_fail(): @@ -224,10 +519,14 @@ def test_get_request_headers_fail(): assert api.get_request_headers() == None -@patch('owlet_api.owletapi.Owlet.__init__', Mock(return_value=None)) +@patch('OwletAPI.owletapi.Owlet.__init__', Mock(return_value=None)) @responses.activate def test_get_devices_ok(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) api = OwletAPI() @@ -235,7 +534,7 @@ def test_get_devices_ok(): api.set_password("moped") api.login() - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/devices.json', json=DEVICES_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.base_properties_url + 'devices.json', json=DEVICES_PAYLOAD, status=200) api.get_devices() assert Owlet.__init__.called_once @@ -244,15 +543,19 @@ def test_get_devices_ok(): args, kwargs = Owlet.__init__.call_args instance, arguments = args assert instance is api - assert arguments == devices_payload[0]['device'] + assert arguments == DEVICES_PAYLOAD[0]['device'] # When calling get_devices again, no new instances of Owlet should be created api.get_devices() assert Owlet.__init__.called_once @responses.activate -def test_update_devices_fail_servererror(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_update_devices_fail_server_error(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) api = OwletAPI() @@ -260,7 +563,7 @@ def test_update_devices_fail_servererror(): api.set_password("moped") api.login() - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/devices.json', json=DEVICES_PAYLOAD, status=500) + responses.add(responses.GET, OwletAPI.base_properties_url + 'devices.json', json=DEVICES_PAYLOAD, status=500) with pytest.raises(OwletTemporaryCommunicationException) as info: api.update_devices() @@ -268,8 +571,12 @@ def test_update_devices_fail_servererror(): assert 'Server request failed - status code' in str(info.value) @responses.activate -def test_update_devices_fail_noresponse(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_update_devices_fail_no_response(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) api = OwletAPI() @@ -283,8 +590,12 @@ def test_update_devices_fail_noresponse(): assert 'Server request failed - no response' in str(info.value) @responses.activate -def test_update_devices_fail_invalidjson(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', +def test_update_devices_fail_invalid_json(): + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) api = OwletAPI() @@ -292,14 +603,14 @@ def test_update_devices_fail_invalidjson(): api.set_password("moped") api.login() - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/devices.json', body="invalid", status=200) + responses.add(responses.GET, OwletAPI.base_properties_url + 'devices.json', body="invalid", status=200) with pytest.raises(OwletTemporaryCommunicationException) as info: api.update_devices() assert 'Server did not send valid json' in str(info.value) -def test_update_devices_fail_noinit(): +def test_update_devices_fail_no_init(): api = OwletAPI() with pytest.raises(OwletNotInitializedException) as info: @@ -311,7 +622,11 @@ def test_update_devices_fail_noinit(): @patch('owlet_api.owlet.Owlet.get_update_interval', Mock(return_value=177)) @responses.activate def test_get_devices_ok(): - responses.add(responses.POST, 'https://user-field.aylanetworks.com/users/sign_in.json', + responses.add(responses.POST, OwletAPI.owlet_login_url + OwletAPI.google_API_key, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.owlet_login_token_provider_url, + json=LOGIN_PAYLOAD, status=200) + responses.add(responses.POST, OwletAPI.base_user_url, json=LOGIN_PAYLOAD, status=200) api = OwletAPI() @@ -319,7 +634,7 @@ def test_get_devices_ok(): api.set_password("moped") api.login() - responses.add(responses.GET, 'https://ads-field.aylanetworks.com/apiv1/devices.json', json=DEVICES_PAYLOAD, status=200) + responses.add(responses.GET, OwletAPI.base_properties_url + 'devices.json', json=DEVICES_PAYLOAD, status=200) api.get_devices() assert api.get_update_interval() == 177