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,
    )
Esempio n. 2
0
    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
Esempio n. 4
0
    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
Esempio n. 5
0
    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,
    )
Esempio n. 7
0
    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)
Esempio n. 8
0
    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)))
Esempio n. 9
0
    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()
Esempio n. 10
0
    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()
Esempio n. 11
0
    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)),
        )
Esempio n. 12
0
    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)