diff --git a/alexa_media/__init__.py b/alexa_media/__init__.py index cf54988a..a6da7b47 100644 --- a/alexa_media/__init__.py +++ b/alexa_media/__init__.py @@ -13,7 +13,7 @@ from homeassistant import util from homeassistant.const import ( - CONF_EMAIL, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_URL) + CONF_EMAIL, CONF_NAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_URL) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.discovery import load_platform @@ -26,9 +26,9 @@ # from .config_flow import configured_instances -REQUIREMENTS = ['alexapy==0.4.2'] +REQUIREMENTS = ['alexapy==0.4.3'] -__version__ = '1.2.3' +__version__ = '1.2.4' _LOGGER = logging.getLogger(__name__) @@ -98,7 +98,7 @@ def setup(hass, config, discovery_info=None): email = account.get(CONF_EMAIL) password = account.get(CONF_PASSWORD) url = account.get(CONF_URL) - hass.data[DOMAIN]['accounts'][email] = {"config": []} + hass.data[DATA_ALEXAMEDIA]['accounts'][email] = {"config": []} login = AlexaLogin(url, email, password, hass.config.path, account.get(CONF_DEBUG)) @@ -171,7 +171,7 @@ async def configuration_callback(callback_data): description=('Please select the verification method. ' '(e.g., sms or email).
{}').format( options - ), + ), submit_caption="Confirm", fields=[{'id': 'claimsoption', 'name': 'Option'}] ) @@ -194,13 +194,13 @@ async def configuration_callback(callback_data): submit_caption="Confirm", fields=[] ) - hass.data[DOMAIN]['accounts'][email]['config'].append(config_id) + hass.data[DATA_ALEXAMEDIA]['accounts'][email]['config'].append(config_id) if 'error_message' in status and status['error_message']: configurator.notify_errors( # use sync to delay next pop config_id, status['error_message']) - if len(hass.data[DOMAIN]['accounts'][email]['config']) > 1: - configurator.async_request_done((hass.data[DOMAIN] + if len(hass.data[DATA_ALEXAMEDIA]['accounts'][email]['config']) > 1: + configurator.async_request_done((hass.data[DATA_ALEXAMEDIA] ['accounts'][email]['config']).pop(0)) @@ -239,12 +239,14 @@ 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. + 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] @@ -257,6 +259,11 @@ def update_devices(): [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) _LOGGER.debug("%s: Found %s devices, %s bluetooth", @@ -264,7 +271,8 @@ def update_devices(): 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[DOMAIN]['accounts'][email]['config']): + 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) @@ -276,9 +284,33 @@ def update_devices(): 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']: @@ -304,7 +336,8 @@ def update_devices(): if new_alexa_clients: for component in ALEXA_COMPONENTS: - load_platform(hass, component, DOMAIN, {}, config) + load_platform(hass, component, DOMAIN, {CONF_NAME: DOMAIN}, + config) # Process last_called data to fire events update_last_called(login_obj) @@ -340,6 +373,22 @@ def update_last_called(login_obj, last_called=None): [email] ['last_called']) = last_called + def update_bluetooth_state(login_obj, device_serial): + """Update the bluetooth state on ws bluetooth event.""" + from alexapy import AlexaAPI + bluetooth = AlexaAPI.get_bluetooth(login_obj) + device = (hass.data[DATA_ALEXAMEDIA] + ['accounts'] + [email] + ['devices'] + ['media_player'] + [device_serial]) + + for b_state in bluetooth['bluetoothStates']: + if device_serial == b_state['deviceSerialNumber']: + device['bluetooth_state'] = b_state + return device['bluetooth_state'] + def last_call_handler(call): """Handle last call service request. @@ -390,31 +439,72 @@ def ws_handler(message_obj): 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 command and json_payload: _LOGGER.debug("%s: Received websocket command: %s : %s", hide_email(email), command, json_payload) + serial = None if command == 'PUSH_ACTIVITY': # Last_Alexa Updated + serial = (json_payload + ['key'] + ['entryId']).split('#')[2] last_called = { - 'serialNumber': (json_payload - ['key'] - ['entryId']).split('#')[2], + 'serialNumber': serial, 'timestamp': json_payload['timestamp'] - } - update_last_called(login_obj, last_called) + } + if (serial and serial in existing_serials): + update_last_called(login_obj, last_called) elif command == 'PUSH_AUDIO_PLAYER_STATE': # Player update - _LOGGER.debug("Updating media_player: %s", json_payload) - hass.bus.fire(('{}_{}'.format(DOMAIN, - hide_email(email)))[0:32], - {'player_state': json_payload}) + serial = (json_payload['dopplerId']['deviceSerialNumber']) + if (serial and serial in existing_serials): + _LOGGER.debug("Updating media_player: %s", json_payload) + hass.bus.fire(('{}_{}'.format(DOMAIN, + hide_email(email)))[0:32], + {'player_state': json_payload}) elif command == 'PUSH_VOLUME_CHANGE': # Player volume update - _LOGGER.debug("Updating media_player volume: %s", json_payload) - hass.bus.fire(('{}_{}'.format(DOMAIN, - hide_email(email)))[0:32], - {'player_state': json_payload}) + serial = (json_payload['dopplerId']['deviceSerialNumber']) + if (serial and serial in existing_serials): + _LOGGER.debug("Updating media_player volume: %s", + json_payload) + hass.bus.fire(('{}_{}'.format(DOMAIN, + hide_email(email)))[0:32], + {'player_state': json_payload}) + elif command == 'PUSH_DOPPLER_CONNECTION_CHANGE': + # Player availability update + serial = (json_payload['dopplerId']['deviceSerialNumber']) + if (serial and serial in existing_serials): + _LOGGER.debug("Updating media_player availability %s", + json_payload) + hass.bus.fire(('{}_{}'.format(DOMAIN, + hide_email(email)))[0:32], + {'player_state': json_payload}) + elif command == 'PUSH_BLUETOOTH_STATE_CHANGE': + # Player bluetooth update + serial = (json_payload['dopplerId']['deviceSerialNumber']) + if (serial and serial in existing_serials): + _LOGGER.debug("Updating media_player bluetooth %s", + json_payload) + bluetooth_state = update_bluetooth_state(login_obj, serial) + hass.bus.fire(('{}_{}'.format(DOMAIN, + hide_email(email)))[0:32], + {'bluetooth_change': bluetooth_state}) + 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 + update_devices(no_throttle=True) def ws_close_handler(): """Handle websocket close. @@ -424,7 +514,8 @@ def ws_close_handler(): email = login_obj.email _LOGGER.debug("%s: Received websocket close; attempting reconnect", hide_email(email)) - (hass.data[DOMAIN]['accounts'][email]['websocket']) = ws_connect() + (hass.data[DATA_ALEXAMEDIA]['accounts'] + [email]['websocket']) = ws_connect() def ws_error_handler(message): """Handle websocket error. @@ -437,23 +528,32 @@ def ws_error_handler(message): _LOGGER.debug("%s: Received websocket error %s", hide_email(email), message) - (hass.data[DOMAIN]['accounts'][email]['websocket']) = None + (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['websocket']) = None include = config.get(CONF_INCLUDE_DEVICES) exclude = config.get(CONF_EXCLUDE_DEVICES) scan_interval = config.get(CONF_SCAN_INTERVAL) email = login_obj.email - (hass.data[DOMAIN]['accounts'][email]['websocket']) = ws_connect() - (hass.data[DOMAIN]['accounts'][email]['login_obj']) = login_obj - (hass.data[DOMAIN]['accounts'][email]['devices']) = {'media_player': {}} - (hass.data[DOMAIN]['accounts'][email]['entities']) = {'media_player': {}} + (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['websocket']) = ws_connect() + (hass.data[DATA_ALEXAMEDIA]['accounts'][email]['login_obj']) = login_obj + if 'devices' not in hass.data[DATA_ALEXAMEDIA]['accounts'][email]: + (hass.data[DATA_ALEXAMEDIA]['accounts'][email] + ['devices']) = {'media_player': {}} + if 'excluded' not in hass.data[DATA_ALEXAMEDIA]['accounts'][email]: + (hass.data[DATA_ALEXAMEDIA]['accounts'][email] + ['excluded']) = {} + if 'entities' not in hass.data[DATA_ALEXAMEDIA]['accounts'][email]: + (hass.data[DATA_ALEXAMEDIA]['accounts'][email] + ['entities']) = {'media_player': {}} + (hass.data[DATA_ALEXAMEDIA] + ['accounts'][email]['new_devices']) = True # force initial update + track_time_interval(hass, lambda now: update_devices(), scan_interval) update_devices() - track_time_interval(hass, lambda now: update_devices(), scan_interval) hass.services.register(DOMAIN, SERVICE_UPDATE_LAST_CALLED, last_call_handler, schema=LAST_CALL_UPDATE_SCHEMA) # Clear configurator. We delay till here to avoid leaving a modal orphan - for config_id in hass.data[DOMAIN]['accounts'][email]['config']: + for config_id in hass.data[DATA_ALEXAMEDIA]['accounts'][email]['config']: configurator = hass.components.configurator configurator.async_request_done(config_id) - hass.data[DOMAIN]['accounts'][email]['config'] = [] + hass.data[DATA_ALEXAMEDIA]['accounts'][email]['config'] = [] return True diff --git a/alexa_media/const.py b/alexa_media/const.py index b67c2418..99414e47 100644 --- a/alexa_media/const.py +++ b/alexa_media/const.py @@ -18,7 +18,8 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) ALEXA_COMPONENTS = [ - 'media_player' + 'media_player', + 'notify' ] CONF_ACCOUNTS = 'accounts' diff --git a/alexa_media/media_player.py b/alexa_media/media_player.py index 672dd2ba..43757a78 100644 --- a/alexa_media/media_player.py +++ b/alexa_media/media_player.py @@ -190,6 +190,14 @@ def _handle_event(self, event): force_refresh = not (self.hass.data[DATA_ALEXAMEDIA] ['accounts'][email]['websocket']) self.schedule_update_ha_state(force_refresh=force_refresh) + elif 'bluetooth_change' in event.data: + if (event.data['bluetooth_change']['deviceSerialNumber'] == + self.device_serial_number): + self._bluetooth_state = event.data['bluetooth_change'] + self._source = self._get_source() + self._source_list = self._get_source_list() + if (self.hass and self.schedule_update_ha_state): + self.schedule_update_ha_state() elif 'player_state' in event.data: player_state = event.data['player_state'] if (player_state['dopplerId'] @@ -206,6 +214,11 @@ def _handle_event(self, event): self._media_vol_level = player_state['volumeSetting']/100 if (self.hass and self.schedule_update_ha_state): self.schedule_update_ha_state() + elif 'dopplerConnectionState' in player_state: + self._available = (player_state['dopplerConnectionState'] + == "ONLINE") + if (self.hass and self.schedule_update_ha_state): + self.schedule_update_ha_state() def _clear_media_details(self): """Set all Media Items to None.""" @@ -513,7 +526,9 @@ def set_volume_level(self, volume): return self.alexa_api.set_volume(volume) self._media_vol_level = volume - self.update() + if not (self.hass.data[DATA_ALEXAMEDIA] + ['accounts'][self._login.email]['websocket']): + self.update() @property def volume_level(self): @@ -546,7 +561,9 @@ def mute_volume(self, mute): self.alexa_api.set_volume(self._previous_volume) else: self.alexa_api.set_volume(50) - self.update() + if not (self.hass.data[DATA_ALEXAMEDIA] + ['accounts'][self._login.email]['websocket']): + self.update() def media_play(self): """Send play command.""" @@ -554,7 +571,9 @@ def media_play(self): and self.available): return self.alexa_api.play() - self.update() + if not (self.hass.data[DATA_ALEXAMEDIA] + ['accounts'][self._login.email]['websocket']): + self.update() def media_pause(self): """Send pause command.""" @@ -562,7 +581,9 @@ def media_pause(self): and self.available): return self.alexa_api.pause() - self.update() + if not (self.hass.data[DATA_ALEXAMEDIA] + ['accounts'][self._login.email]['websocket']): + self.update() def turn_off(self): """Turn the client off. @@ -589,7 +610,9 @@ def media_next_track(self): and self.available): return self.alexa_api.next() - self.update() + if not (self.hass.data[DATA_ALEXAMEDIA] + ['accounts'][self._login.email]['websocket']): + self.update() def media_previous_track(self): """Send previous track command.""" @@ -597,7 +620,9 @@ def media_previous_track(self): and self.available): return self.alexa_api.previous() - self.update() + if not (self.hass.data[DATA_ALEXAMEDIA] + ['accounts'][self._login.email]['websocket']): + self.update() def send_tts(self, message): """Send TTS to Device. @@ -632,7 +657,9 @@ def play_media(self, media_type, media_id, enqueue=None, **kwargs): else: self.alexa_api.play_music(media_type, media_id, customer_id=self._customer_id, **kwargs) - self.update() + if not (self.hass.data[DATA_ALEXAMEDIA] + ['accounts'][self._login.email]['websocket']): + self.update() @property def device_state_attributes(self): diff --git a/alexa_media/notify.py b/alexa_media/notify.py index 83f7878e..aa0217df 100644 --- a/alexa_media/notify.py +++ b/alexa_media/notify.py @@ -92,24 +92,16 @@ def convert(self, names, type_="entities", filter_matches=False): devices.append(item) return devices - # This can't be enabled because notify is setup before the media_player - # once a method is determined to wait till media_player, this can be used - # @property - # def targets(self): - # """Return a dictionary of Alexa devices.""" - # devices = {} - # # if ('accounts' not in self.hass.data[DATA_ALEXAMEDIA] or - # # self.hass.data[DATA_ALEXAMEDIA]['accounts'].items()): - # # return devices - # for account, account_dict in (self.hass.data[DATA_ALEXAMEDIA] - # ['accounts'].items()): - # for alexa in account_dict['entities']['media_player'].values(): - # devices[alexa.name] = alexa - # _LOGGER.debug("Account: %s Devices: %s Raw:%s", - # hide_email(account), - # devices, - # account_dict) - # return devices + @property + def targets(self): + """Return a dictionary of Alexa devices.""" + devices = {} + for account, account_dict in (self.hass.data[DATA_ALEXAMEDIA] + ['accounts'].items()): + for serial, alexa in (account_dict + ['devices']['media_player'].items()): + devices[alexa['accountName']] = serial + return devices @property def devices(self): @@ -126,9 +118,9 @@ def devices(self): def send_message(self, message="", **kwargs): """Send a message to a Alexa device.""" - _LOGGER.info("Message: %s, kwargs: %s", - message, - kwargs) + _LOGGER.debug("Message: %s, kwargs: %s", + message, + kwargs) kwargs['message'] = message targets = kwargs.get(ATTR_TARGET) title = (kwargs.get(ATTR_TITLE) if ATTR_TITLE in kwargs @@ -141,13 +133,13 @@ def send_message(self, message="", **kwargs): entities.extend(self.hass.components.group.expand_entity_ids( entities)) except ValueError: - _LOGGER.info("Invalid Home Assistant entity in %s", entities) + _LOGGER.debug("Invalid Home Assistant entity in %s", entities) if data['type'] == "tts": targets = self.convert(entities, type_="entities", filter_matches=True) _LOGGER.debug("TTS entities: %s", targets) for alexa in targets: - _LOGGER.info("TTS by %s : %s", alexa, message) + _LOGGER.debug("TTS by %s : %s", alexa, message) alexa.send_tts(message) elif data['type'] == "announce": targets = self.convert(entities, type_="serialnumbers", @@ -160,12 +152,12 @@ def send_message(self, message="", **kwargs): for alexa in (account_dict['entities'] ['media_player'].values()): if alexa.unique_id in targets and alexa.available: - _LOGGER.info(("%s: Announce by %s to " - "targets: %s: %s"), - hide_email(account), - alexa, - list(map(hide_serial, targets)), - message) + _LOGGER.debug(("%s: Announce by %s to " + "targets: %s: %s"), + hide_email(account), + alexa, + list(map(hide_serial, targets)), + message) alexa.send_announcement(message, targets=targets, title=title, @@ -177,5 +169,5 @@ def send_message(self, message="", **kwargs): targets = self.convert(entities, type_="entities", filter_matches=True) for alexa in targets: - _LOGGER.info("Push by %s : %s %s", alexa, title, message) + _LOGGER.debug("Push by %s : %s %s", alexa, title, message) alexa.send_mobilepush(message, title=title) diff --git a/custom_components.json b/custom_components.json index e17839ac..9afdf55b 100644 --- a/custom_components.json +++ b/custom_components.json @@ -1,6 +1,6 @@ { "alexa_media": { - "version": "1.2.3", + "version": "1.2.4", "local_location": "/custom_components/alexa_media/__init__.py", "remote_location": "https://raw.githubusercontent.com/keatontaylor/alexa_media_player/master/alexa_media/__init__.py", "visit_repo": "https://github.com/keatontaylor/alexa_media_player", @@ -10,6 +10,18 @@ "https://raw.githubusercontent.com/keatontaylor/alexa_media_player/master/alexa_media/media_player.py", "https://raw.githubusercontent.com/keatontaylor/alexa_media_player/master/alexa_media/notify.py" ] + }, + "alexa_media (dev)": { + "version": "1.2.4", + "local_location": "/custom_components/alexa_media/__init__.py", + "remote_location": "https://raw.githubusercontent.com/keatontaylor/alexa_media_player/dev/alexa_media/__init__.py", + "visit_repo": "https://github.com/keatontaylor/alexa_media_player", + "changelog": "https://github.com/keatontaylor/alexa_media_player/wiki/Changelog", + "resources": [ + "https://raw.githubusercontent.com/keatontaylor/alexa_media_player/dev/alexa_media/const.py", + "https://raw.githubusercontent.com/keatontaylor/alexa_media_player/dev/alexa_media/media_player.py", + "https://raw.githubusercontent.com/keatontaylor/alexa_media_player/dev/alexa_media/notify.py" + ] } }