async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None) -> bool: """Set up the Alexa alarm control panel platform.""" devices = [] # type: List[AlexaAlarmControlPanel] account = config[CONF_EMAIL] if config else discovery_info["config"][ CONF_EMAIL] include_filter = config.get(CONF_INCLUDE_DEVICES, []) exclude_filter = config.get(CONF_EXCLUDE_DEVICES, []) account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] guard_media_players = {} for key, device in account_dict["devices"]["media_player"].items(): if key not in account_dict["entities"]["media_player"]: _LOGGER.debug( "%s: Media player %s not loaded yet; delaying load", hide_email(account), hide_serial(key), ) raise ConfigEntryNotReady if "GUARD_EARCON" in device["capabilities"]: guard_media_players[key] = account_dict["entities"][ "media_player"][key] if "alarm_control_panel" not in (account_dict["entities"]): (hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"] ["alarm_control_panel"]) = {} alexa_client: AlexaAlarmControlPanel = AlexaAlarmControlPanel( account_dict["login_obj"], guard_media_players) await alexa_client.init() if not (alexa_client and alexa_client.unique_id): _LOGGER.debug( "%s: Skipping creation of uninitialized device: %s", hide_email(account), alexa_client, ) elif alexa_client.unique_id not in ( account_dict["entities"]["alarm_control_panel"]): devices.append(alexa_client) (hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"] ["alarm_control_panel"][alexa_client.unique_id]) = alexa_client else: _LOGGER.debug("%s: Skipping already added device: %s", hide_email(account), alexa_client) return await add_devices( hide_email(account), devices, add_devices_callback, include_filter, exclude_filter, )
async def update_last_called(login_obj, last_called=None): """Update the last called device for the login_obj. This will store the last_called in hass.data and also fire an event to notify listeners. """ from alexapy import AlexaAPI if not last_called: last_called = await AlexaAPI.get_last_device_serial(login_obj) _LOGGER.debug("%s: Updated last_called: %s", hide_email(email), hide_serial(last_called)) 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)): _LOGGER.debug( "%s: last_called changed: %s to %s", hide_email(email), hide_serial(stored_data['last_called'] if 'last_called' in stored_data else None), hide_serial(last_called)) hass.bus.async_fire(f'{DOMAIN}_{hide_email(email)}'[0:32], {'last_called_change': last_called}) (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['last_called'] ) = last_called
def parse_value_from_coordinator( coordinator: DataUpdateCoordinator, entity_id: Text, namespace: Text, name: Text, since: Optional[datetime] = None, ) -> Any: """Parse out values from coordinator for Alexa Entities.""" if coordinator.data and entity_id in coordinator.data: for cap_state in coordinator.data[entity_id]: if (cap_state.get("namespace") == namespace and cap_state.get("name") == name): if is_cap_state_still_acceptable(cap_state, since): return cap_state.get("value") else: _LOGGER.debug( "Coordinator data for %s is too old to be returned.", hide_serial(entity_id), ) return None else: _LOGGER.debug("Coordinator has no data for %s", hide_serial(entity_id)) return None
async def update_bluetooth_state(login_obj, device_serial): """Update the bluetooth state on ws bluetooth event.""" bluetooth = await AlexaAPI.get_bluetooth(login_obj) device = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][ "media_player" ][device_serial] if "bluetoothStates" in bluetooth: for b_state in bluetooth["bluetoothStates"]: if device_serial == b_state["deviceSerialNumber"]: # _LOGGER.debug("%s: setting value for: %s to %s", # hide_email(email), # hide_serial(device_serial), # hide_serial(b_state)) device["bluetooth_state"] = b_state return device["bluetooth_state"] _LOGGER.debug( "%s: get_bluetooth for: %s failed with %s", hide_email(email), hide_serial(device_serial), hide_serial(bluetooth), ) return None
async def update_last_called(login_obj, last_called=None, force=False): """Update the last called device for the login_obj. This will store the last_called in hass.data and also fire an event to notify listeners. """ if not last_called or not (last_called and last_called.get("summary")): try: last_called = await AlexaAPI.get_last_device_serial(login_obj) except TypeError: _LOGGER.debug( "%s: Error updating last_called: %s", hide_email(email), hide_serial(last_called), ) return _LOGGER.debug("%s: Updated last_called: %s", hide_email(email), hide_serial(last_called)) stored_data = hass.data[DATA_ALEXAMEDIA]["accounts"][email] if (force or "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): _LOGGER.debug( "%s: last_called changed: %s to %s", hide_email(email), hide_serial(stored_data["last_called"] if "last_called" in stored_data else None), hide_serial(last_called), ) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"last_called_change": last_called}, ) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "last_called"] = last_called
async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up the Alexa sensor platform.""" devices: List[LightEntity] = [] account = config[CONF_EMAIL] if config else discovery_info["config"][ CONF_EMAIL] account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] include_filter = config.get(CONF_INCLUDE_DEVICES, []) exclude_filter = config.get(CONF_EXCLUDE_DEVICES, []) coordinator = account_dict["coordinator"] hue_emulated_enabled = "emulated_hue" in hass.config.as_dict().get( "components", set()) light_entities = account_dict.get("devices", {}).get("light", []) if light_entities and account_dict["options"].get( CONF_EXTENDED_ENTITY_DISCOVERY): for le in light_entities: if not (le["is_hue_v1"] and hue_emulated_enabled): _LOGGER.debug( "Creating entity %s for a light with name %s", hide_serial(le["id"]), le["name"], ) light = AlexaLight(coordinator, account_dict["login_obj"], le) account_dict["entities"]["light"].append(light) devices.append(light) else: _LOGGER.debug( "Light '%s' has not been added because it may originate from emulated_hue", le["name"], ) if devices: await coordinator.async_refresh() return await add_devices( hide_email(account), devices, add_devices_callback, include_filter, exclude_filter, )
async def ws_handler(message_obj): """Handle websocket messages. This allows push notifications from Alexa to update last_called and media state. """ command = (message_obj.json_payload['command'] if isinstance(message_obj.json_payload, dict) and 'command' in message_obj.json_payload else None) json_payload = (message_obj.json_payload['payload'] if isinstance(message_obj.json_payload, dict) and 'payload' in message_obj.json_payload else None) existing_serials = (hass.data[DATA_ALEXAMEDIA]['accounts'][email] ['entities']['media_player'].keys() if 'entities' in (hass.data[DATA_ALEXAMEDIA]['accounts'][email]) else []) if command and json_payload: _LOGGER.debug("%s: Received websocket command: %s : %s", hide_email(email), command, hide_serial(json_payload)) serial = None if ('dopplerId' in json_payload and 'deviceSerialNumber' in json_payload['dopplerId']): serial = (json_payload['dopplerId']['deviceSerialNumber']) elif ('key' in json_payload and 'entryId' in json_payload['key'] and json_payload['key']['entryId'].find('#') != -1): serial = (json_payload['key']['entryId']).split('#')[2] else: serial = None if command == 'PUSH_ACTIVITY': # Last_Alexa Updated last_called = { 'serialNumber': serial, 'timestamp': json_payload['timestamp'] } if (serial and serial in existing_serials): await update_last_called(login_obj, last_called) hass.bus.async_fire(f'{DOMAIN}_{hide_email(email)}'[0:32], {'push_activity': json_payload}) elif command == 'PUSH_AUDIO_PLAYER_STATE': # Player update if (serial and serial in existing_serials): _LOGGER.debug("Updating media_player: %s", hide_serial(json_payload)) hass.bus.async_fire(f'{DOMAIN}_{hide_email(email)}'[0:32], {'player_state': json_payload}) elif command == 'PUSH_VOLUME_CHANGE': # Player volume update if (serial and serial in existing_serials): _LOGGER.debug("Updating media_player volume: %s", hide_serial(json_payload)) hass.bus.async_fire(f'{DOMAIN}_{hide_email(email)}'[0:32], {'player_state': json_payload}) elif command == 'PUSH_DOPPLER_CONNECTION_CHANGE': # Player availability update if (serial and serial in existing_serials): _LOGGER.debug("Updating media_player availability %s", hide_serial(json_payload)) hass.bus.async_fire(f'{DOMAIN}_{hide_email(email)}'[0:32], {'player_state': json_payload}) elif command == 'PUSH_BLUETOOTH_STATE_CHANGE': # Player bluetooth update bt_event = json_payload['bluetoothEvent'] bt_success = json_payload['bluetoothEventSuccess'] if (serial and serial in existing_serials and bt_success and bt_event and bt_event in ['DEVICE_CONNECTED', 'DEVICE_DISCONNECTED']): _LOGGER.debug("Updating media_player bluetooth %s", hide_serial(json_payload)) bluetooth_state = await update_bluetooth_state( login_obj, serial) # _LOGGER.debug("bluetooth_state %s", # hide_serial(bluetooth_state)) if bluetooth_state: hass.bus.async_fire( f'{DOMAIN}_{hide_email(email)}'[0:32], {'bluetooth_change': bluetooth_state}) elif command == 'PUSH_MEDIA_QUEUE_CHANGE': # Player availability update if (serial and serial in existing_serials): _LOGGER.debug("Updating media_player queue %s", hide_serial(json_payload)) hass.bus.async_fire(f'{DOMAIN}_{hide_email(email)}'[0:32], {'queue_state': json_payload}) elif command == 'PUSH_NOTIFICATION_CHANGE': # Player update await process_notifications(login_obj) if (serial and serial in existing_serials): _LOGGER.debug("Updating mediaplayer notifications: %s", hide_serial(json_payload)) hass.bus.async_fire(f'{DOMAIN}_{hide_email(email)}'[0:32], {'notification_update': json_payload}) if (serial and serial not in existing_serials and serial not in (hass.data[DATA_ALEXAMEDIA]['accounts'] [email]['excluded'].keys())): _LOGGER.debug("Discovered new media_player %s", serial) (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['new_devices'] ) = True await update_devices(login_obj, no_throttle=True)
async def update_devices(login_obj): """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 email: Text = login_obj.email if email not in hass.data[DATA_ALEXAMEDIA]['accounts']: return existing_serials = (hass.data[DATA_ALEXAMEDIA]['accounts'][email] ['entities']['media_player'].keys() if 'entities' in (hass.data[DATA_ALEXAMEDIA]['accounts'][email]) else []) existing_entities = (hass.data[DATA_ALEXAMEDIA]['accounts'][email] ['entities']['media_player'].values()) if ('websocket' in hass.data[DATA_ALEXAMEDIA]['accounts'][email] and 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 try: auth_info = await AlexaAPI.get_authentication(login_obj) devices = await AlexaAPI.get_devices(login_obj) bluetooth = await AlexaAPI.get_bluetooth(login_obj) preferences = await AlexaAPI.get_device_preferences(login_obj) dnd = await AlexaAPI.get_dnd_state(login_obj) raw_notifications = await AlexaAPI.get_notifications(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] ['configurator'])): raise AlexapyLoginError() except (AlexapyLoginError, RuntimeError): _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_platform_callback) return new_alexa_clients = [] # list of newly discovered device names exclude_filter = [] include_filter = [] for device in devices: if include and device['accountName'] not in include: include_filter.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: exclude_filter.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 if 'bluetoothStates' in bluetooth: for b_state in bluetooth['bluetoothStates']: if device['serialNumber'] == b_state['deviceSerialNumber']: device['bluetooth_state'] = b_state if 'devicePreferences' in preferences: 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'])) if 'doNotDisturbDeviceStatusList' in dnd: for dev in dnd['doNotDisturbDeviceStatusList']: if dev['deviceSerialNumber'] == device['serialNumber']: device['dnd'] = dev['enabled'] _LOGGER.debug("DND %s found for %s", device['dnd'], hide_serial(device['serialNumber'])) device['auth_info'] = auth_info (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 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: if component == "notify": hass.async_create_task( async_load_platform(hass, component, DOMAIN, { CONF_NAME: DOMAIN, "config": cleaned_config }, config)) else: hass.async_add_job( hass.config_entries.async_forward_entry_setup( config_entry, component)) await process_notifications(login_obj, raw_notifications) # Process last_called data to fire events await update_last_called(login_obj) async_call_later( hass, scan_interval, lambda _: hass.async_create_task( update_devices(login_obj, no_throttle=True)))
async def ws_handler(message_obj): """Handle websocket messages. This allows push notifications from Alexa to update last_called and media state. """ command = (message_obj.json_payload["command"] if isinstance(message_obj.json_payload, dict) and "command" in message_obj.json_payload else None) json_payload = (message_obj.json_payload["payload"] if isinstance(message_obj.json_payload, dict) and "payload" in message_obj.json_payload else None) existing_serials = _existing_serials(hass, login_obj) seen_commands = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_commands"] if command and json_payload: _LOGGER.debug( "%s: Received websocket command: %s : %s", hide_email(email), command, hide_serial(json_payload), ) serial = None if command not in seen_commands: seen_commands[command] = time.time() _LOGGER.debug("Adding %s to seen_commands: %s", command, seen_commands) if ("dopplerId" in json_payload and "deviceSerialNumber" in json_payload["dopplerId"]): serial = json_payload["dopplerId"]["deviceSerialNumber"] elif ("key" in json_payload and "entryId" in json_payload["key"] and json_payload["key"]["entryId"].find("#") != -1): serial = (json_payload["key"]["entryId"]).split("#")[2] json_payload["key"]["serialNumber"] = serial else: serial = None if command == "PUSH_ACTIVITY": # Last_Alexa Updated last_called = { "serialNumber": serial, "timestamp": json_payload["timestamp"], } if serial and serial in existing_serials: await update_last_called(login_obj, last_called) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"push_activity": json_payload}, ) elif command in ( "PUSH_AUDIO_PLAYER_STATE", "PUSH_MEDIA_CHANGE", "PUSH_MEDIA_PROGRESS_CHANGE", ): # Player update/ Push_media from tune_in if serial and serial in existing_serials: _LOGGER.debug("Updating media_player: %s", hide_serial(json_payload)) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"player_state": json_payload}, ) elif command == "PUSH_VOLUME_CHANGE": # Player volume update if serial and serial in existing_serials: _LOGGER.debug("Updating media_player volume: %s", hide_serial(json_payload)) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"player_state": json_payload}, ) elif command in ( "PUSH_DOPPLER_CONNECTION_CHANGE", "PUSH_EQUALIZER_STATE_CHANGE", ): # Player availability update if serial and serial in existing_serials: _LOGGER.debug( "Updating media_player availability %s", hide_serial(json_payload), ) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"player_state": json_payload}, ) elif command == "PUSH_BLUETOOTH_STATE_CHANGE": # Player bluetooth update bt_event = json_payload["bluetoothEvent"] bt_success = json_payload["bluetoothEventSuccess"] if (serial and serial in existing_serials and bt_success and bt_event and bt_event in ["DEVICE_CONNECTED", "DEVICE_DISCONNECTED"]): _LOGGER.debug("Updating media_player bluetooth %s", hide_serial(json_payload)) bluetooth_state = await update_bluetooth_state( login_obj, serial) # _LOGGER.debug("bluetooth_state %s", # hide_serial(bluetooth_state)) if bluetooth_state: async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"bluetooth_change": bluetooth_state}, ) elif command == "PUSH_MEDIA_QUEUE_CHANGE": # Player availability update if serial and serial in existing_serials: _LOGGER.debug("Updating media_player queue %s", hide_serial(json_payload)) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"queue_state": json_payload}, ) elif command == "PUSH_NOTIFICATION_CHANGE": # Player update await process_notifications(login_obj) if serial and serial in existing_serials: _LOGGER.debug( "Updating mediaplayer notifications: %s", hide_serial(json_payload), ) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"notification_update": json_payload}, ) elif command in [ "PUSH_DELETE_DOPPLER_ACTIVITIES", # delete Alexa history "PUSH_LIST_ITEM_CHANGE", # update shopping list "PUSH_CONTENT_FOCUS_CHANGE", # likely prime related refocus ]: pass else: _LOGGER.warning( "Unhandled command: %s with data %s. Please report at %s", command, hide_serial(json_payload), ISSUE_URL, ) if serial in existing_serials: hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_activity"]["serials"][serial] = time.time() if (serial and serial not in existing_serials and serial not in (hass.data[DATA_ALEXAMEDIA]["accounts"] [email]["excluded"].keys())): _LOGGER.debug("Discovered new media_player %s", serial) (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] ) = True coordinator = hass.data[DATA_ALEXAMEDIA]["accounts"][ email].get("coordinator") if coordinator: await coordinator.async_request_refresh()
async def ws_handler(message_obj): """Handle websocket messages. This allows push notifications from Alexa to update last_called and media state. """ command = (message_obj.json_payload["command"] if isinstance(message_obj.json_payload, dict) and "command" in message_obj.json_payload else None) json_payload = (message_obj.json_payload["payload"] if isinstance(message_obj.json_payload, dict) and "payload" in message_obj.json_payload else None) existing_serials = _existing_serials(hass, login_obj) seen_commands = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_commands"] if command and json_payload: _LOGGER.debug( "%s: Received websocket command: %s : %s", hide_email(email), command, hide_serial(json_payload), ) serial = None command_time = time.time() if command not in seen_commands: _LOGGER.debug("Adding %s to seen_commands: %s", command, seen_commands) seen_commands[command] = command_time if ("dopplerId" in json_payload and "deviceSerialNumber" in json_payload["dopplerId"]): serial = json_payload["dopplerId"]["deviceSerialNumber"] elif ("key" in json_payload and "entryId" in json_payload["key"] and json_payload["key"]["entryId"].find("#") != -1): serial = (json_payload["key"]["entryId"]).split("#")[2] json_payload["key"]["serialNumber"] = serial else: serial = None if command == "PUSH_ACTIVITY": # Last_Alexa Updated last_called = { "serialNumber": serial, "timestamp": json_payload["timestamp"], } try: if serial and serial in existing_serials: await update_last_called(login_obj, last_called) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"push_activity": json_payload}, ) except (AlexapyConnectionError): # Catch case where activities doesn't report valid json pass elif command in ( "PUSH_AUDIO_PLAYER_STATE", "PUSH_MEDIA_CHANGE", "PUSH_MEDIA_PROGRESS_CHANGE", ): # Player update/ Push_media from tune_in if serial and serial in existing_serials: _LOGGER.debug("Updating media_player: %s", hide_serial(json_payload)) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"player_state": json_payload}, ) elif command == "PUSH_VOLUME_CHANGE": # Player volume update if serial and serial in existing_serials: _LOGGER.debug("Updating media_player volume: %s", hide_serial(json_payload)) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"player_state": json_payload}, ) elif command in ( "PUSH_DOPPLER_CONNECTION_CHANGE", "PUSH_EQUALIZER_STATE_CHANGE", ): # Player availability update if serial and serial in existing_serials: _LOGGER.debug( "Updating media_player availability %s", hide_serial(json_payload), ) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"player_state": json_payload}, ) elif command == "PUSH_BLUETOOTH_STATE_CHANGE": # Player bluetooth update bt_event = json_payload["bluetoothEvent"] bt_success = json_payload["bluetoothEventSuccess"] if (serial and serial in existing_serials and bt_success and bt_event and bt_event in ["DEVICE_CONNECTED", "DEVICE_DISCONNECTED"]): _LOGGER.debug("Updating media_player bluetooth %s", hide_serial(json_payload)) bluetooth_state = await update_bluetooth_state( login_obj, serial) # _LOGGER.debug("bluetooth_state %s", # hide_serial(bluetooth_state)) if bluetooth_state: async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"bluetooth_change": bluetooth_state}, ) elif command == "PUSH_MEDIA_QUEUE_CHANGE": # Player availability update if serial and serial in existing_serials: _LOGGER.debug("Updating media_player queue %s", hide_serial(json_payload)) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"queue_state": json_payload}, ) elif command == "PUSH_NOTIFICATION_CHANGE": # Player update await process_notifications(login_obj) if serial and serial in existing_serials: _LOGGER.debug( "Updating mediaplayer notifications: %s", hide_serial(json_payload), ) async_dispatcher_send( hass, f"{DOMAIN}_{hide_email(email)}"[0:32], {"notification_update": json_payload}, ) elif command in [ "PUSH_DELETE_DOPPLER_ACTIVITIES", # delete Alexa history "PUSH_LIST_CHANGE", # clear a shopping list https://github.com/custom-components/alexa_media_player/issues/1190 "PUSH_LIST_ITEM_CHANGE", # update shopping list "PUSH_CONTENT_FOCUS_CHANGE", # likely prime related refocus "PUSH_DEVICE_SETUP_STATE_CHANGE", # likely device changes mid setup ]: pass else: _LOGGER.warning( "Unhandled command: %s with data %s. Please report at %s", command, hide_serial(json_payload), ISSUE_URL, ) if serial in existing_serials: history = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_activity"]["serials"].get(serial) if history is None or ( history and command_time - history[len(history) - 1][1] > 2): history = [(command, command_time)] else: history.append([command, command_time]) hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_activity"]["serials"][serial] = history events = [] for old_command, old_command_time in history: if (old_command in { "PUSH_VOLUME_CHANGE", "PUSH_EQUALIZER_STATE_CHANGE" } and command_time - old_command_time < 0.25): events.append((old_command, round(command_time - old_command_time, 2))) elif old_command in {"PUSH_AUDIO_PLAYER_STATE"}: # There is a potential false positive generated during this event events = [] if len(events) >= 4: _LOGGER.debug( "%s: Detected potential DND websocket change with %s events %s", hide_serial(serial), len(events), events, ) await update_dnd_state(login_obj) if (serial and serial not in existing_serials and serial not in (hass.data[DATA_ALEXAMEDIA]["accounts"] [email]["excluded"].keys())): _LOGGER.debug("Discovered new media_player %s", serial) (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] ) = True coordinator = hass.data[DATA_ALEXAMEDIA]["accounts"][ email].get("coordinator") if coordinator: await coordinator.async_request_refresh()
async def update_devices(login_obj): """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 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() existing_entities = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "entities"]["media_player"].values() websocket_enabled = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( "websocket") 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 = {} try: if new_devices: auth_info = await AlexaAPI.get_authentication(login_obj) devices = await AlexaAPI.get_devices(login_obj) bluetooth = await AlexaAPI.get_bluetooth(login_obj) preferences = await AlexaAPI.get_device_preferences(login_obj) dnd = await AlexaAPI.get_dnd_state(login_obj) raw_notifications = await AlexaAPI.get_notifications(login_obj) _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): _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_platform_callback) return 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: if include and device["accountName"] not in include: include_filter.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: exclude_filter.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 if "bluetoothStates" in bluetooth: for b_state in bluetooth["bluetoothStates"]: if device["serialNumber"] == b_state["deviceSerialNumber"]: device["bluetooth_state"] = b_state break if "devicePreferences" in preferences: for dev in preferences["devicePreferences"]: if dev["deviceSerialNumber"] == device["serialNumber"]: device["locale"] = dev["locale"] device["timeZoneId"] = dev["timeZoneId"] _LOGGER.debug( "Locale %s timezone %s found for %s", device["locale"], device["timeZoneId"], hide_serial(device["serialNumber"]), ) break if "doNotDisturbDeviceStatusList" in dnd: for dev in dnd["doNotDisturbDeviceStatusList"]: if dev["deviceSerialNumber"] == device["serialNumber"]: device["dnd"] = dev["enabled"] _LOGGER.debug( "DND %s found for %s", device["dnd"], hide_serial(device["serialNumber"]), ) break hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "auth_info"] = device["auth_info"] = auth_info (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 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: if component == "notify": hass.async_create_task( async_load_platform( hass, component, DOMAIN, { CONF_NAME: DOMAIN, "config": cleaned_config }, config, )) else: hass.async_add_job( hass.config_entries.async_forward_entry_setup( config_entry, component)) hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] = False async_call_later( hass, scan_interval if not websocket_enabled else scan_interval * 10, lambda _: hass.async_create_task( update_devices( # pylint: disable=unexpected-keyword-arg login_obj, no_throttle=True)), )
async def ws_handler(message_obj): """Handle websocket messages. This allows push notifications from Alexa to update last_called and media state. """ command = (message_obj.json_payload["command"] if isinstance(message_obj.json_payload, dict) and "command" in message_obj.json_payload else None) json_payload = (message_obj.json_payload["payload"] if isinstance(message_obj.json_payload, dict) and "payload" in message_obj.json_payload else None) existing_serials = _existing_serials() if "websocket_commands" not in ( hass.data[DATA_ALEXAMEDIA]["accounts"][email]): (hass.data[DATA_ALEXAMEDIA]["accounts"][email] ["websocket_commands"]) = {} seen_commands = hass.data[DATA_ALEXAMEDIA]["accounts"][email][ "websocket_commands"] if command and json_payload: import time _LOGGER.debug( "%s: Received websocket command: %s : %s", hide_email(email), command, hide_serial(json_payload), ) serial = None if command not in seen_commands: seen_commands[command] = time.time() _LOGGER.debug("Adding %s to seen_commands: %s", command, seen_commands) if ("dopplerId" in json_payload and "deviceSerialNumber" in json_payload["dopplerId"]): serial = json_payload["dopplerId"]["deviceSerialNumber"] elif ("key" in json_payload and "entryId" in json_payload["key"] and json_payload["key"]["entryId"].find("#") != -1): serial = (json_payload["key"]["entryId"]).split("#")[2] else: serial = None if command == "PUSH_ACTIVITY": # Last_Alexa Updated last_called = { "serialNumber": serial, "timestamp": json_payload["timestamp"], } if serial and serial in existing_serials: await update_last_called(login_obj, last_called) hass.bus.async_fire( f"{DOMAIN}_{hide_email(email)}"[0:32], {"push_activity": json_payload}, ) elif command in ( "PUSH_AUDIO_PLAYER_STATE", "PUSH_MEDIA_CHANGE", "PUSH_MEDIA_PROGRESS_CHANGE", ): # Player update/ Push_media from tune_in if serial and serial in existing_serials: _LOGGER.debug("Updating media_player: %s", hide_serial(json_payload)) hass.bus.async_fire( f"{DOMAIN}_{hide_email(email)}"[0:32], {"player_state": json_payload}, ) elif command == "PUSH_VOLUME_CHANGE": # Player volume update if serial and serial in existing_serials: _LOGGER.debug("Updating media_player volume: %s", hide_serial(json_payload)) hass.bus.async_fire( f"{DOMAIN}_{hide_email(email)}"[0:32], {"player_state": json_payload}, ) elif command in ( "PUSH_DOPPLER_CONNECTION_CHANGE", "PUSH_EQUALIZER_STATE_CHANGE", ): # Player availability update if serial and serial in existing_serials: _LOGGER.debug( "Updating media_player availability %s", hide_serial(json_payload), ) hass.bus.async_fire( f"{DOMAIN}_{hide_email(email)}"[0:32], {"player_state": json_payload}, ) elif command == "PUSH_BLUETOOTH_STATE_CHANGE": # Player bluetooth update bt_event = json_payload["bluetoothEvent"] bt_success = json_payload["bluetoothEventSuccess"] if (serial and serial in existing_serials and bt_success and bt_event and bt_event in ["DEVICE_CONNECTED", "DEVICE_DISCONNECTED"]): _LOGGER.debug("Updating media_player bluetooth %s", hide_serial(json_payload)) bluetooth_state = await update_bluetooth_state( login_obj, serial) # _LOGGER.debug("bluetooth_state %s", # hide_serial(bluetooth_state)) if bluetooth_state: hass.bus.async_fire( f"{DOMAIN}_{hide_email(email)}"[0:32], {"bluetooth_change": bluetooth_state}, ) elif command == "PUSH_MEDIA_QUEUE_CHANGE": # Player availability update if serial and serial in existing_serials: _LOGGER.debug("Updating media_player queue %s", hide_serial(json_payload)) hass.bus.async_fire( f"{DOMAIN}_{hide_email(email)}"[0:32], {"queue_state": json_payload}, ) elif command == "PUSH_NOTIFICATION_CHANGE": # Player update await process_notifications(login_obj) if serial and serial in existing_serials: _LOGGER.debug( "Updating mediaplayer notifications: %s", hide_serial(json_payload), ) hass.bus.async_fire( f"{DOMAIN}_{hide_email(email)}"[0:32], {"notification_update": json_payload}, ) if (serial and serial not in existing_serials and serial not in (hass.data[DATA_ALEXAMEDIA]["accounts"] [email]["excluded"].keys())): _LOGGER.debug("Discovered new media_player %s", serial) (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] ) = True await update_devices(login_obj, no_throttle=True)