""" Support for Wink hubs. For more details about this component, please refer to the documentation at https://home-assistant.io/components/wink/ """ import asyncio import logging import time import json import os from datetime import timedelta import voluptuous as vol import requests 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 ( ATTR_BATTERY_LEVEL, CONF_EMAIL, CONF_PASSWORD, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, __version__, ATTR_ENTITY_ID, STATE_ON, STATE_OFF) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent import homeassistant.helpers.config_validation as cv from homeassistant.config import load_yaml_config_file REQUIREMENTS = ['python-wink==1.7.0', 'pubnubsub-handler==1.0.2'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'wink' SUBSCRIPTION_HANDLER = None CONF_CLIENT_ID = 'client_id' CONF_CLIENT_SECRET = 'client_secret' CONF_USER_AGENT = 'user_agent' CONF_OAUTH = 'oauth' CONF_LOCAL_CONTROL = 'local_control' CONF_APPSPOT = 'appspot' CONF_MISSING_OAUTH_MSG = 'Missing oauth2 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' ATTR_NAME = 'name' ATTR_PAIRING_MODE = 'pairing_mode' ATTR_KIDDE_RADIO_CODE = 'kidde_radio_code' ATTR_HUB_NAME = 'hub_name' 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 = 'pull_newly_added_devices_from_wink' SERVICE_REFRESH_STATES = 'refresh_state_from_wink' SERVICE_RENAME_DEVICE = 'rename_wink_device' SERVICE_DELETE_DEVICE = 'delete_wink_device' SERVICE_SET_PAIRING_MODE = 'pair_new_device' SERVICE_SET_CHIME_VOLUME = "set_chime_volume" SERVICE_SET_SIREN_VOLUME = "set_siren_volume" SERVICE_ENABLE_CHIME = "enable_chime" SERVICE_SET_SIREN_TONE = "set_siren_tone" SERVICE_SET_AUTO_SHUTOFF = "siren_set_auto_shutoff" SERVICE_SIREN_STROBE_ENABLED = "set_siren_strobe_enabled" SERVICE_CHIME_STROBE_ENABLED = "set_chime_strobe_enabled" SERVICE_ENABLE_SIREN = "enable_siren" ATTR_VOLUME = "volume" ATTR_TONE = "tone" ATTR_ENABLED = "enabled" ATTR_AUTO_SHUTOFF = "auto_shutoff" VOLUMES = ["low", "medium", "high"] TONES = ["doorbell", "fur_elise", "doorbell_extended", "alert", "william_tell", "rondo_alla_turca", "police_siren", "evacuation", "beep_beep", "beep"] CHIME_TONES = TONES + ["inactive"] AUTO_SHUTOFF_TIMES = [None, -1, 30, 60, 120] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Inclusive(CONF_EMAIL, CONF_APPSPOT, msg=CONF_MISSING_OAUTH_MSG): cv.string, vol.Inclusive(CONF_PASSWORD, CONF_APPSPOT, msg=CONF_MISSING_OAUTH_MSG): cv.string, vol.Inclusive(CONF_CLIENT_ID, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG): cv.string, vol.Inclusive(CONF_CLIENT_SECRET, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG): cv.string, vol.Optional(CONF_LOCAL_CONTROL, default=False): cv.boolean }) }, extra=vol.ALLOW_EXTRA) RENAME_DEVICE_SCHEMA = vol.Schema({ vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_NAME): cv.string }, extra=vol.ALLOW_EXTRA) DELETE_DEVICE_SCHEMA = vol.Schema({ vol.Required(ATTR_ENTITY_ID): cv.entity_ids }, extra=vol.ALLOW_EXTRA) SET_PAIRING_MODE_SCHEMA = vol.Schema({ vol.Required(ATTR_HUB_NAME): cv.string, vol.Required(ATTR_PAIRING_MODE): cv.string, vol.Optional(ATTR_KIDDE_RADIO_CODE): cv.string }, extra=vol.ALLOW_EXTRA) SET_VOLUME_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_VOLUME): vol.In(VOLUMES) }) SET_SIREN_TONE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_TONE): vol.In(TONES) }) SET_CHIME_MODE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_TONE): vol.In(CHIME_TONES) }) SET_AUTO_SHUTOFF_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_AUTO_SHUTOFF): vol.In(AUTO_SHUTOFF_TIMES) }) SET_STROBE_ENABLED_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_ENABLED): cv.boolean }) ENABLED_SIREN_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_ENABLED): cv.boolean }) WINK_COMPONENTS = [ 'binary_sensor', 'sensor', 'light', 'switch', 'lock', 'cover', 'climate', 'fan', 'alarm_control_panel', 'scene' ] WINK_HUBS = [] 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[DOMAIN]['configurator'] = True configurator = hass.components.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( 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[DOMAIN]['configurator'] = True configurator = hass.components.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( DOMAIN, wink_configuration_callback, description=description ) def setup(hass, config): """Set up the Wink component.""" import pywink from pubnubsubhandler import PubNubSubscriptionHandler descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) 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]["oauth"]["email"] _password = hass.data[DOMAIN]["oauth"]["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.""" def __init__(self, wink, hass): """Initialize the Wink device.""" self.hass = hass self.wink = wink hass.data[DOMAIN]['pubnub'].add_subscription( self.wink.pubnub_channel, self._pubnub_update) hass.data[DOMAIN]['unique_ids'].append(self.wink.object_id() + self.wink.name()) def _pubnub_update(self, message): try: if message is None: _LOGGER.error("Error on pubnub update for %s " "polling API for current state", self.name) self.schedule_update_ha_state(True) else: self.wink.pubnub_update(message) self.schedule_update_ha_state() except (ValueError, KeyError, AttributeError): _LOGGER.error("Error in pubnub JSON for %s " "polling API for current state", self.name) self.schedule_update_ha_state(True) @property def name(self): """Return the name of the device.""" return self.wink.name() @property def available(self): """Return true if connection == True.""" return self.wink.available() def update(self): """Update state of the device.""" self.wink.update_state() @property def should_poll(self): """Only poll if we are not subscribed to pubnub.""" return self.wink.pubnub_channel is None @property def device_state_attributes(self): """Return the state attributes.""" attributes = {} battery = self._battery_level if battery: attributes[ATTR_BATTERY_LEVEL] = battery man_dev_model = self._manufacturer_device_model if man_dev_model: attributes["manufacturer_device_model"] = man_dev_model man_dev_id = self._manufacturer_device_id if man_dev_id: attributes["manufacturer_device_id"] = man_dev_id dev_man = self._device_manufacturer if dev_man: attributes["device_manufacturer"] = dev_man model_name = self._model_name if model_name: attributes["model_name"] = model_name tamper = self._tamper if tamper is not None: attributes["tamper_detected"] = tamper return attributes @property def _battery_level(self): """Return the battery level.""" if self.wink.battery_level() is not None: return self.wink.battery_level() * 100 @property def _manufacturer_device_model(self): """Return the manufacturer device model.""" return self.wink.manufacturer_device_model() @property def _manufacturer_device_id(self): """Return the manufacturer device id.""" return self.wink.manufacturer_device_id() @property def _device_manufacturer(self): """Return the device manufacturer.""" return self.wink.device_manufacturer() @property def _model_name(self): """Return the model name.""" return self.wink.model_name() @property def _tamper(self): """Return the devices tamper status.""" if hasattr(self.wink, 'tamper_detected'): return self.wink.tamper_detected() return None class WinkSirenDevice(WinkDevice): """Representation of a Wink siren device.""" @asyncio.coroutine def async_added_to_hass(self): """Callback when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['switch'].append(self) @property def state(self): """Return sirens state.""" if self.wink.state(): return STATE_ON return STATE_OFF @property def icon(self): """Return the icon to use in the frontend, if any.""" return "mdi:bell-ring" @property def device_state_attributes(self): """Return the state attributes.""" attributes = super(WinkSirenDevice, self).device_state_attributes auto_shutoff = self.wink.auto_shutoff() if auto_shutoff is not None: attributes["auto_shutoff"] = auto_shutoff siren_volume = self.wink.siren_volume() if siren_volume is not None: attributes["siren_volume"] = siren_volume chime_volume = self.wink.chime_volume() if chime_volume is not None: attributes["chime_volume"] = chime_volume strobe_enabled = self.wink.strobe_enabled() if strobe_enabled is not None: attributes["siren_strobe_enabled"] = strobe_enabled chime_strobe_enabled = self.wink.chime_strobe_enabled() if chime_strobe_enabled is not None: attributes["chime_strobe_enabled"] = chime_strobe_enabled siren_sound = self.wink.siren_sound() if siren_sound is not None: attributes["siren_sound"] = siren_sound chime_mode = self.wink.chime_mode() if chime_mode is not None: attributes["chime_mode"] = chime_mode return attributes