From f0531e35a289d22f75d75ccd22bbe624b84a0aae Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Fri, 5 Jul 2019 00:45:59 -0700 Subject: [PATCH 01/20] Sync dev with master (#246) * Update issue templates * Update location of version info Signed-off-by: Alan Tse --- .github/ISSUE_TEMPLATE/bug_report.md | 33 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..335112cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**System details** + - Home-assistant (version): + - Hassio (Yes/No): (Please note you may have to restart hassio 2-3 times to load the latest version of alexapy after an update. This looks like a HA bug). + - alexa_media (version from `const.py` or HA startup): + - alexapy (version from `pip show alexapy` or HA startup): + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From d1ed479bcee849d4a9deabbf31397c7ec43feaa5 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 13 Jul 2019 00:03:31 -0700 Subject: [PATCH 02/20] Sync dev with master (#252) * Update issue templates * Update location of version info Signed-off-by: Alan Tse From 5846371b80e6b71225e5064c09fbc9913b98f41e Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 3 Aug 2019 23:47:38 -0700 Subject: [PATCH 03/20] fix(media_player): remove unused MEDIA_PLAYER_SCHEMA (#261) --- custom_components/alexa_media/media_player.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/alexa_media/media_player.py b/custom_components/alexa_media/media_player.py index bd439373..c5a06b3d 100644 --- a/custom_components/alexa_media/media_player.py +++ b/custom_components/alexa_media/media_player.py @@ -12,8 +12,7 @@ from typing import List # noqa pylint: disable=unused-import import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import (MEDIA_PLAYER_SCHEMA, - MediaPlayerDevice) +from homeassistant.components.media_player import (MediaPlayerDevice) from homeassistant.components.media_player.const import ( DOMAIN, MEDIA_TYPE_MUSIC, From c560742d0b9fc94b236a4c129b1b9b9b12af2a8f Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 3 Aug 2019 23:47:51 -0700 Subject: [PATCH 04/20] fix(media_player): alternative serial numbers not recognized for mobile app media player (#253) * Fix last_called websocket handler to recognize appDeviceList serial numbers * Fix last_called poll to recognize appDeviceList serial numbers --- custom_components/alexa_media/media_player.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/custom_components/alexa_media/media_player.py b/custom_components/alexa_media/media_player.py index c5a06b3d..54933003 100644 --- a/custom_components/alexa_media/media_player.py +++ b/custom_components/alexa_media/media_player.py @@ -159,8 +159,10 @@ def _handle_event(self, event): assumes the MediaClient state is already updated. """ if 'last_called_change' in event.data: - if (event.data['last_called_change']['serialNumber'] == - self.device_serial_number): + event_serial = event.data['last_called_change']['serialNumber'] + if (event_serial == self.device_serial_number or + any(item['serialNumber'] == + event_serial for item in self._app_device_list)): _LOGGER.debug("%s is last_called: %s", self.name, hide_serial(self.device_serial_number)) self._last_called = True @@ -241,6 +243,7 @@ def refresh(self, device=None): self._device_family = device['deviceFamily'] self._device_type = device['deviceType'] self._device_serial_number = device['serialNumber'] + self._app_device_list = device['appDeviceList'] self._device_owner_customer_id = device['deviceOwnerCustomerId'] self._software_version = device['softwareVersion'] self._available = device['online'] @@ -358,10 +361,10 @@ def _get_last_called(self): self._device_name, hide_serial(self._device_serial_number), hide_serial(last_called_serial)) - if (last_called_serial is not None and - self._device_serial_number == last_called_serial): - return True - return False + return (last_called_serial is not None and + (self._device_serial_number == last_called_serial or + any(item['serialNumber'] == + last_called_serial for item in self._app_device_list))) @property def available(self): From 2b26a12d022a35c471a8b0443f26b58c968836f3 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 3 Aug 2019 23:48:05 -0700 Subject: [PATCH 05/20] Fix TypeError exception for regions without Guard (#245) --- custom_components/alexa_media/alarm_control_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/alexa_media/alarm_control_panel.py b/custom_components/alexa_media/alarm_control_panel.py index d07bd450..27f0f36a 100644 --- a/custom_components/alexa_media/alarm_control_panel.py +++ b/custom_components/alexa_media/alarm_control_panel.py @@ -90,7 +90,7 @@ def __init__(self, login, hass): ['amazonBridgeDetails']['amazonBridgeDetails'] ['LambdaBridge_AAA/OnGuardSmartHomeBridgeService'] ['applianceDetails']['applianceDetails']) - except KeyError: + except (KeyError, TypeError): guard_dict = {} for key, value in guard_dict.items(): if value['modelName'] == "REDROCK_GUARD_PANEL": From 4cc934c073caf92ba402fa9f431a04f3dadb50fc Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 4 Aug 2019 00:18:10 -0700 Subject: [PATCH 06/20] fix(guard): add 1s delay for guard state check after voice activity (#262) --- custom_components/alexa_media/__init__.py | 3 +++ custom_components/alexa_media/alarm_control_panel.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index 8e09163b..bc6355ec 100644 --- a/custom_components/alexa_media/__init__.py +++ b/custom_components/alexa_media/__init__.py @@ -464,6 +464,9 @@ def ws_handler(message_obj): } if (serial and serial in existing_serials): update_last_called(login_obj, last_called) + hass.bus.fire(('{}_{}'.format(DOMAIN, + hide_email(email)))[0:32], + {'push_activity': json_payload}) elif command == 'PUSH_AUDIO_PLAYER_STATE': # Player update serial = (json_payload['dopplerId']['deviceSerialNumber']) diff --git a/custom_components/alexa_media/alarm_control_panel.py b/custom_components/alexa_media/alarm_control_panel.py index 27f0f36a..c7066d37 100644 --- a/custom_components/alexa_media/alarm_control_panel.py +++ b/custom_components/alexa_media/alarm_control_panel.py @@ -15,6 +15,7 @@ from homeassistant.const import (STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.event import call_later from . import DATA_ALEXAMEDIA from . import DOMAIN as ALEXA_DOMAIN @@ -116,7 +117,9 @@ def _handle_event(self, event): Used instead of polling. """ - self.refresh() + if 'push_activity' in event.data: + call_later(self.hass, 1, lambda _: + self.refresh(no_throttle=True)) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def refresh(self): From ff6483e5997401cfced7afa635093ebcc87d289c Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sun, 23 Jun 2019 00:03:14 -0700 Subject: [PATCH 07/20] Add basic switch support (do not disturb, repeat, shuffle) --- custom_components/alexa_media/__init__.py | 9 ++ custom_components/alexa_media/const.py | 3 +- custom_components/alexa_media/media_player.py | 52 +++++- custom_components/alexa_media/switch.py | 153 ++++++++++++++++++ 4 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 custom_components/alexa_media/switch.py diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index bc6355ec..a007e170 100644 --- a/custom_components/alexa_media/__init__.py +++ b/custom_components/alexa_media/__init__.py @@ -263,6 +263,7 @@ def update_devices(): devices = AlexaAPI.get_devices(login_obj) bluetooth = AlexaAPI.get_bluetooth(login_obj) preferences = AlexaAPI.get_device_preferences(login_obj) + dnd = AlexaAPI.get_dnd_state(login_obj) _LOGGER.debug("%s: Found %s devices, %s bluetooth", hide_email(email), len(devices) if devices is not None else '', @@ -320,6 +321,14 @@ def update_devices(): _LOGGER.debug("Locale %s found for %s", device['locale'], hide_serial(device['serialNumber'])) + + 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'])) + (hass.data[DATA_ALEXAMEDIA] ['accounts'] [email] diff --git a/custom_components/alexa_media/const.py b/custom_components/alexa_media/const.py index 3a170b4b..64b3cbe8 100644 --- a/custom_components/alexa_media/const.py +++ b/custom_components/alexa_media/const.py @@ -24,7 +24,8 @@ ALEXA_COMPONENTS = [ 'media_player', 'notify', - 'alarm_control_panel' + 'alarm_control_panel', + 'switch' ] CONF_ACCOUNTS = 'accounts' diff --git a/custom_components/alexa_media/media_player.py b/custom_components/alexa_media/media_player.py index 54933003..3578ecfd 100644 --- a/custom_components/alexa_media/media_player.py +++ b/custom_components/alexa_media/media_player.py @@ -22,6 +22,7 @@ SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, + SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -46,7 +47,7 @@ SUPPORT_VOLUME_SET | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | SUPPORT_VOLUME_MUTE | SUPPORT_PAUSE | - SUPPORT_SELECT_SOURCE) + SUPPORT_SELECT_SOURCE | SUPPORT_SHUFFLE_SET) _LOGGER = logging.getLogger(__name__) DEPENDENCIES = [ALEXA_DOMAIN] @@ -131,8 +132,12 @@ def __init__(self, device, login, hass): self._previous_volume = None self._source = None self._source_list = [] + self._shuffle = None + self._repeat = None # Last Device self._last_called = None + # Do not Disturb state + self._dnd = None # Polling state self._should_poll = True self._last_update = 0 @@ -251,6 +256,7 @@ def refresh(self, device=None): self._cluster_members = device['clusterMembers'] self._bluetooth_state = device['bluetooth_state'] self._locale = device['locale'] if 'locale' in device else 'en-US' + self._dnd = device['dnd'] if self._available is True: _LOGGER.debug("%s: Refreshing %s", self.account, self.name) self._source = self._get_source() @@ -311,6 +317,15 @@ def refresh(self, device=None): None and 'mediaLength' in self._session['progress']) else None) + if self._session['transport'] is not None: + self._shuffle = (self._session['transport'] + ['shuffle'] == "SELECTED" + if ('shuffle' in self._session['transport']) + else None) + self._repeat = (self._session['transport'] + ['repeat'] == "SELECTED" + if ('repeat' in self._session['transport']) + else None) @property def source(self): @@ -502,6 +517,41 @@ def device_family(self): """Return the make of the device (ex. Echo, Other).""" return self._device_family + @property + def dnd_state(self): + """Return the Do Not Disturb state.""" + return self._dnd + + @dnd_state.setter + def dnd_state(self, state): + """Set the Do Not Disturb state.""" + self._dnd = state + + def set_shuffle(self, shuffle): + """Enable/disable shuffle mode.""" + self.alexa_api.shuffle(shuffle) + self.shuffle_state = shuffle + + @property + def shuffle_state(self): + """Return the Shuffle state.""" + return self._shuffle + + @shuffle_state.setter + def shuffle_state(self, state): + """Set the Shuffle state.""" + self._shuffle = state + + @property + def repeat_state(self): + """Return the Repeat state.""" + return self._repeat + + @repeat_state.setter + def repeat_state(self, state): + """Set the Repeat state.""" + self._repeat = state + @property def supported_features(self): """Flag media player features that are supported.""" diff --git a/custom_components/alexa_media/switch.py b/custom_components/alexa_media/switch.py new file mode 100644 index 00000000..ebebe8fe --- /dev/null +++ b/custom_components/alexa_media/switch.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +""" +Alexa Devices Switches. + +For more details about this platform, please refer to the documentation at +https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 +""" +import logging +from typing import List # noqa pylint: disable=unused-import + +from homeassistant import util +from homeassistant.components.switch import SwitchDevice +from homeassistant.helpers.event import call_later + +from . import DATA_ALEXAMEDIA +from . import DOMAIN as ALEXA_DOMAIN +from . import ( + MIN_TIME_BETWEEN_FORCED_SCANS, MIN_TIME_BETWEEN_SCANS, + hide_email, hide_serial +) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices_callback, + discovery_info=None): + """Set up the Alexa switch platform.""" + _LOGGER.debug("Loading switches") + devices = [] # type: List[DNDSwitch] + SWITCH_TYPES = [ + ('dnd', DNDSwitch), + ('shuffle', ShuffleSwitch), + ('repeat', RepeatSwitch) + ] + for account, account_dict in (hass.data[DATA_ALEXAMEDIA] + ['accounts'].items()): + for key, device in account_dict['devices']['media_player'].items(): + if 'switch' not in account_dict['entities']: + (hass.data[DATA_ALEXAMEDIA] + ['accounts'] + [account] + ['entities'] + ['switch']) = {} + if key not in account_dict['entities']['media_player']: + _LOGGER.debug("Media Players not loaded yet; delaying load") + call_later(hass, 5, lambda _: + setup_platform(hass, + config, + add_devices_callback, + discovery_info)) + return True + elif key not in account_dict['entities']['switch']: + (hass.data[DATA_ALEXAMEDIA] + ['accounts'] + [account] + ['entities'] + ['switch'][key]) = {} + for (switch_key, class_) in SWITCH_TYPES: + alexa_client = class_(account_dict['entities'] + ['media_player'] + [key]) # type: AlexaMediaSwitch + (hass.data[DATA_ALEXAMEDIA] + ['accounts'] + [account] + ['entities'] + ['switch'][key][switch_key]) = alexa_client + _LOGGER.debug("%s: Found %s %s switch", + hide_email(account), + hide_serial(key), + switch_key) + devices.append(alexa_client) + if devices: + add_devices_callback(devices, True) + return True + + +class AlexaMediaSwitch(SwitchDevice): + """Representation of a Alexa Media switch.""" + + def __init__(self, + client, + switch_property, + switch_function, + name="Alexa"): + """Initialize the Alexa Switch device.""" + # Class info + self._client = client + self._name = name + self._switch_property = switch_property + self._switch_function = switch_function + _LOGGER.debug("Creating %s switch for %s", name, client) + + def _set_switch(self, state, **kwargs): + success = self._switch_function(state) + # if function returns success, make immediate state change + if success: + self._switch_property = state + + @property + def is_on(self): + """Return true if on.""" + return self._switch_property + + def turn_on(self, **kwargs): + """Turn on switch.""" + self._set_switch(True, **kwargs) + + def turn_off(self, **kwargs): + """Turn off switch.""" + self._set_switch(False, **kwargs) + + @property + def name(self): + """Return the name of the switch.""" + return "{} {} switch".format(self._client.name, self._name) + + +class DNDSwitch(AlexaMediaSwitch): + """Representation of a Alexa Media Do Not Disturb switch.""" + + def __init__(self, client): + """Initialize the Alexa Switch.""" + # Class info + super().__init__(client, + client.dnd_state, + client.alexa_api.set_dnd_state, + "do not disturb") + + +class ShuffleSwitch(AlexaMediaSwitch): + """Representation of a Alexa Media Shuffle switch.""" + + def __init__(self, client): + """Initialize the Alexa Switch.""" + # Class info + super().__init__(client, + client.shuffle_state, + client.alexa_api.shuffle, + "shuffle") + + +class RepeatSwitch(AlexaMediaSwitch): + """Representation of a Alexa Media Repeat switch.""" + + def __init__(self, client): + """Initialize the Alexa Switch.""" + # Class info + super().__init__(client, + client.repeat_state, + client.alexa_api.repeat, + "repeat") From 54afcfd025a222675214bfafdf924673e26fd398 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sun, 4 Aug 2019 02:58:33 -0700 Subject: [PATCH 08/20] feat(switches): add code to update state changes --- custom_components/alexa_media/__init__.py | 9 +++ custom_components/alexa_media/media_player.py | 20 +++++ custom_components/alexa_media/switch.py | 73 ++++++++++++++++--- 3 files changed, 90 insertions(+), 12 deletions(-) diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index a007e170..9b8d96b5 100644 --- a/custom_components/alexa_media/__init__.py +++ b/custom_components/alexa_media/__init__.py @@ -512,6 +512,15 @@ def ws_handler(message_obj): hass.bus.fire(('{}_{}'.format(DOMAIN, hide_email(email)))[0:32], {'bluetooth_change': bluetooth_state}) + elif command == 'PUSH_MEDIA_QUEUE_CHANGE': + # Player availability update + serial = (json_payload['dopplerId']['deviceSerialNumber']) + if (serial and serial in existing_serials): + _LOGGER.debug("Updating media_player queue %s", + json_payload) + hass.bus.fire(('{}_{}'.format(DOMAIN, + hide_email(email)))[0:32], + {'queue_state': json_payload}) if (serial and serial not in existing_serials and serial not in (hass.data[DATA_ALEXAMEDIA] ['accounts'] diff --git a/custom_components/alexa_media/media_player.py b/custom_components/alexa_media/media_player.py index 3578ecfd..5abef99d 100644 --- a/custom_components/alexa_media/media_player.py +++ b/custom_components/alexa_media/media_player.py @@ -207,6 +207,26 @@ def _handle_event(self, event): == "ONLINE") if (self.hass and self.schedule_update_ha_state): self.schedule_update_ha_state() + if 'queue_state' in event.data: + queue_state = event.data['queue_state'] + if (queue_state['dopplerId'] + ['deviceSerialNumber'] == self.device_serial_number): + if ('trackOrderChanged' in queue_state and + not queue_state['trackOrderChanged'] and + 'loopMode' in queue_state): + self._repeat = (queue_state['loopMode'] + == 'LOOP_QUEUE') + _LOGGER.debug("%s repeat updated to: %s %s", + self.name, + self._repeat, + queue_state['loopMode']) + elif 'playBackOrder' in queue_state: + self._shuffle = (queue_state['playBackOrder'] + == 'SHUFFLE_ALL') + _LOGGER.debug("%s shuffle updated to: %s %s", + self.name, + self._shuffle, + queue_state['playBackOrder']) def _clear_media_details(self): """Set all Media Items to None.""" diff --git a/custom_components/alexa_media/switch.py b/custom_components/alexa_media/switch.py index ebebe8fe..30da5c00 100644 --- a/custom_components/alexa_media/switch.py +++ b/custom_components/alexa_media/switch.py @@ -12,6 +12,7 @@ from homeassistant import util from homeassistant.components.switch import SwitchDevice +from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.helpers.event import call_later from . import DATA_ALEXAMEDIA @@ -60,7 +61,9 @@ def setup_platform(hass, config, add_devices_callback, for (switch_key, class_) in SWITCH_TYPES: alexa_client = class_(account_dict['entities'] ['media_player'] - [key]) # type: AlexaMediaSwitch + [key], + hass, + account) # type: AlexaMediaSwitch (hass.data[DATA_ALEXAMEDIA] ['accounts'] [account] @@ -80,28 +83,55 @@ class AlexaMediaSwitch(SwitchDevice): """Representation of a Alexa Media switch.""" def __init__(self, + hass, client, switch_property, switch_function, + account, name="Alexa"): """Initialize the Alexa Switch device.""" # Class info self._client = client + self._account = account self._name = name self._switch_property = switch_property + self._state = False self._switch_function = switch_function _LOGGER.debug("Creating %s switch for %s", name, client) + # Register event handler on bus + hass.bus.listen(('{}_{}'.format(ALEXA_DOMAIN, + client.account))[0:32], + self._handle_event) + + def _handle_event(self, event): + """Handle events. + + This will update PUSH_MEDIA_QUEUE_CHANGE events to see if the switch + should be updated. + """ + if 'queue_state' in event.data: + queue_state = event.data['queue_state'] + if (queue_state['dopplerId'] + ['deviceSerialNumber'] == self._client.unique_id): + self._state = getattr(self._client, self._switch_property) + self.schedule_update_ha_state() def _set_switch(self, state, **kwargs): success = self._switch_function(state) # if function returns success, make immediate state change if success: - self._switch_property = state + setattr(self._client, self._switch_property, state) + _LOGGER.debug("Switch set to %s based on %s", + getattr(self._client, + self._switch_property), + state) + self.schedule_update_ha_state() + @property def is_on(self): """Return true if on.""" - return self._switch_property + return getattr(self._client, self._switch_property) def turn_on(self, **kwargs): """Turn on switch.""" @@ -116,38 +146,57 @@ def name(self): """Return the name of the switch.""" return "{} {} switch".format(self._client.name, self._name) + @property + def should_poll(self): + """Return the polling state.""" + return not (self.hass.data[DATA_ALEXAMEDIA] + ['accounts'][self._account]['websocket']) + + def update(self): + """Update state.""" + try: + self.schedule_update_ha_state() + except NoEntitySpecifiedError: + pass # we ignore this due to a harmless startup race condition + class DNDSwitch(AlexaMediaSwitch): """Representation of a Alexa Media Do Not Disturb switch.""" - def __init__(self, client): + def __init__(self, client, hass, account): """Initialize the Alexa Switch.""" # Class info - super().__init__(client, - client.dnd_state, + super().__init__(hass, + client, + 'dnd_state', client.alexa_api.set_dnd_state, + account, "do not disturb") class ShuffleSwitch(AlexaMediaSwitch): """Representation of a Alexa Media Shuffle switch.""" - def __init__(self, client): + def __init__(self, client, hass, account): """Initialize the Alexa Switch.""" # Class info - super().__init__(client, - client.shuffle_state, + super().__init__(hass, + client, + 'shuffle_state', client.alexa_api.shuffle, + account, "shuffle") class RepeatSwitch(AlexaMediaSwitch): """Representation of a Alexa Media Repeat switch.""" - def __init__(self, client): + def __init__(self, client, hass, account): """Initialize the Alexa Switch.""" # Class info - super().__init__(client, - client.repeat_state, + super().__init__(hass, + client, + 'repeat_state', client.alexa_api.repeat, + account, "repeat") From a10d7e62171e16b52041b817ae7e11e09c0e3221 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 6 Aug 2019 22:44:18 -0700 Subject: [PATCH 09/20] fix(guard): increase delay to check state on voice --- custom_components/alexa_media/alarm_control_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/alexa_media/alarm_control_panel.py b/custom_components/alexa_media/alarm_control_panel.py index c7066d37..3c698ee1 100644 --- a/custom_components/alexa_media/alarm_control_panel.py +++ b/custom_components/alexa_media/alarm_control_panel.py @@ -118,7 +118,7 @@ def _handle_event(self, event): Used instead of polling. """ if 'push_activity' in event.data: - call_later(self.hass, 1, lambda _: + call_later(self.hass, 2, lambda _: self.refresh(no_throttle=True)) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) From e8ab5d1693784ca315c5848d4155d29980ac8cc9 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 6 Aug 2019 22:44:48 -0700 Subject: [PATCH 10/20] fix(guard): schedule HA update after processing voice --- custom_components/alexa_media/alarm_control_panel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/alexa_media/alarm_control_panel.py b/custom_components/alexa_media/alarm_control_panel.py index 3c698ee1..507b10bd 100644 --- a/custom_components/alexa_media/alarm_control_panel.py +++ b/custom_components/alexa_media/alarm_control_panel.py @@ -155,6 +155,7 @@ def refresh(self): else: self._state = STATE_ALARM_DISARMED _LOGGER.debug("%s: Alarm State: %s", self.account, self.state) + self.schedule_update_ha_state() def alarm_disarm(self, code=None): # pylint: disable=unexpected-keyword-arg From f213820d4de2644a7f89876af68d6fc093688a9e Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 6 Aug 2019 21:44:41 -0700 Subject: [PATCH 11/20] chore(guard): obfuscate email in debug message --- custom_components/alexa_media/alarm_control_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/alexa_media/alarm_control_panel.py b/custom_components/alexa_media/alarm_control_panel.py index 507b10bd..39f9c700 100644 --- a/custom_components/alexa_media/alarm_control_panel.py +++ b/custom_components/alexa_media/alarm_control_panel.py @@ -37,7 +37,7 @@ def setup_platform(hass, config, add_devices_callback, # type: AlexaAlarmControlPanel if not (alexa_client and alexa_client.unique_id): _LOGGER.debug("%s: Skipping creation of uninitialized device: %s", - account, + hide_email(account), alexa_client) continue devices.append(alexa_client) From f84d2ba5111da73f767a04a49d75682c374e716f Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 6 Aug 2019 21:45:26 -0700 Subject: [PATCH 12/20] fix(guard): add additional checks for failed guard access --- custom_components/alexa_media/alarm_control_panel.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/alexa_media/alarm_control_panel.py b/custom_components/alexa_media/alarm_control_panel.py index 39f9c700..686072f9 100644 --- a/custom_components/alexa_media/alarm_control_panel.py +++ b/custom_components/alexa_media/alarm_control_panel.py @@ -84,14 +84,15 @@ def __init__(self, login, hass): self._should_poll = False self._attrs = {} - data = self.alexa_api.get_guard_details(self._login) try: + from simplejson import JSONDecodeError + data = self.alexa_api.get_guard_details(self._login) guard_dict = (data['locationDetails'] ['locationDetails']['Default_Location'] ['amazonBridgeDetails']['amazonBridgeDetails'] ['LambdaBridge_AAA/OnGuardSmartHomeBridgeService'] ['applianceDetails']['applianceDetails']) - except (KeyError, TypeError): + except (KeyError, TypeError, JSONDecodeError): guard_dict = {} for key, value in guard_dict.items(): if value['modelName'] == "REDROCK_GUARD_PANEL": From 9a388f502d47e618128aa0e79a1dc823802e078b Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 6 Aug 2019 21:47:05 -0700 Subject: [PATCH 13/20] fix(media_player): fix bug where get_last_called called before init --- custom_components/alexa_media/media_player.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/custom_components/alexa_media/media_player.py b/custom_components/alexa_media/media_player.py index 54933003..1df70706 100644 --- a/custom_components/alexa_media/media_player.py +++ b/custom_components/alexa_media/media_player.py @@ -351,12 +351,15 @@ def _get_source_list(self): return ['Local Speaker'] + sources def _get_last_called(self): - last_called_serial = (None if self.hass is None else - (self.hass.data[DATA_ALEXAMEDIA] - ['accounts'] - [self._login.email] - ['last_called'] - ['serialNumber'])) + try: + last_called_serial = (None if self.hass is None else + (self.hass.data[DATA_ALEXAMEDIA] + ['accounts'] + [self._login.email] + ['last_called'] + ['serialNumber'])) + except TypeError: + last_called_serial = None _LOGGER.debug("%s: Last_called check: self: %s reported: %s", self._device_name, hide_serial(self._device_serial_number), From c38a753dfddb102d7e1ba6c2a145e4001c5afb7c Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 6 Aug 2019 22:05:56 -0700 Subject: [PATCH 14/20] fix(captcha): add captcha to handle OTP selection page --- custom_components/alexa_media/__init__.py | 36 +++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index bc6355ec..4720452c 100644 --- a/custom_components/alexa_media/__init__.py +++ b/custom_components/alexa_media/__init__.py @@ -112,16 +112,15 @@ def setup_platform_callback(hass, config, login, callback_data): request_configuration and configuration_callback """ _LOGGER.debug(("Status: %s got captcha: %s securitycode: %s" - " Claimsoption: %s VerificationCode: %s"), + " Claimsoption: %s AuthSelectOption: %s " + " VerificationCode: %s"), login.status, callback_data.get('captcha'), callback_data.get('securitycode'), callback_data.get('claimsoption'), + callback_data.get('authselectoption'), callback_data.get('verificationcode')) - login.login(captcha=callback_data.get('captcha'), - securitycode=callback_data.get('securitycode'), - claimsoption=callback_data.get('claimsoption'), - verificationcode=callback_data.get('verificationcode')) + login.login(data=callback_data) test_login_status(hass, config, login, setup_platform_callback) @@ -136,6 +135,14 @@ def configuration_callback(callback_data): login, callback_data) status = login.status email = login.email + # links = "" + footer = "" + if 'error_message' in status and status['error_message']: + footer = ('\nNOTE: Actual Amazon error message in red below. ' + 'Remember password will be provided automatically' + ' and Amazon error message normally appears first!') + # if login.links: + # links = '\n\nGo to link with link# (e.g. link0)\n' + login.links # Get Captcha if (status and 'captcha_image_url' in status and status['captcha_image_url'] is not None): @@ -174,6 +181,22 @@ def configuration_callback(callback_data): ) else: configuration_callback({}) + elif (status and 'authselect_required' in status and + status['authselect_required']): # Get picker method + options = status['authselect_message'] + if options: + config_id = configurator.request_config( + "Alexa Media Player - OTP Method - {}".format(email), + configuration_callback, + description=('Please select the OTP method. ' + '(e.g., 0, 1).
{}'.format(options) + # + links + + footer), + submit_caption="Confirm", + fields=[{'id': 'authselectoption', 'name': 'Option'}] + ) + else: + configuration_callback({}) elif (status and 'verificationcode_required' in status and status['verificationcode_required']): # Get picker method config_id = configurator.request_config( @@ -218,6 +241,9 @@ def test_login_status(hass, config, login, elif ('claimspicker_required' in login.status and login.status['claimspicker_required']): _LOGGER.debug("Creating configurator to select verification option") + elif ('authselect_required' in login.status and + login.status['authselect_required']): + _LOGGER.debug("Creating configurator to select OTA option") elif ('verificationcode_required' in login.status and login.status['verificationcode_required']): _LOGGER.debug("Creating configurator to enter verification code") From 04299d70ab51fbc81d2d94b91a1c2b7f5dbe7fe7 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 6 Aug 2019 22:06:28 -0700 Subject: [PATCH 15/20] style(configurator): update messaging --- custom_components/alexa_media/__init__.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index 4720452c..1bef5ac4 100644 --- a/custom_components/alexa_media/__init__.py +++ b/custom_components/alexa_media/__init__.py @@ -150,7 +150,9 @@ def configuration_callback(callback_data): "Alexa Media Player - Captcha - {}".format(email), configuration_callback, description=('Please enter the text for the captcha.' - ' Please enter anything if the image is missing.' + ' Please hit confirm to reload image.' + # + links + + footer ), description_image=status['captcha_image_url'], submit_caption="Confirm", @@ -161,7 +163,9 @@ def configuration_callback(callback_data): config_id = configurator.request_config( "Alexa Media Player - 2FA - {}".format(email), configuration_callback, - description=('Please enter your Two-Factor Security code.'), + description=('Please enter your Two-Factor Security code.' + # + links + + footer), submit_caption="Confirm", fields=[{'id': 'securitycode', 'name': 'Security Code'}] ) @@ -173,9 +177,9 @@ def configuration_callback(callback_data): "Alexa Media Player - Verification Method - {}".format(email), configuration_callback, description=('Please select the verification method. ' - '(e.g., sms or email).
{}').format( - options - ), + '(e.g., sms or email).\n{}'.format(options) + # + links + + footer), submit_caption="Confirm", fields=[{'id': 'claimsoption', 'name': 'Option'}] ) @@ -202,7 +206,9 @@ def configuration_callback(callback_data): config_id = configurator.request_config( "Alexa Media Player - Verification Code - {}".format(email), configuration_callback, - description=('Please enter received verification code.'), + description=('Please enter received verification code.' + # + links + + footer), submit_caption="Confirm", fields=[{'id': 'verificationcode', 'name': 'Verification Code'}] ) From 4b552f65f8b60413640f8abab0f68d870d95cf79 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 6 Aug 2019 23:16:40 -0700 Subject: [PATCH 16/20] chore(alexapy): update to 0.7.1 --- custom_components/alexa_media/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/alexa_media/manifest.json b/custom_components/alexa_media/manifest.json index b49effb1..edd2de47 100644 --- a/custom_components/alexa_media/manifest.json +++ b/custom_components/alexa_media/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://github.com/keatontaylor/alexa_media_player/wiki", "dependencies": [], "codeowners": ["@keatontaylor", "@alandtse"], - "requirements": ["alexapy==0.7.0"] + "requirements": ["alexapy==0.7.1"] } From d0d7a9ba44b708aedfd286b554c9ee2eb1efe8c7 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Wed, 7 Aug 2019 20:32:12 -0700 Subject: [PATCH 17/20] fix(wshandler): properly handle entryId does not contain # --- custom_components/alexa_media/__init__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index 2c635e1f..605b7036 100644 --- a/custom_components/alexa_media/__init__.py +++ b/custom_components/alexa_media/__init__.py @@ -496,13 +496,16 @@ def ws_handler(message_obj): serial = None if command == 'PUSH_ACTIVITY': # Last_Alexa Updated - serial = (json_payload - ['key'] - ['entryId']).split('#')[2] - last_called = { - 'serialNumber': serial, - 'timestamp': json_payload['timestamp'] - } + if (json_payload + ['key'] + ['entryId']).find('#') != -1: + serial = (json_payload + ['key'] + ['entryId']).split('#')[2] + last_called = { + 'serialNumber': serial, + 'timestamp': json_payload['timestamp'] + } if (serial and serial in existing_serials): update_last_called(login_obj, last_called) hass.bus.fire(('{}_{}'.format(DOMAIN, From a8479e31060e51050f1e59a5d47e02e9cd262b99 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Wed, 7 Aug 2019 20:33:12 -0700 Subject: [PATCH 18/20] chore(const): bump version --- custom_components/alexa_media/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/alexa_media/const.py b/custom_components/alexa_media/const.py index 64b3cbe8..8c4e8747 100644 --- a/custom_components/alexa_media/const.py +++ b/custom_components/alexa_media/const.py @@ -9,7 +9,7 @@ """ from datetime import timedelta -__version__ = '1.3.1' +__version__ = '1.4.0' PROJECT_URL = "https://github.com/keatontaylor/alexa_media_player/" ISSUE_URL = "{}issues".format(PROJECT_URL) From 241bc1c47c59ac6fbb3a9d0917e5a1c9a622406a Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Wed, 7 Aug 2019 20:33:36 -0700 Subject: [PATCH 19/20] fix(switch): add unique_id function --- custom_components/alexa_media/switch.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/custom_components/alexa_media/switch.py b/custom_components/alexa_media/switch.py index 30da5c00..8de3c1f7 100644 --- a/custom_components/alexa_media/switch.py +++ b/custom_components/alexa_media/switch.py @@ -141,6 +141,11 @@ def turn_off(self, **kwargs): """Turn off switch.""" self._set_switch(False, **kwargs) + @property + def unique_id(self): + """Return the unique ID.""" + return self._client.unique_id + '_' + self._name + @property def name(self): """Return the name of the switch.""" From 021962d8ba2a857226f1ddff8650943c124579cd Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Wed, 7 Aug 2019 20:33:49 -0700 Subject: [PATCH 20/20] style(switch): clean up whitespace --- custom_components/alexa_media/switch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/alexa_media/switch.py b/custom_components/alexa_media/switch.py index 8de3c1f7..c4edfeca 100644 --- a/custom_components/alexa_media/switch.py +++ b/custom_components/alexa_media/switch.py @@ -127,7 +127,6 @@ def _set_switch(self, state, **kwargs): state) self.schedule_update_ha_state() - @property def is_on(self): """Return true if on."""