def update_devices(): """Ping Alexa API to identify all devices, bluetooth, and last called device. This will add new devices and services when discovered. By default this runs every SCAN_INTERVAL seconds unless another method calls it. While throttled at MIN_TIME_BETWEEN_SCANS, care should be taken to reduce the number of runs to avoid flooding. Slow changing states should be checked here instead of in spawned components like media_player since this object is one per account. Each AlexaAPI call generally results in one webpage request. """ from alexapy import AlexaAPI email = login_obj.get_email() _LOGGER.debug("Updating devices for %s", hide_email(email)) devices = AlexaAPI.get_devices(login_obj) bluetooth = AlexaAPI.get_bluetooth(login_obj) last_called = AlexaAPI.get_last_device_serial(login_obj) _LOGGER.debug("Found %s devices, %s bluetooth, last_called: %s", len(devices), len(bluetooth), last_called) if ((devices is None or bluetooth is None) and len(_CONFIGURING) == 0): _LOGGER.debug("Alexa API disconnected; attempting to relogin") login_obj.login_with_cookie() testLoginStatus(hass, config, login_obj, setup_platform_callback) new_alexa_clients = [] # list of newly discovered device jsons available_client_ids = [] # list of known serial numbers for device in devices: if include and device['accountName'] not in include: continue elif exclude and device['accountName'] in exclude: continue for b_state in bluetooth['bluetoothStates']: if device['serialNumber'] == b_state['deviceSerialNumber']: device['bluetooth_state'] = b_state available_client_ids.append(device['serialNumber']) (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['devices'] ['media_player'][device['serialNumber']]) = device if device['serialNumber'] not in alexa_clients: new_alexa_clients.append(device) if new_alexa_clients: for component in ALEXA_COMPONENTS: load_platform(hass, component, DOMAIN, {}, config) # Process last_called data to fire events stored_data = hass.data[DATA_ALEXAMEDIA]['accounts'][email] if (('last_called' in stored_data and last_called != stored_data['last_called']) or ('last_called' not in stored_data and last_called is not None)): hass.bus.fire('{}_{}'.format(DOMAIN, email), {'last_called_change': last_called}) (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['last_called'] ) = AlexaAPI.get_last_device_serial(login_obj)
async def async_update_data(): """Fetch data from API endpoint. This is the place to pre-process the data to lookup tables so entities can quickly look up their data. This will ping Alexa API to identify all devices, bluetooth, and the last called device. This will add new devices and services when discovered. By default this runs every SCAN_INTERVAL seconds unless another method calls it. if websockets is connected, it will increase the delay 10-fold between updates. While throttled at MIN_TIME_BETWEEN_SCANS, care should be taken to reduce the number of runs to avoid flooding. Slow changing states should be checked here instead of in spawned components like media_player since this object is one per account. Each AlexaAPI call generally results in two webpage requests. """ from alexapy import AlexaAPI email: Text = login_obj.email if email not in hass.data[DATA_ALEXAMEDIA]["accounts"]: return existing_serials = _existing_serials(hass, login_obj) existing_entities = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "entities"]["media_player"].values() auth_info = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( "auth_info") new_devices = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "new_devices"] devices = {} bluetooth = {} preferences = {} dnd = {} raw_notifications = {} tasks = [ AlexaAPI.get_devices(login_obj), AlexaAPI.get_bluetooth(login_obj), AlexaAPI.get_device_preferences(login_obj), AlexaAPI.get_dnd_state(login_obj), AlexaAPI.get_notifications(login_obj), ] if new_devices: tasks.append(AlexaAPI.get_authentication(login_obj)) try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with async_timeout.timeout(10): if new_devices: ( devices, bluetooth, preferences, dnd, raw_notifications, auth_info, ) = await asyncio.gather(*tasks) else: ( devices, bluetooth, preferences, dnd, raw_notifications, ) = await asyncio.gather(*tasks) _LOGGER.debug( "%s: Found %s devices, %s bluetooth", hide_email(email), len(devices) if devices is not None else "", len(bluetooth.get("bluetoothStates", [])) if bluetooth is not None else "", ) if (devices is None or bluetooth is None) and not (hass.data[ DATA_ALEXAMEDIA]["accounts"][email]["configurator"]): raise AlexapyLoginError() except (AlexapyLoginError, RuntimeError, JSONDecodeError): _LOGGER.debug("%s: Alexa API disconnected; attempting to relogin", hide_email(email)) await login_obj.login_with_cookie() await test_login_status(hass, config_entry, login_obj, setup_alexa) return except BaseException as err: raise UpdateFailed(f"Error communicating with API: {err}") await process_notifications(login_obj, raw_notifications) # Process last_called data to fire events await update_last_called(login_obj) new_alexa_clients = [] # list of newly discovered device names exclude_filter = [] include_filter = [] for device in devices: serial = device["serialNumber"] dev_name = device["accountName"] if include and dev_name not in include: include_filter.append(dev_name) if "appDeviceList" in device: for app in device["appDeviceList"]: (hass.data[DATA_ALEXAMEDIA]["accounts"][email] ["excluded"][app["serialNumber"]]) = device hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ serial] = device continue elif exclude and dev_name in exclude: exclude_filter.append(dev_name) if "appDeviceList" in device: for app in device["appDeviceList"]: (hass.data[DATA_ALEXAMEDIA]["accounts"][email] ["excluded"][app["serialNumber"]]) = device hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ serial] = device continue if "bluetoothStates" in bluetooth: for b_state in bluetooth["bluetoothStates"]: if serial == b_state["deviceSerialNumber"]: device["bluetooth_state"] = b_state break if "devicePreferences" in preferences: for dev in preferences["devicePreferences"]: if dev["deviceSerialNumber"] == serial: device["locale"] = dev["locale"] device["timeZoneId"] = dev["timeZoneId"] _LOGGER.debug( "%s: Locale %s timezone %s", dev_name, device["locale"], device["timeZoneId"], ) break if "doNotDisturbDeviceStatusList" in dnd: for dev in dnd["doNotDisturbDeviceStatusList"]: if dev["deviceSerialNumber"] == serial: device["dnd"] = dev["enabled"] _LOGGER.debug("%s: DND %s", dev_name, device["dnd"]) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "devices"]["switch"].setdefault( serial, {"dnd": True}) break hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "auth_info"] = device["auth_info"] = auth_info hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ "media_player"][serial] = device if serial not in existing_serials: new_alexa_clients.append(dev_name) elif serial in existing_entities: await hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "entities"]["media_player"].get(serial).refresh( device, no_api=True) _LOGGER.debug( "%s: Existing: %s New: %s;" " Filtered out by not being in include: %s " "or in exclude: %s", hide_email(email), list(existing_entities), new_alexa_clients, include_filter, exclude_filter, ) if new_alexa_clients: cleaned_config = config.copy() cleaned_config.pop(CONF_PASSWORD, None) # CONF_PASSWORD contains sensitive info which is no longer needed for component in ALEXA_COMPONENTS: _LOGGER.debug("Loading %s", component) hass.async_add_job( hass.config_entries.async_forward_entry_setup( config_entry, component)) hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] = False
def update_devices(): """Ping Alexa API to identify all devices, bluetooth, and last called device. This will add new devices and services when discovered. By default this runs every SCAN_INTERVAL seconds unless another method calls it. if websockets is connected, it will return immediately unless 'new_devices' has been set to True. While throttled at MIN_TIME_BETWEEN_SCANS, care should be taken to reduce the number of runs to avoid flooding. Slow changing states should be checked here instead of in spawned components like media_player since this object is one per account. Each AlexaAPI call generally results in two webpage requests. """ from alexapy import AlexaAPI existing_serials = (hass.data[DATA_ALEXAMEDIA]['accounts'][email] ['entities']['media_player'].keys()) existing_entities = (hass.data[DATA_ALEXAMEDIA]['accounts'][email] ['entities']['media_player'].values()) if (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['websocket'] and not (hass.data[DATA_ALEXAMEDIA]['accounts'][email] ['new_devices'])): return hass.data[DATA_ALEXAMEDIA]['accounts'][email]['new_devices'] = False devices = AlexaAPI.get_devices(login_obj) bluetooth = AlexaAPI.get_bluetooth(login_obj) preferences = AlexaAPI.get_device_preferences(login_obj) _LOGGER.debug("%s: Found %s devices, %s bluetooth", hide_email(email), len(devices) if devices is not None else '', len(bluetooth) if bluetooth is not None else '') if ((devices is None or bluetooth is None) and not hass.data[DATA_ALEXAMEDIA]['accounts'][email]['config']): _LOGGER.debug("Alexa API disconnected; attempting to relogin") login_obj.login_with_cookie() test_login_status(hass, config, login_obj, setup_platform_callback) return new_alexa_clients = [] # list of newly discovered device names excluded = [] included = [] for device in devices: if include and device['accountName'] not in include: included.append(device['accountName']) if 'appDeviceList' in device: for app in device['appDeviceList']: (hass.data[DATA_ALEXAMEDIA]['accounts'][email] ['excluded'][app['serialNumber']]) = device (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['excluded'][ device['serialNumber']]) = device continue elif exclude and device['accountName'] in exclude: excluded.append(device['accountName']) if 'appDeviceList' in device: for app in device['appDeviceList']: (hass.data[DATA_ALEXAMEDIA]['accounts'][email] ['excluded'][app['serialNumber']]) = device (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['excluded'][ device['serialNumber']]) = device continue for b_state in bluetooth['bluetoothStates']: if device['serialNumber'] == b_state['deviceSerialNumber']: device['bluetooth_state'] = b_state for dev in preferences['devicePreferences']: if dev['deviceSerialNumber'] == device['serialNumber']: device['locale'] = dev['locale'] _LOGGER.debug("Locale %s found for %s", device['locale'], hide_serial(device['serialNumber'])) (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['devices'] ['media_player'][device['serialNumber']]) = device if device['serialNumber'] not in existing_serials: new_alexa_clients.append(device['accountName']) _LOGGER.debug( "%s: Existing: %s New: %s;" " Filtered by: include_devices: %s exclude_devices:%s", hide_email(email), list(existing_entities), new_alexa_clients, included, excluded) if new_alexa_clients: for component in ALEXA_COMPONENTS: load_platform(hass, component, DOMAIN, {CONF_NAME: DOMAIN}, config) # Process last_called data to fire events update_last_called(login_obj)
async def async_update_data(): """Fetch data from API endpoint. This is the place to pre-process the data to lookup tables so entities can quickly look up their data. This will ping Alexa API to identify all devices, bluetooth, and the last called device. This will add new devices and services when discovered. By default this runs every SCAN_INTERVAL seconds unless another method calls it. if websockets is connected, it will increase the delay 10-fold between updates. While throttled at MIN_TIME_BETWEEN_SCANS, care should be taken to reduce the number of runs to avoid flooding. Slow changing states should be checked here instead of in spawned components like media_player since this object is one per account. Each AlexaAPI call generally results in two webpage requests. """ email = config.get(CONF_EMAIL) login_obj = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"] if (email not in hass.data[DATA_ALEXAMEDIA]["accounts"] or not login_obj.status.get("login_successful") or login_obj.session.closed or login_obj.close_requested): return existing_serials = _existing_serials(hass, login_obj) existing_entities = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "entities"]["media_player"].values() auth_info = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( "auth_info") new_devices = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "new_devices"] devices = {} bluetooth = {} preferences = {} dnd = {} raw_notifications = {} tasks = [ AlexaAPI.get_devices(login_obj), AlexaAPI.get_bluetooth(login_obj), AlexaAPI.get_device_preferences(login_obj), AlexaAPI.get_dnd_state(login_obj), ] if new_devices: tasks.append(AlexaAPI.get_authentication(login_obj)) try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with async_timeout.timeout(30): if new_devices: ( devices, bluetooth, preferences, dnd, auth_info, ) = await asyncio.gather(*tasks) else: ( devices, bluetooth, preferences, dnd, ) = await asyncio.gather(*tasks) _LOGGER.debug( "%s: Found %s devices, %s bluetooth", hide_email(email), len(devices) if devices is not None else "", len(bluetooth.get("bluetoothStates", [])) if bluetooth is not None else "", ) await process_notifications(login_obj, raw_notifications) # Process last_called data to fire events await update_last_called(login_obj) except (AlexapyLoginError, JSONDecodeError): _LOGGER.debug( "%s: Alexa API disconnected; attempting to relogin : status %s", hide_email(email), login_obj.status, ) if login_obj.status: hass.bus.async_fire( "alexa_media_relogin_required", event_data={ "email": hide_email(email), "url": login_obj.url }, ) return except BaseException as err: raise UpdateFailed(f"Error communicating with API: {err}") new_alexa_clients = [] # list of newly discovered device names exclude_filter = [] include_filter = [] for device in devices: serial = device["serialNumber"] dev_name = device["accountName"] if include and dev_name not in include: include_filter.append(dev_name) if "appDeviceList" in device: for app in device["appDeviceList"]: (hass.data[DATA_ALEXAMEDIA]["accounts"][email] ["excluded"][app["serialNumber"]]) = device hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ serial] = device continue if exclude and dev_name in exclude: exclude_filter.append(dev_name) if "appDeviceList" in device: for app in device["appDeviceList"]: (hass.data[DATA_ALEXAMEDIA]["accounts"][email] ["excluded"][app["serialNumber"]]) = device hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"][ serial] = device continue if (dev_name not in include_filter and device.get("capabilities") and not any( x in device["capabilities"] for x in ["MUSIC_SKILL", "TIMERS_AND_ALARMS", "REMINDERS"])): # skip devices without music or notification skill _LOGGER.debug("Excluding %s for lacking capability", dev_name) continue if "bluetoothStates" in bluetooth: for b_state in bluetooth["bluetoothStates"]: if serial == b_state["deviceSerialNumber"]: device["bluetooth_state"] = b_state break if "devicePreferences" in preferences: for dev in preferences["devicePreferences"]: if dev["deviceSerialNumber"] == serial: device["locale"] = dev["locale"] device["timeZoneId"] = dev["timeZoneId"] _LOGGER.debug( "%s: Locale %s timezone %s", dev_name, device["locale"], device["timeZoneId"], ) break if "doNotDisturbDeviceStatusList" in dnd: for dev in dnd["doNotDisturbDeviceStatusList"]: if dev["deviceSerialNumber"] == serial: device["dnd"] = dev["enabled"] _LOGGER.debug("%s: DND %s", dev_name, device["dnd"]) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "devices"]["switch"].setdefault( serial, {"dnd": True}) break hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "auth_info"] = device["auth_info"] = auth_info hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ "media_player"][serial] = device if serial not in existing_serials: new_alexa_clients.append(dev_name) elif (serial in existing_serials and hass.data[DATA_ALEXAMEDIA] ["accounts"][email]["entities"]["media_player"].get(serial) and hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"] ["media_player"].get(serial).enabled): await hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "entities"]["media_player"].get(serial).refresh( device, skip_api=True) _LOGGER.debug( "%s: Existing: %s New: %s;" " Filtered out by not being in include: %s " "or in exclude: %s", hide_email(email), list(existing_entities), new_alexa_clients, include_filter, exclude_filter, ) if new_alexa_clients: cleaned_config = config.copy() cleaned_config.pop(CONF_PASSWORD, None) # CONF_PASSWORD contains sensitive info which is no longer needed for component in ALEXA_COMPONENTS: entry_setup = len(hass.data[DATA_ALEXAMEDIA]["accounts"][email] ["entities"][component]) if not entry_setup: _LOGGER.debug("Loading config entry for %s", component) hass.async_add_job( hass.config_entries.async_forward_entry_setup( config_entry, component)) else: _LOGGER.debug("Loading %s", component) hass.async_create_task( async_load_platform( hass, component, DOMAIN, { CONF_NAME: DOMAIN, "config": cleaned_config }, cleaned_config, )) hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] = False await login_obj.save_cookiefile() if login_obj.access_token: hass.config_entries.async_update_entry( config_entry, data={ **config_entry.data, CONF_OAUTH: { "access_token": login_obj.access_token, "refresh_token": login_obj.refresh_token, "expires_in": login_obj.expires_in, }, }, )