diff --git a/homeassistant/components/frontend/www_static/images/config_wink.png b/homeassistant/components/frontend/www_static/images/config_wink.png new file mode 100644 index 00000000000..6b91f8cb58e Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/config_wink.png differ diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 7024291e7fe..58a6f51b67b 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -7,15 +7,21 @@ https://home-assistant.io/components/wink/ import logging import time import json +import os from datetime import timedelta import voluptuous as vol +import requests +from homeassistant.loader import get_component +from homeassistant.core import callback +from homeassistant.components.http import HomeAssistantView from homeassistant.helpers import discovery from homeassistant.helpers.event import track_time_interval from homeassistant.const import ( - CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL, CONF_EMAIL, CONF_PASSWORD, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + ATTR_BATTERY_LEVEL, CONF_EMAIL, CONF_PASSWORD, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, __version__) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -23,11 +29,10 @@ REQUIREMENTS = ['python-wink==1.3.1', 'pubnubsub-handler==1.0.2'] _LOGGER = logging.getLogger(__name__) -CHANNELS = [] - DOMAIN = 'wink' SUBSCRIPTION_HANDLER = None + CONF_CLIENT_ID = 'client_id' CONF_CLIENT_SECRET = 'client_secret' CONF_USER_AGENT = 'user_agent' @@ -37,8 +42,24 @@ CONF_DEFINED_BOTH_MSG = 'Remove access token to use oath2.' CONF_MISSING_OATH_MSG = 'Missing oath2 credentials.' CONF_TOKEN_URL = "https://winkbearertoken.appspot.com/token" +ATTR_ACCESS_TOKEN = 'access_token' +ATTR_REFRESH_TOKEN = 'refresh_token' +ATTR_CLIENT_ID = 'client_id' +ATTR_CLIENT_SECRET = 'client_secret' + +WINK_AUTH_CALLBACK_PATH = '/auth/wink/callback' +WINK_AUTH_START = '/auth/wink' +WINK_CONFIG_FILE = '.wink.conf' +USER_AGENT = "Manufacturer/Home-Assistant%s python/3 Wink/3" % (__version__) + +DEFAULT_CONFIG = { + 'client_id': 'CLIENT_ID_HERE', + 'client_secret': 'CLIENT_SECRET_HERE' +} + SERVICE_ADD_NEW_DEVICES = 'add_new_devices' SERVICE_REFRESH_STATES = 'refresh_state_from_wink' +SERVICE_KEEP_ALIVE = 'keep_pubnub_updates_flowing' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -52,11 +73,6 @@ CONFIG_SCHEMA = vol.Schema({ msg=CONF_MISSING_OATH_MSG): cv.string, vol.Exclusive(CONF_EMAIL, CONF_OATH, msg=CONF_DEFINED_BOTH_MSG): cv.string, - vol.Exclusive(CONF_ACCESS_TOKEN, CONF_OATH, - msg=CONF_DEFINED_BOTH_MSG): cv.string, - vol.Exclusive(CONF_ACCESS_TOKEN, CONF_APPSPOT, - msg=CONF_DEFINED_BOTH_MSG): cv.string, - vol.Optional(CONF_USER_AGENT, default=None): cv.string }) }, extra=vol.ALLOW_EXTRA) @@ -66,30 +82,118 @@ WINK_COMPONENTS = [ ] +def _write_config_file(file_path, config): + try: + with open(file_path, 'w') as conf_file: + conf_file.write(json.dumps(config, sort_keys=True, indent=4)) + except IOError as error: + _LOGGER.error("Saving config file failed: %s", error) + raise IOError("Saving Wink config file failed") + return config + + +def _read_config_file(file_path): + try: + with open(file_path, 'r') as conf_file: + return json.loads(conf_file.read()) + except IOError as error: + _LOGGER.error("Reading config file failed: %s", error) + raise IOError("Reading Wink config file failed") + + +def _request_app_setup(hass, config): + """Assist user with configuring the Wink dev application.""" + hass.data['configurator'] = True + configurator = get_component('configurator') + + # pylint: disable=unused-argument + def wink_configuration_callback(callback_data): + """Handle configuration updates.""" + _config_path = hass.config.path(WINK_CONFIG_FILE) + if not os.path.isfile(_config_path): + setup(hass, config) + return + + client_id = callback_data.get('client_id') + client_secret = callback_data.get('client_secret') + if None not in (client_id, client_secret): + _write_config_file(_config_path, + {ATTR_CLIENT_ID: client_id, + ATTR_CLIENT_SECRET: client_secret}) + setup(hass, config) + return + else: + error_msg = ("Your input was invalid. Please try again.") + _configurator = hass.data[DOMAIN]['configuring'][DOMAIN] + configurator.notify_errors(_configurator, error_msg) + + start_url = "{}{}".format(hass.config.api.base_url, + WINK_AUTH_CALLBACK_PATH) + + description = """Please create a Wink developer app at + https://developer.wink.com. + Add a Redirect URI of {}. + They will provide you a Client ID and secret + after reviewing your request. + (This can take several days). + """.format(start_url) + + hass.data[DOMAIN]['configuring'][DOMAIN] = configurator.request_config( + hass, DOMAIN, wink_configuration_callback, + description=description, submit_caption="submit", + description_image="/static/images/config_wink.png", + fields=[{'id': 'client_id', 'name': 'Client ID', 'type': 'string'}, + {'id': 'client_secret', + 'name': 'Client secret', + 'type': 'string'}] + ) + + +def _request_oauth_completion(hass, config): + """Request user complete Wink OAuth2 flow.""" + hass.data['configurator'] = True + configurator = get_component('configurator') + if DOMAIN in hass.data[DOMAIN]['configuring']: + configurator.notify_errors( + hass.data[DOMAIN]['configuring'][DOMAIN], + "Failed to register, please try again.") + return + + # pylint: disable=unused-argument + def wink_configuration_callback(callback_data): + """Call setup again.""" + setup(hass, config) + + start_url = '{}{}'.format(hass.config.api.base_url, WINK_AUTH_START) + + description = "Please authorize Wink by visiting {}".format(start_url) + + hass.data[DOMAIN]['configuring'][DOMAIN] = configurator.request_config( + hass, DOMAIN, wink_configuration_callback, + description=description + ) + + def setup(hass, config): """Set up the Wink component.""" import pywink - import requests from pubnubsubhandler import PubNubSubscriptionHandler - hass.data[DOMAIN] = {} - hass.data[DOMAIN]['entities'] = [] - hass.data[DOMAIN]['unique_ids'] = [] - hass.data[DOMAIN]['entities'] = {} - - user_agent = config[DOMAIN].get(CONF_USER_AGENT) - - if user_agent: - pywink.set_user_agent(user_agent) - - access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) - client_id = config[DOMAIN].get('client_id') + if hass.data.get(DOMAIN) is None: + hass.data[DOMAIN] = { + 'unique_ids': [], + 'entities': {}, + 'oauth': {}, + 'configuring': {}, + 'pubnub': None, + 'configurator': False + } def _get_wink_token_from_web(): - email = hass.data[DOMAIN]["oath"]["email"] - password = hass.data[DOMAIN]["oath"]["password"] + _email = hass.data[DOMAIN]["oauth"]["email"] + _password = hass.data[DOMAIN]["oauth"]["password"] - payload = {'username': email, 'password': password} + payload = {'username': _email, 'password': _password} token_response = requests.post(CONF_TOKEN_URL, data=payload) try: token = token_response.text.split(':')[1].split()[0].rstrip('Wink Auth +

{}

""" + + if data.get('code') is not None: + response = self.request_token(data.get('code'), + self.config_file["client_secret"]) + + config_contents = { + ATTR_ACCESS_TOKEN: response['access_token'], + ATTR_REFRESH_TOKEN: response['refresh_token'], + ATTR_CLIENT_ID: self.config_file["client_id"], + ATTR_CLIENT_SECRET: self.config_file["client_secret"] + } + _write_config_file(hass.config.path(WINK_CONFIG_FILE), + config_contents) + + hass.async_add_job(setup, hass, self.config) + + return web.Response(text=html_response.format(response_message), + content_type='text/html') + + error_msg = "No code returned from Wink API" + _LOGGER.error(error_msg) + return web.Response(text=html_response.format(error_msg), + content_type='text/html') + + class WinkDevice(Entity): """Representation a base Wink device."""