diff --git a/config/configuration.yaml b/config/configuration.yaml
index fd3d29e0..5579c9e0 100755
--- a/config/configuration.yaml
+++ b/config/configuration.yaml
@@ -233,13 +233,13 @@ climate:
# name: Living Room TV
# scan_interval: 180
-tts:
- - platform: amazon_polly
- aws_access_key_id: !secret aws_access_key_ID
- aws_secret_access_key: !secret aws_secret_access_key
- region_name: 'us-east-1'
- text_type: ssml
- cache: True
+# tts:
+# - platform: amazon_polly
+# aws_access_key_id: !secret aws_access_key_ID
+# aws_secret_access_key: !secret aws_secret_access_key
+# region_name: 'us-east-1'
+# text_type: ssml
+# cache: True
# cache_dir: /data/tts
wink:
diff --git a/config/custom_components/media_player/alexa.py b/config/custom_components/media_player/alexa.py
new file mode 100755
index 00000000..02e82beb
--- /dev/null
+++ b/config/custom_components/media_player/alexa.py
@@ -0,0 +1,1077 @@
+"""
+Support to interface with Alexa Devices.
+
+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
+VERSION 0.9.5
+"""
+import logging
+
+from datetime import timedelta
+
+import requests
+import voluptuous as vol
+
+from homeassistant import util
+from homeassistant.components.media_player import (
+ MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK,
+ SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK,
+ SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE,
+ SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_SET,
+ MediaPlayerDevice, DOMAIN, MEDIA_PLAYER_SCHEMA,
+ SUPPORT_SELECT_SOURCE)
+from homeassistant.const import (
+ CONF_EMAIL, CONF_PASSWORD, CONF_URL,
+ STATE_IDLE, STATE_STANDBY, STATE_PAUSED,
+ STATE_PLAYING)
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.service import extract_entity_ids
+from homeassistant.helpers.event import track_utc_time_change
+# from homeassistant.util.json import load_json, save_json
+# from homeassistant.util import dt as dt_util
+
+SUPPORT_ALEXA = (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK |
+ SUPPORT_NEXT_TRACK | SUPPORT_STOP |
+ SUPPORT_VOLUME_SET | SUPPORT_PLAY |
+ SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF |
+ SUPPORT_VOLUME_MUTE | SUPPORT_PAUSE |
+ SUPPORT_SELECT_SOURCE)
+_CONFIGURING = []
+_LOGGER = logging.getLogger(__name__)
+
+REQUIREMENTS = ['beautifulsoup4==4.6.0', 'simplejson==3.16.0']
+
+MIN_TIME_BETWEEN_SCANS = timedelta(seconds=15)
+MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
+
+ALEXA_DATA = "alexa_media"
+
+SERVICE_ALEXA_TTS = 'alexa_tts'
+
+ATTR_MESSAGE = 'message'
+ALEXA_TTS_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
+ vol.Required(ATTR_MESSAGE): cv.string,
+})
+
+CONF_DEBUG = 'debug'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_EMAIL): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Required(CONF_URL): cv.string,
+ vol.Optional(CONF_DEBUG, default=False): cv.boolean,
+})
+
+
+def request_configuration(hass, config, setup_platform_callback,
+ status=None):
+ """Request configuration steps from the user."""
+ configurator = hass.components.configurator
+
+ async def configuration_callback(callback_data):
+ """Handle the submitted configuration."""
+ hass.async_add_job(setup_platform_callback, callback_data)
+
+ # Get Captcha
+ if (status and 'captcha_image_url' in status and
+ status['captcha_image_url'] is not None):
+ config_id = configurator.request_config(
+ "Alexa Media Player - Captcha", configuration_callback,
+ description=('Please enter the text for the captcha.'
+ ' Please enter anything if the image is missing.'
+ ),
+ description_image=status['captcha_image_url'],
+ submit_caption="Confirm",
+ fields=[{'id': 'captcha', 'name': 'Captcha'}]
+ )
+ elif (status and 'securitycode_required' in status and
+ status['securitycode_required']): # Get 2FA code
+ config_id = configurator.request_config(
+ "Alexa Media Player - 2FA", configuration_callback,
+ description=('Please enter your Two-Factor Security code.'),
+ submit_caption="Confirm",
+ fields=[{'id': 'securitycode', 'name': 'Security Code'}]
+ )
+ elif (status and 'claimspicker_required' in status and
+ status['claimspicker_required']): # Get picker method
+ options = status['claimspicker_message']
+ config_id = configurator.request_config(
+ "Alexa Media Player - Verification Method", configuration_callback,
+ description=('Please select the verification method. '
+ '(e.g., sms or email).
{}').format(
+ options
+ ),
+ submit_caption="Confirm",
+ fields=[{'id': 'claimsoption', 'name': 'Option'}]
+ )
+ elif (status and 'verificationcode_required' in status and
+ status['verificationcode_required']): # Get picker method
+ config_id = configurator.request_config(
+ "Alexa Media Player - Verification Code", configuration_callback,
+ description=('Please enter received verification code.'),
+ submit_caption="Confirm",
+ fields=[{'id': 'verificationcode', 'name': 'Verification Code'}]
+ )
+ else: # Check login
+ config_id = configurator.request_config(
+ "Alexa Media Player - Begin", configuration_callback,
+ description=('Please hit confirm to begin login attempt.'),
+ submit_caption="Confirm",
+ fields=[]
+ )
+ _CONFIGURING.append(config_id)
+ if (len(_CONFIGURING) > 0 and 'error_message' in status
+ and status['error_message']):
+ configurator.notify_errors( # use sync to delay next pop
+ _CONFIGURING[len(_CONFIGURING)-1], status['error_message'])
+ if (len(_CONFIGURING) > 1):
+ configurator.async_request_done(_CONFIGURING.pop(0))
+
+
+def setup_platform(hass, config, add_devices_callback,
+ discovery_info=None):
+ """Set up the Alexa platform."""
+ if ALEXA_DATA not in hass.data:
+ hass.data[ALEXA_DATA] = {}
+
+ email = config.get(CONF_EMAIL)
+ password = config.get(CONF_PASSWORD)
+ url = config.get(CONF_URL)
+
+ login = AlexaLogin(url, email, password, hass.config.path,
+ config.get(CONF_DEBUG))
+
+ async def setup_platform_callback(callback_data):
+ _LOGGER.debug(("Status: {} got captcha: {} securitycode: {}"
+ " Claimsoption: {} VerificationCode: {}").format(
+ login.status,
+ callback_data.get('captcha'),
+ callback_data.get('securitycode'),
+ callback_data.get('claimsoption'),
+ 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'))
+ testLoginStatus(hass, config, add_devices_callback, login,
+ setup_platform_callback)
+
+ testLoginStatus(hass, config, add_devices_callback, login,
+ setup_platform_callback)
+
+
+def testLoginStatus(hass, config, add_devices_callback, login,
+ setup_platform_callback):
+ """Test the login status."""
+ if 'login_successful' in login.status and login.status['login_successful']:
+ _LOGGER.debug("Setting up Alexa devices")
+ hass.async_add_job(setup_alexa, hass, config,
+ add_devices_callback, login)
+ return
+ elif ('captcha_required' in login.status and
+ login.status['captcha_required']):
+ _LOGGER.debug("Creating configurator to request captcha")
+ elif ('securitycode_required' in login.status and
+ login.status['securitycode_required']):
+ _LOGGER.debug("Creating configurator to request 2FA")
+ elif ('claimspicker_required' in login.status and
+ login.status['claimspicker_required']):
+ _LOGGER.debug("Creating configurator to select verification option")
+ elif ('verificationcode_required' in login.status and
+ login.status['verificationcode_required']):
+ _LOGGER.debug("Creating configurator to enter verification code")
+ elif ('login_failed' in login.status and
+ login.status['login_failed']):
+ _LOGGER.debug("Creating configurator to start new login attempt")
+ hass.async_add_job(request_configuration, hass, config,
+ setup_platform_callback,
+ login.status)
+
+
+def setup_alexa(hass, config, add_devices_callback, login_obj):
+ """Set up a alexa api based on host parameter."""
+ alexa_clients = hass.data[ALEXA_DATA]
+ # alexa_sessions = {}
+ track_utc_time_change(hass, lambda now: update_devices(), second=30)
+
+ url = config.get(CONF_URL)
+
+ @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
+ def update_devices():
+ """Update the devices objects."""
+ devices = AlexaAPI.get_devices(url, login_obj._session)
+ bluetooth = AlexaAPI.get_bluetooth(url, login_obj._session)
+
+ if ((devices is None or bluetooth is None)
+ and len(_CONFIGURING) == 0):
+ _LOGGER.debug("Alexa API disconnected; attempting to relogin")
+ login_obj.login_with_cookie()
+
+ new_alexa_clients = []
+ available_client_ids = []
+ for device in devices:
+
+ for b_state in bluetooth['bluetoothStates']:
+ if device['serialNumber'] == b_state['deviceSerialNumber']:
+ device['bluetooth_state'] = b_state
+
+ available_client_ids.append(device['serialNumber'])
+
+ if device['serialNumber'] not in alexa_clients:
+ new_client = AlexaClient(config, login_obj._session, device,
+ update_devices, url)
+ alexa_clients[device['serialNumber']] = new_client
+ new_alexa_clients.append(new_client)
+ elif device['online']:
+ alexa_clients[device['serialNumber']].refresh(device)
+
+ if new_alexa_clients:
+ def tts_handler(call):
+ for alexa in service_to_entities(call):
+ if call.service == SERVICE_ALEXA_TTS:
+ message = call.data.get(ATTR_MESSAGE)
+ alexa.send_tts(message)
+
+ def service_to_entities(call):
+ """Return the known devices that a service call mentions."""
+ entity_ids = extract_entity_ids(hass, call)
+ if entity_ids:
+ entities = [entity for entity in new_alexa_clients
+ if entity.entity_id in entity_ids]
+ else:
+ entities = None
+
+ return entities
+
+ hass.services.register(DOMAIN, SERVICE_ALEXA_TTS, tts_handler,
+ schema=ALEXA_TTS_SCHEMA)
+ add_devices_callback(new_alexa_clients)
+
+ update_devices()
+ # Clear configurator. We delay till here to avoid leaving a modal orphan
+ global _CONFIGURING
+ for config_id in _CONFIGURING:
+ configurator = hass.components.configurator
+ configurator.async_request_done(config_id)
+ _CONFIGURING = []
+
+
+class AlexaClient(MediaPlayerDevice):
+ """Representation of a Alexa device."""
+
+ def __init__(self, config, session, device, update_devices, url):
+ """Initialize the Alexa device."""
+ # Class info
+ self.alexa_api = AlexaAPI(self, session, url)
+
+ self.update_devices = update_devices
+ # Device info
+ self._device = None
+ self._device_name = None
+ self._device_serial_number = None
+ self._device_type = None
+ self._device_family = None
+ self._device_owner_customer_id = None
+ self._software_version = None
+ self._available = None
+ self._capabilities = []
+ # Media
+ self._session = None
+ self._media_duration = None
+ self._media_image_url = None
+ self._media_title = None
+ self._media_pos = None
+ self._media_album_name = None
+ self._media_artist = None
+ self._player_state = None
+ self._media_is_muted = None
+ self._media_vol_level = None
+ self._previous_volume = None
+ self._source = None
+ self._source_list = []
+ self.refresh(device)
+
+ def _clear_media_details(self):
+ """Set all Media Items to None."""
+ # General
+ self._media_duration = None
+ self._media_image_url = None
+ self._media_title = None
+ self._media_pos = None
+ self._media_album_name = None
+ self._media_artist = None
+ self._media_player_state = None
+ self._media_is_muted = None
+ self._media_vol_level = None
+
+ def refresh(self, device):
+ """Refresh key device data."""
+ self._device = device
+ self._device_name = device['accountName']
+ self._device_family = device['deviceFamily']
+ self._device_type = device['deviceType']
+ self._device_serial_number = device['serialNumber']
+ self._device_owner_customer_id = device['deviceOwnerCustomerId']
+ self._software_version = device['softwareVersion']
+ self._available = device['online']
+ self._capabilities = device['capabilities']
+ self._bluetooth_state = device['bluetooth_state']
+ self._source = self._get_source()
+ self._source_list = self._get_source_list()
+ session = self.alexa_api.get_state()
+
+ self._clear_media_details()
+ # update the session if it exists; not doing relogin here
+ if session is not None:
+ self._session = session
+ if 'playerInfo' in self._session:
+ self._session = self._session['playerInfo']
+ if self._session['state'] is not None:
+ self._media_player_state = self._session['state']
+ self._media_pos = (self._session['progress']['mediaProgress']
+ if (self._session['progress'] is not None
+ and 'mediaProgress' in
+ self._session['progress'])
+ else None)
+ self._media_is_muted = (self._session['volume']['muted']
+ if (self._session['volume'] is not None
+ and 'muted' in
+ self._session['volume'])
+ else None)
+ self._media_vol_level = (self._session['volume']
+ ['volume'] / 100
+ if(self._session['volume'] is not None
+ and 'volume' in
+ self._session['volume'])
+ else None)
+ self._media_title = (self._session['infoText']['title']
+ if (self._session['infoText'] is not None
+ and 'title' in
+ self._session['infoText'])
+ else None)
+ self._media_artist = (self._session['infoText']['subText1']
+ if (self._session['infoText'] is not None
+ and 'subText1' in
+ self._session['infoText'])
+ else None)
+ self._media_album_name = (self._session['infoText']['subText2']
+ if (self._session['infoText'] is not
+ None and 'subText2' in
+ self._session['infoText'])
+ else None)
+ self._media_image_url = (self._session['mainArt']['url']
+ if (self._session['mainArt'] is not
+ None and 'url' in
+ self._session['mainArt'])
+ else None)
+ self._media_duration = (self._session['progress']
+ ['mediaLength']
+ if (self._session['progress'] is not
+ None and 'mediaLength' in
+ self._session['progress'])
+ else None)
+
+ @property
+ def source(self):
+ """Return the current input source."""
+ return self._source
+
+ @property
+ def source_list(self):
+ """List of available input sources."""
+ return self._source_list
+
+ def select_source(self, source):
+ """Select input source."""
+ if source == 'Local Speaker':
+ self.alexa_api.disconnect_bluetooth()
+ self._source = 'Local Speaker'
+ elif self._bluetooth_state['pairedDeviceList'] is not None:
+ for devices in self._bluetooth_state['pairedDeviceList']:
+ if devices['friendlyName'] == source:
+ self.alexa_api.set_bluetooth(devices['address'])
+ self._source = source
+
+ def _get_source(self):
+ source = 'Local Speaker'
+ if self._bluetooth_state['pairedDeviceList'] is not None:
+ for device in self._bluetooth_state['pairedDeviceList']:
+ if device['connected'] is True:
+ return device['friendlyName']
+ return source
+
+ def _get_source_list(self):
+ sources = []
+ if self._bluetooth_state['pairedDeviceList'] is not None:
+ for devices in self._bluetooth_state['pairedDeviceList']:
+ sources.append(devices['friendlyName'])
+ return ['Local Speaker'] + sources
+
+ @property
+ def available(self):
+ """Return the availability of the client."""
+ return self._available
+
+ @property
+ def unique_id(self):
+ """Return the id of this Alexa client."""
+ return self.device_serial_number
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._device_name
+
+ @property
+ def device_serial_number(self):
+ """Return the machine identifier of the device."""
+ return self._device_serial_number
+
+ @property
+ def device(self):
+ """Return the device, if any."""
+ return self._device
+
+ @property
+ def session(self):
+ """Return the session, if any."""
+ return self._session
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ if self._media_player_state == 'PLAYING':
+ return STATE_PLAYING
+ elif self._media_player_state == 'PAUSED':
+ return STATE_PAUSED
+ elif self._media_player_state == 'IDLE':
+ return STATE_IDLE
+ return STATE_STANDBY
+
+ def update(self):
+ """Get the latest details."""
+ self.update_devices(no_throttle=True)
+
+ @property
+ def media_content_type(self):
+ """Return the content type of current playing media."""
+ if self.state in [STATE_PLAYING, STATE_PAUSED]:
+ return MEDIA_TYPE_MUSIC
+ return STATE_STANDBY
+
+ @property
+ def media_artist(self):
+ """Return the artist of current playing media, music track only."""
+ return self._media_artist
+
+ @property
+ def media_album_name(self):
+ """Return the album name of current playing media, music track only."""
+ return self._media_album_name
+
+ @property
+ def media_duration(self):
+ """Return the duration of current playing media in seconds."""
+ return self._media_duration
+
+ @property
+ def media_image_url(self):
+ """Return the image URL of current playing media."""
+ return self._media_image_url
+
+ @property
+ def media_title(self):
+ """Return the title of current playing media."""
+ return self._media_title
+
+ @property
+ def device_family(self):
+ """Return the make of the device (ex. Echo, Other)."""
+ return self._device_family
+
+ @property
+ def supported_features(self):
+ """Flag media player features that are supported."""
+ return SUPPORT_ALEXA
+
+ def set_volume_level(self, volume):
+ """Set volume level, range 0..1."""
+ if not (self.state in [STATE_PLAYING, STATE_PAUSED]
+ and self.available):
+ return
+ self.alexa_api.set_volume(volume)
+ self._media_vol_level = volume
+
+ @property
+ def volume_level(self):
+ """Return the volume level of the client (0..1)."""
+ return self._media_vol_level
+
+ @property
+ def is_volume_muted(self):
+ """Return boolean if volume is currently muted."""
+ if self.volume_level == 0:
+ return True
+ return False
+
+ def mute_volume(self, mute):
+ """Mute the volume.
+
+ Since we can't actually mute, we'll:
+ - On mute, store volume and set volume to 0
+ - On unmute, set volume to previously stored volume
+ """
+ if not (self.state == STATE_PLAYING and self.available):
+ return
+
+ self._media_is_muted = mute
+ if mute:
+ self._previous_volume = self.volume_level
+ self.alexa_api.set_volume(0)
+ else:
+ if self._previous_volume is not None:
+ self.alexa_api.set_volume(self._previous_volume)
+ else:
+ self.alexa_api.set_volume(50)
+
+ def media_play(self):
+ """Send play command."""
+ if not (self.state in [STATE_PLAYING, STATE_PAUSED]
+ and self.available):
+ return
+ self.alexa_api.play()
+
+ def media_pause(self):
+ """Send pause command."""
+ if not (self.state in [STATE_PLAYING, STATE_PAUSED]
+ and self.available):
+ return
+ self.alexa_api.pause()
+
+ def turn_off(self):
+ """Turn the client off."""
+ # Fake it since we can't turn the client off
+ self.media_pause()
+
+ def media_next_track(self):
+ """Send next track command."""
+ if not (self.state in [STATE_PLAYING, STATE_PAUSED]
+ and self.available):
+ return
+ self.alexa_api.next()
+
+ def media_previous_track(self):
+ """Send previous track command."""
+ if not (self.state in [STATE_PLAYING, STATE_PAUSED]
+ and self.available):
+ return
+ self.alexa_api.previous()
+
+ def send_tts(self, message):
+ """Send TTS to Device NOTE: Does not work on WHA Groups."""
+ self.alexa_api.send_tts(message)
+
+ def play_media(self, media_type, media_id, **kwargs):
+ """Send the play_media command to the media player."""
+ if media_type == "music":
+ self.alexa_api.send_tts("Sorry, text to speech can only be called "
+ " with the media player alexa tts service")
+ else:
+ self.alexa_api.play_music(media_type, media_id)
+
+ @property
+ def device_state_attributes(self):
+ """Return the scene state attributes."""
+ attr = {
+ 'available': self._available,
+ }
+ return attr
+
+
+class AlexaLogin():
+ """Class to handle login connection to Alexa."""
+
+ def __init__(self, url, email, password, configpath, debug=False):
+ """Set up initial connection and log in."""
+ self._url = url
+ self._email = email
+ self._password = password
+ self._session = None
+ self._data = None
+ self.status = {}
+ self._cookiefile = configpath("{}.pickle".format(ALEXA_DATA))
+ self._debugpost = configpath("{}post.html".format(ALEXA_DATA))
+ self._debugget = configpath("{}get.html".format(ALEXA_DATA))
+ self._lastreq = None
+ self._debug = debug
+
+ self.login_with_cookie()
+
+ def login_with_cookie(self):
+ """Attempt to login after loading cookie."""
+ import pickle
+ cookies = None
+
+ if (self._cookiefile):
+ try:
+ _LOGGER.debug(
+ "Trying cookie from file {}".format(
+ self._cookiefile))
+ with open(self._cookiefile, 'rb') as myfile:
+ cookies = pickle.load(myfile)
+ _LOGGER.debug("cookie loaded: {}".format(cookies))
+ except Exception as ex:
+ template = ("An exception of type {0} occurred."
+ " Arguments:\n{1!r}")
+ message = template.format(type(ex).__name__, ex.args)
+ _LOGGER.debug(
+ "Error loading pickled cookie from {}: {}".format(
+ self._cookiefile, message))
+
+ self.login(cookies=cookies)
+
+ def reset_login(self):
+ """Remove data related to existing login."""
+ self._session = None
+ self._data = None
+ self._lastreq = None
+ self.status = {}
+
+ def get_inputs(self, soup, searchfield={'name': 'signIn'}):
+ """Parse soup for form with searchfield."""
+ data = {}
+ form = soup.find('form', searchfield)
+ for field in form.find_all('input'):
+ try:
+ data[field['name']] = ""
+ data[field['name']] = field['value']
+ except: # noqa: E722 pylint: disable=bare-except
+ pass
+ return data
+
+ def test_loggedin(self, cookies=None):
+ """Function that will test the connection is logged in.
+
+ Attempts to get device list, and if unsuccessful login failed
+ """
+ if self._session is None:
+ '''initiate session'''
+
+ self._session = requests.Session()
+
+ '''define session headers'''
+ self._session.headers = {
+ 'User-Agent': ('Mozilla/5.0 (Windows NT 6.3; Win64; x64) '
+ 'AppleWebKit/537.36 (KHTML, like Gecko) '
+ 'Chrome/68.0.3440.106 Safari/537.36'),
+ 'Accept': ('text/html,application/xhtml+xml, '
+ 'application/xml;q=0.9,*/*;q=0.8'),
+ 'Accept-Language': '*'
+ }
+ self._session.cookies = cookies
+
+ get_resp = self._session.get('https://alexa.' + self._url +
+ '/api/devices-v2/device')
+ # with open(self._debugget, mode='wb') as localfile:
+ # localfile.write(get_resp.content)
+
+ try:
+ from json.decoder import JSONDecodeError
+ from simplejson import JSONDecodeError as SimpleJSONDecodeError
+ # Need to catch both as Python 3.5 appears to use simplejson
+ except ImportError:
+ JSONDecodeError = ValueError
+ try:
+ get_resp.json()
+ except (JSONDecodeError, SimpleJSONDecodeError) as ex:
+ # ValueError is necessary for Python 3.5 for some reason
+ template = ("An exception of type {0} occurred."
+ " Arguments:\n{1!r}")
+ message = template.format(type(ex).__name__, ex.args)
+ _LOGGER.debug("Not logged in: {}".format(message))
+ return False
+ _LOGGER.debug("Logged in.")
+ return True
+
+ def login(self, cookies=None, captcha=None, securitycode=None,
+ claimsoption=None, verificationcode=None):
+ """Login to Amazon."""
+ from bs4 import BeautifulSoup
+ import pickle
+
+ if (cookies is not None and self.test_loggedin(cookies)):
+ _LOGGER.debug("Using cookies to log in")
+ self.status = {}
+ self.status['login_successful'] = True
+ _LOGGER.debug("Log in successful with cookies")
+ return
+ else:
+ _LOGGER.debug("No valid cookies for log in; using credentials")
+ # site = 'https://www.' + self._url + '/gp/sign-in.html'
+ # use alexa site instead
+ site = 'https://alexa.' + self._url + '/api/devices-v2/device'
+ if self._session is None:
+ '''initiate session'''
+
+ self._session = requests.Session()
+
+ '''define session headers'''
+ self._session.headers = {
+ 'User-Agent': ('Mozilla/5.0 (Windows NT 6.3; Win64; x64) '
+ 'AppleWebKit/537.36 (KHTML, like Gecko) '
+ 'Chrome/68.0.3440.106 Safari/537.36'),
+ 'Accept': ('text/html,application/xhtml+xml, '
+ 'application/xml;q=0.9,*/*;q=0.8'),
+ 'Accept-Language': '*'
+ }
+
+ if self._lastreq is not None:
+ site = self._lastreq.url
+ _LOGGER.debug("Loaded last request to {} ".format(site))
+ html = self._lastreq.text
+ '''get BeautifulSoup object of the html of the login page'''
+ if self._debug:
+ with open(self._debugget, mode='wb') as localfile:
+ localfile.write(self._lastreq.content)
+
+ soup = BeautifulSoup(html, 'html.parser')
+ site = soup.find('form').get('action')
+ if site is None:
+ site = self._lastreq.url
+ elif site == 'verify':
+ import re
+ site = re.search(r'(.+)/(.*)',
+ self._lastreq.url).groups()[0] + "/verify"
+
+ if self._data is None:
+ resp = self._session.get(site)
+ self._lastreq = resp
+ if resp.history:
+ _LOGGER.debug("Get to {} was redirected to {}".format(
+ site,
+ resp.url))
+ self._session.headers['Referer'] = resp.url
+ else:
+ _LOGGER.debug("Get to {} was not redirected".format(site))
+ self._session.headers['Referer'] = site
+
+ html = resp.text
+ '''get BeautifulSoup object of the html of the login page'''
+ if self._debug:
+ with open(self._debugget, mode='wb') as localfile:
+ localfile.write(resp.content)
+
+ soup = BeautifulSoup(html, 'html.parser')
+ '''scrape login page to get all the inputs required for login'''
+ self._data = self.get_inputs(soup)
+ site = soup.find('form', {'name': 'signIn'}).get('action')
+
+ # _LOGGER.debug("Init Form Data: {}".format(self._data))
+
+ '''add username and password to the data for post request'''
+ '''check if there is an input field'''
+ if "email" in self._data:
+ self._data['email'] = self._email.encode('utf-8')
+ if "password" in self._data:
+ self._data['password'] = self._password.encode('utf-8')
+ if "rememberMe" in self._data:
+ self._data['rememberMe'] = "true".encode('utf-8')
+
+ status = {}
+ _LOGGER.debug(("Preparing post to {} Captcha: {}"
+ " SecurityCode: {} Claimsoption: {} "
+ "VerificationCode: {}").format(
+ site,
+ captcha,
+ securitycode,
+ claimsoption,
+ verificationcode
+ ))
+ if (captcha is not None and 'guess' in self._data):
+ self._data['guess'] = captcha.encode('utf-8')
+ if (securitycode is not None and 'otpCode' in self._data):
+ self._data['otpCode'] = securitycode.encode('utf-8')
+ self._data['rememberDevice'] = ""
+ if (claimsoption is not None and 'option' in self._data):
+ self._data['option'] = claimsoption.encode('utf-8')
+ if (verificationcode is not None and 'code' in self._data):
+ self._data['code'] = verificationcode.encode('utf-8')
+ self._session.headers['Content-Type'] = ("application/x-www-form-"
+ "urlencoded; charset=utf-8")
+ self._data.pop('', None)
+
+ if self._debug:
+ _LOGGER.debug("Cookies: {}".format(self._session.cookies))
+ _LOGGER.debug("Submit Form Data: {}".format(self._data))
+ _LOGGER.debug("Header: {}".format(self._session.headers))
+
+ '''submit post request with username/password and other needed info'''
+ post_resp = self._session.post(site, data=self._data)
+ self._session.headers['Referer'] = site
+
+ self._lastreq = post_resp
+ if self._debug:
+ with open(self._debugpost, mode='wb') as localfile:
+ localfile.write(post_resp.content)
+
+ post_soup = BeautifulSoup(post_resp.content, 'html.parser')
+
+ login_tag = post_soup.find('form', {'name': 'signIn'})
+ captcha_tag = post_soup.find(id="auth-captcha-image")
+
+ '''another login required and no captcha request? try once more.
+ This is a necessary hack as the first attempt always fails.
+ TODO: Figure out how to remove this hack
+ '''
+ if (login_tag is not None and captcha_tag is None):
+ login_url = login_tag.get("action")
+ _LOGGER.debug("Performing second login to: {}".format(
+ login_url))
+ post_resp = self._session.post(login_url,
+ data=self._data)
+ if self._debug:
+ with open(self._debugpost, mode='wb') as localfile:
+ localfile.write(post_resp.content)
+ post_soup = BeautifulSoup(post_resp.content, 'html.parser')
+ login_tag = post_soup.find('form', {'name': 'signIn'})
+ captcha_tag = post_soup.find(id="auth-captcha-image")
+
+ securitycode_tag = post_soup.find(id="auth-mfa-otpcode")
+ errorbox = (post_soup.find(id="auth-error-message-box")
+ if post_soup.find(id="auth-error-message-box") else
+ post_soup.find(id="auth-warning-message-box"))
+ claimspicker_tag = post_soup.find('form', {'name': 'claimspicker'})
+ verificationcode_tag = post_soup.find('form', {'action': 'verify'})
+
+ '''pull out Amazon error message'''
+
+ if errorbox:
+ error_message = errorbox.find('h4').string
+ for li in errorbox.findAll('li'):
+ error_message += li.find('span').string
+ _LOGGER.debug("Error message: {}".format(error_message))
+ status['error_message'] = error_message
+
+ if captcha_tag is not None:
+ _LOGGER.debug("Captcha requested")
+ status['captcha_required'] = True
+ status['captcha_image_url'] = captcha_tag.get('src')
+ self._data = self.get_inputs(post_soup)
+
+ elif securitycode_tag is not None:
+ _LOGGER.debug("2FA requested")
+ status['securitycode_required'] = True
+ self._data = self.get_inputs(post_soup, {'id': 'auth-mfa-form'})
+
+ elif claimspicker_tag is not None:
+ claims_message = ""
+ options_message = ""
+ for div in claimspicker_tag.findAll('div', 'a-row'):
+ claims_message += "{}\n".format(div.string)
+ for label in claimspicker_tag.findAll('label'):
+ value = (label.find('input')['value']) if label.find(
+ 'input') else ""
+ message = (label.find('span').string) if label.find(
+ 'span') else ""
+ valuemessage = ("Option: {} = `{}`.\n".format(
+ value, message)) if value != "" else ""
+ options_message += valuemessage
+ _LOGGER.debug("Verification method requested: {}".format(
+ claims_message, options_message))
+ status['claimspicker_required'] = True
+ status['claimspicker_message'] = options_message
+ self._data = self.get_inputs(post_soup, {'name': 'claimspicker'})
+ elif verificationcode_tag is not None:
+ _LOGGER.debug("Verification code requested:")
+ status['verificationcode_required'] = True
+ self._data = self.get_inputs(post_soup, {'action': 'verify'})
+ elif login_tag is not None:
+ login_url = login_tag.get("action")
+ _LOGGER.debug("Another login requested to: {}".format(
+ login_url))
+ status['login_failed'] = True
+
+ else:
+ _LOGGER.debug("Captcha/2FA not requested; confirming login.")
+ if self.test_loggedin():
+ _LOGGER.debug("Login confirmed; saving cookie to {}".format(
+ self._cookiefile))
+ status['login_successful'] = True
+ with open(self._cookiefile, 'wb') as myfile:
+ try:
+ pickle.dump(self._session.cookies, myfile)
+ except Exception as ex:
+ template = ("An exception of type {0} occurred."
+ " Arguments:\n{1!r}")
+ message = template.format(type(ex).__name__, ex.args)
+ _LOGGER.debug(
+ "Error saving pickled cookie to {}: {}".format(
+ self._cookiefile,
+ message))
+ else:
+ _LOGGER.debug("Login failed; check credentials")
+ status['login_failed'] = True
+
+ self.status = status
+
+
+class AlexaAPI():
+ """Class for accessing Alexa."""
+
+ def __init__(self, device, session, url):
+ """Initialize Alexa device."""
+ self._device = device
+ self._session = session
+ self._url = 'https://alexa.' + url
+
+ csrf = self._session.cookies.get_dict()['csrf']
+ self._session.headers['csrf'] = csrf
+
+ def _post_request(self, uri, data):
+ try:
+ self._session.post(self._url + uri, json=data)
+ except Exception as ex:
+ template = ("An exception of type {0} occurred."
+ " Arguments:\n{1!r}")
+ message = template.format(type(ex).__name__, ex.args)
+ _LOGGER.error("An error occured accessing the API: {}".format(
+ message))
+
+ def _get_request(self, uri, data=None):
+ try:
+ return self._session.get(self._url + uri, json=data)
+ except Exception as ex:
+ template = ("An exception of type {0} occurred."
+ " Arguments:\n{1!r}")
+ message = template.format(type(ex).__name__, ex.args)
+ _LOGGER.error("An error occured accessing the API: {}".format(
+ message))
+ return None
+
+ def play_music(self, provider_id, search_phrase):
+ """Play Music based on search."""
+ data = {
+ "behaviorId": "PREVIEW",
+ "sequenceJson": "{\"@type\": \
+ \"com.amazon.alexa.behaviors.model.Sequence\", \
+ \"startNode\":{\"@type\": \
+ \"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\", \
+ \"type\":\"Alexa.Music.PlaySearchPhrase\",\"operationPayload\": \
+ {\"deviceType\":\"" + self._device._device_type + "\", \
+ \"deviceSerialNumber\":\"" + self._device.unique_id +
+ "\",\"locale\":\"en-US\", \
+ \"customerId\":\"" + self._device._device_owner_customer_id +
+ "\", \"searchPhrase\": \"" + search_phrase + "\", \
+ \"sanitizedSearchPhrase\": \"" + search_phrase + "\", \
+ \"musicProviderId\": \"" + provider_id + "\"}}}",
+ "status": "ENABLED"
+ }
+ self._post_request('/api/behaviors/preview',
+ data=data)
+
+ def send_tts(self, message):
+ """Send message for TTS at speaker."""
+ data = {
+ "behaviorId": "PREVIEW",
+ "sequenceJson": "{\"@type\": \
+ \"com.amazon.alexa.behaviors.model.Sequence\", \
+ \"startNode\":{\"@type\": \
+ \"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\", \
+ \"type\":\"Alexa.Speak\",\"operationPayload\": \
+ {\"deviceType\":\"" + self._device._device_type + "\", \
+ \"deviceSerialNumber\":\"" + self._device.unique_id +
+ "\",\"locale\":\"en-US\", \
+ \"customerId\":\"" + self._device._device_owner_customer_id +
+ "\", \"textToSpeak\": \"" + message + "\"}}}",
+ "status": "ENABLED"
+ }
+ self._post_request('/api/behaviors/preview',
+ data=data)
+
+ def set_media(self, data):
+ """Select the media player."""
+ self._post_request('/api/np/command?deviceSerialNumber=' +
+ self._device.unique_id + '&deviceType=' +
+ self._device._device_type, data=data)
+
+ def previous(self):
+ """Play previous."""
+ self.set_media({"type": "PreviousCommand"})
+
+ def next(self):
+ """Play next."""
+ self.set_media({"type": "NextCommand"})
+
+ def pause(self):
+ """Pause."""
+ self.set_media({"type": "PauseCommand"})
+
+ def play(self):
+ """Play."""
+ self.set_media({"type": "PlayCommand"})
+
+ def set_volume(self, volume):
+ """Set volume."""
+ self.set_media({"type": "VolumeLevelCommand",
+ "volumeLevel": volume*100})
+
+ def get_state(self):
+ """Get state."""
+ try:
+ response = self._get_request('/api/np/player?deviceSerialNumber=' +
+ self._device.unique_id +
+ '&deviceType=' +
+ self._device._device_type +
+ '&screenWidth=2560')
+ return response.json()
+ except Exception as ex:
+ template = ("An exception of type {0} occurred."
+ " Arguments:\n{1!r}")
+ message = template.format(type(ex).__name__, ex.args)
+ _LOGGER.error("An error occured accessing the API: {}".format(
+ message))
+ return None
+
+ @staticmethod
+ def get_bluetooth(url, session):
+ """Get paired bluetooth devices."""
+ try:
+
+ response = session.get('https://alexa.' + url +
+ '/api/bluetooth?cached=false')
+ return response.json()
+ except Exception as ex:
+ template = ("An exception of type {0} occurred."
+ " Arguments:\n{1!r}")
+ message = template.format(type(ex).__name__, ex.args)
+ _LOGGER.error("An error occured accessing the API: {}".format(
+ message))
+ return None
+
+ def set_bluetooth(self, mac):
+ """Pair with bluetooth device with mac address."""
+ self._post_request('/api/bluetooth/pair-sink/' +
+ self._device._device_type + '/' +
+ self._device.unique_id,
+ data={"bluetoothDeviceAddress": mac})
+
+ def disconnect_bluetooth(self):
+ """Disconnect all bluetooth devices."""
+ self._post_request('/api/bluetooth/disconnect-sink/' +
+ self._device._device_type + '/' +
+ self._device.unique_id, data=None)
+
+ @staticmethod
+ def get_devices(url, session):
+ """Identify all Alexa devices."""
+ try:
+ response = session.get('https://alexa.' + url +
+ '/api/devices-v2/device')
+ return response.json()['devices']
+ except Exception as ex:
+ template = ("An exception of type {0} occurred."
+ " Arguments:\n{1!r}")
+ message = template.format(type(ex).__name__, ex.args)
+ _LOGGER.error("An error occured accessing the API: {}".format(
+ message))
+ return None
\ No newline at end of file
diff --git a/config/group/media_players.yaml b/config/group/media_players.yaml
index 506f3872..c73355b1 100755
--- a/config/group/media_players.yaml
+++ b/config/group/media_players.yaml
@@ -1,9 +1,13 @@
media_players:
entities:
- media_player.livingroomcc
- - media_player.whole_house
- media_player.living_room_tv
- media_player.living_room_ultra
- media_player.upstairs_living_room
- - media_player.alarm_clock
- media_player.bedroom_alarm_panel
+ - media_player.living_room
+ - media_player.office
+ - media_player.kitchen
+ - media_player.justin_room
+ - media_player.tap
+ - media_player.upstairs
diff --git a/config/packages/alexa_tts.yaml b/config/packages/alexa_tts.yaml
new file mode 100755
index 00000000..2d8e823b
--- /dev/null
+++ b/config/packages/alexa_tts.yaml
@@ -0,0 +1,82 @@
+#####################################################################
+# @package : alexa_tts
+# @description : alexa_tts settings
+#####################################################################
+homeassistant:
+ customize:
+ ################################################
+ ## Node Anchors
+ ################################################
+ package.node_anchors:
+ customize: &customize
+ package: 'alexa_tts'
+
+ hidden: &hidden
+ <<: *customize
+ hidden: true
+
+###################################################################
+##media_player
+###################################################################
+media_player:
+ - platform: alexa
+ email: !secret alexa_email
+ password: !secret alexa_password
+ url: amazon.com
+#################################################################
+##group
+#################################################################
+group:
+#
+ all_echoes:
+ view: yes
+ control: hidden
+ name: 'Alexa'
+ entities:
+ - media_player.living_room
+ - media_player.office
+ - media_player.kitchen
+ - media_player.justin_room
+ - media_player.tap
+ - media_player.upstairs
+#
+
+##################################################################
+#automation
+##################################################################
+# automation:
+# ## Announce what is typed as input
+# - alias: Alexa TTS
+# trigger:
+# platform: state
+# entity_id: input_select.alexa
+# action:
+# - service: media_player.volume_set
+# data_template:
+# entity_id: >
+# {% if is_state('input_select.alexa', 'Living Room') %}
+# media_player.livingroom
+# {% elif is_state('input_select.alexa', 'This device') %}
+# media_player.this_device
+# {% elif is_state('input_select.alexa', 'None') %}
+# false
+# {% endif %}
+# volume_level: '{{ states.input_number.alexa_volume.state | float /10 }}'
+# - service: media_player.alexa_tts
+# data_template:
+# entity_id: >
+# {% if is_state('input_select.alexa', 'Living Room') %}
+# media_player.livingroom
+# {% elif is_state('input_select.alexa', 'This device') %}
+# media_player.this_device
+# {% elif is_state('input_select.alexa', 'None') %}
+# false
+# {% endif %}
+# message: "{{ states.input_text.alexa_tts.state }}"
+#
+# - delay: '00:00:02'
+#
+# - service: input_select.select_option
+# data:
+# entity_id: input_select.alexa
+# option: None
diff --git a/config/script/speech_engine.yaml b/config/script/speech_engine.yaml
index c97778ee..c3982735 100755
--- a/config/script/speech_engine.yaml
+++ b/config/script/speech_engine.yaml
@@ -31,8 +31,9 @@ speech_engine:
media_player: >-
{% if media_player | length == 0 %}
{% set media_player = [
- 'media_player.livingroomcc',
- 'media_player.bedroom_alarm_panel'
+ 'media_player.living_room',
+ 'media_player.bedroom_alarm_panel',
+ 'media_player.office'
] %}
{% endif %}
diff --git a/config/script/speech_processing.yaml b/config/script/speech_processing.yaml
index 1b2a052d..36d1a555 100755
--- a/config/script/speech_processing.yaml
+++ b/config/script/speech_processing.yaml
@@ -58,19 +58,19 @@ speech_processing:
0.3
{% endif %}
- - service: tts.amazon_polly_say
+ - service: media_player.alexa_tts
data_template:
entity_id: >
{% if states.group.bed.state == 'off' %}
- media_player.livingroomCC
+ media_player.living_room
{% else %}
media_player.alarm_clock, media_player.bedroom_alarm_panel
{% endif %}
message: >-
-
+ #
{{ speech_message }}
-
- cache: true
+ #
+ # cache: true
- service: input_boolean.turn_off
data:
diff --git a/config/travis_secrets.yaml b/config/travis_secrets.yaml
index 330e7ebc..aa54c8f4 100755
--- a/config/travis_secrets.yaml
+++ b/config/travis_secrets.yaml
@@ -9,7 +9,9 @@ MQTT_username: MQTT_username
MQTT_password: password
ssl_certificate: fake_key.pem
ssl_key: fake_key.pem
-http_base_url: your.website.com
+http_base_url: vCloudInfo.com
+alexa_email: carlo@vCloudInfo.COM
+alexa_password: YOUTUBE_vCloudInfo
ifttt_key: iftttKEYPassphrase
camera1_url: http://192.168.10.21:88/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=admin&pwd=password
camera2_url: http://192.168.10.22:88/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=admin&pwd=password