#!/usr/bin/python3
#
# Dependencies (Linux):
# $ pip3 install python-jwt gcloud sseclient requests_toolbelt firebase
#
# Extra dependencies (Windows 10):
# $ pip3 install pycryptodome

import sys, os, time, requests, json, firebase

sess = None
url_props = None
url_activate = None
#headers = {'Content-Type' : 'application/json', 'Accept' : 'application/json'}
headers = {}
auth_token = None
expire_time = 0
dsn = None
owlet_region = 'world'
region_config = {
    'world': {
        'url_mini': 'https://ayla-sso.owletdata.com/mini/',
        'url_signin': 'https://user-field-1a2039d9.aylanetworks.com/api/v1/token_sign_in',
        'url_base': 'https://ads-field-1a2039d9.aylanetworks.com/apiv1',
        'apiKey': 'AIzaSyCsDZ8kWxQuLJAMVnmEhEkayH1TSxKXfGA',
        'databaseURL': 'https://owletcare-prod.firebaseio.com',
        'storage_bucket': 'owletcare-prod.appspot.com',
        'app_id': 'sso-prod-3g-id',
        'app_secret': 'sso-prod-UEjtnPCtFfjdwIwxqnC0OipxRFU',
    },
    'europe': {
        'url_mini': 'https://ayla-sso.eu.owletdata.com/mini/',
        'url_signin': 'https://user-field-eu-1a2039d9.aylanetworks.com/api/v1/token_sign_in',
        'url_base': 'https://ads-field-eu-1a2039d9.aylanetworks.com/apiv1',
        'apiKey': 'AIzaSyDm6EhV70wudwN3iOSq3vTjtsdGjdFLuuM',
        'databaseURL': 'https://owletcare-prod-eu.firebaseio.com',
        'storage_bucket': 'owletcare-prod-eu.appspot.com',
        'app_id': 'OwletCare-Android-EU-fw-id',
        'app_secret': 'OwletCare-Android-EU-JKupMPBoj_Npce_9a95Pc8Qo0Mw',
    }
}

class FatalError(Exception):
    pass

def log(s):
    sys.stderr.write(s + '\n')
    sys.stderr.flush()

def record(s):
    sys.stdout.write(s + '\n')
    sys.stdout.flush()

def login():
    global auth_token, expire_time, owlet_region
    try:
        owlet_user, owlet_pass = os.environ['OWLET_USER'], os.environ['OWLET_PASS']
    except KeyError as e:
        raise FatalError("OWLET_USER or OWLET_PASS env var is not defined")
    
    if 'OWLET_REGION' in os.environ:
        owlet_region = os.environ['OWLET_REGION']

    if owlet_region not in region_config:
        raise FatalError("OWLET_REGION env var '{}' not recognised - must be one of {}".format(
            owlet_region, region_config.keys()))

    if auth_token is not None and (expire_time > time.time()):
        return
    log('Logging in')
    # authenticate against Firebase, get the JWT
    config = {
            "apiKey": region_config[owlet_region]['apiKey'],
            "databaseURL": region_config[owlet_region]['databaseURL'],
            "storageBucket": region_config[owlet_region]['storage_bucket'],
            "authDomain": None,
            }
    auth = firebase.Firebase(config).auth()
    user = auth.sign_in_with_email_and_password(owlet_user, owlet_pass)
    jwt = user['idToken']
    # authenticate against owletdata.com, get the mini_token
    r = requests.get(region_config[owlet_region]
                     ['url_mini'], headers={'Authorization': jwt})
    r.raise_for_status()
    mini_token = r.json()['mini_token']
    # authenticate against Ayla, get the access_token
    r = requests.post(region_config[owlet_region]['url_signin'], json={
                "app_id": region_config[owlet_region]['app_id'],
                "app_secret": region_config[owlet_region]['app_secret'],
                "provider": "owl_id",
                "token": mini_token,
                })
    r.raise_for_status()
    auth_token = r.json()['access_token']
    # we will re-auth 60 seconds before the token expires
    expire_time = time.time() + r.json()['expires_in'] - 60
    headers['Authorization'] = 'auth_token ' + auth_token
    log('Auth token %s' % auth_token)

def fetch_dsn():
    global dsn, url_props, url_activate
    if dsn is None:
        log('Getting DSN')
        r = sess.get(region_config[owlet_region]
                     ['url_base'] + '/devices.json', headers=headers)
        r.raise_for_status()
        devs = r.json()
        if len(devs) < 1:
            raise FatalError('Found zero Owlet monitors')
        dsn = devs[0]['device']['dsn']
        log('Found Owlet monitor device serial number %s' % dsn)
        url_props = region_config[owlet_region]['url_base'] + \
            '/dsns/' + dsn + '/properties.json'
        url_activate = region_config[owlet_region]['url_base'] + \
            '/dsns/' + dsn + '/properties/APP_ACTIVE/datapoints.json'

def reactivate():
    payload = { "datapoint": { "metadata": {}, "value": 1 } }
    r = sess.post(url_activate, json=payload, headers=headers)
    r.raise_for_status()

def fetch_props():
    # Ayla cloud API data is updated only when APP_ACTIVE periodically reset to 1.
    reactivate()
    my_props = {}
    r = sess.get(url_props, headers=headers)
    r.raise_for_status()
    props = r.json()
    for prop in props:
        n = prop['property']['name']
        del(prop['property']['name'])
        my_props[n] = prop['property']
    return my_props

def record_vitals(p):
    charge_status = p['CHARGE_STATUS']['value']
    base_station_on = p['BASE_STATION_ON']['value']
    heart = "%d" % p['HEART_RATE']['value']
    oxy = "%d" % p['OXYGEN_LEVEL']['value']
    mov = "wiggling" if p['MOVEMENT']['value'] else "still"
    disp = "%d, " % time.time()
    if charge_status >= 1:
        disp += "sock charging (%d)" % charge_status
        # base_station_on is (always?) 1 in this case
    elif charge_status == 0:
        if base_station_on == 0:
            # sock was unplugged, but user did not turn on the base station.
            # heart and oxygen levels appear to be reported, but we can't
            # yet assume the sock was placed on the baby's foot.
            disp += "sock not charging, base station off"
        elif base_station_on == 1:
            # base station was intentionally turned on, the sock is presumably
            # on the baby's foot, so we can trust heart and oxygen levels
            disp += heart + ", " + oxy + ", " + mov
            record(disp)
        else:
            raise FatalError("Unexpected base_station_on=%d" % base_station_on)
    log("Status: " + disp)

def loop():
    global sess
    sess = requests.session()
    while True:
        try:
            login()
            fetch_dsn()
            record_vitals(fetch_props())
            time.sleep(10)
        except requests.exceptions.RequestException as e:
            log('Network error: %s' % e)
            time.sleep(1)
            sess = requests.session()

def main():
    try:
        loop()
    except FatalError as e:
        sys.stderr.write('%s\n' % e)
        sys.exit(1)

if __name__ == "__main__":
    main()
