Advertisement
Guest User

Untitled

a guest
Jun 27th, 2017
68
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.03 KB | None | 0 0
  1.  
  2. """
  3. Support for Wink hubs.
  4. For more details about this component, please refer to the documentation at
  5. https://home-assistant.io/components/wink/
  6. """
  7. import logging
  8. import time
  9. import json
  10. import os
  11. from datetime import timedelta
  12.  
  13. import voluptuous as vol
  14. import requests
  15.  
  16. from homeassistant.loader import get_component
  17. from homeassistant.core import callback
  18. from homeassistant.components.http import HomeAssistantView
  19. from homeassistant.helpers import discovery
  20. from homeassistant.helpers.event import track_time_interval
  21. from homeassistant.const import (
  22. ATTR_BATTERY_LEVEL, CONF_EMAIL, CONF_PASSWORD,
  23. EVENT_HOMEASSISTANT_STOP, MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
  24. from homeassistant.helpers.entity import Entity
  25. import homeassistant.helpers.config_validation as cv
  26.  
  27. REQUIREMENTS = ['python-wink==1.2.6', 'pubnubsub-handler==1.0.2']
  28.  
  29. _LOGGER = logging.getLogger(__name__)
  30.  
  31. CHANNELS = []
  32.  
  33. DOMAIN = 'wink'
  34.  
  35. _CONFIGURING = {}
  36.  
  37. SUBSCRIPTION_HANDLER = None
  38.  
  39. CONF_CLIENT_ID = 'client_id'
  40. CONF_CLIENT_SECRET = 'client_secret'
  41. CONF_USER_AGENT = 'user_agent'
  42. CONF_OATH = 'oath'
  43. CONF_APPSPOT = 'appspot'
  44. CONF_DEFINED_BOTH_MSG = 'Remove access token to use oath2.'
  45. CONF_MISSING_OATH_MSG = 'Missing oath2 credentials.'
  46. CONF_TOKEN_URL = "https://winkbearertoken.appspot.com/token"
  47.  
  48. ATTR_ACCESS_TOKEN = 'access_token'
  49. ATTR_REFRESH_TOKEN = 'refresh_token'
  50. ATTR_CLIENT_ID = 'client_id'
  51. ATTR_CLIENT_SECRET = 'client_secret'
  52.  
  53. WINK_AUTH_CALLBACK_PATH = '/auth/wink/callback'
  54. WINK_AUTH_START = '/auth/wink'
  55. WINK_CONFIG_FILE = '.wink.conf'
  56. USER_AGENT = "Manufacturer/Home-Assistant%s.%s.%s python/3 Wink/3"
  57.  
  58. DEFAULT_CONFIG = {
  59. 'client_id': 'CLIENT_ID_HERE',
  60. 'client_secret': 'CLIENT_SECRET_HERE'
  61. }
  62.  
  63. SERVICE_ADD_NEW_DEVICES = 'add_new_devices'
  64. SERVICE_REFRESH_STATES = 'refresh_state_from_wink'
  65.  
  66. CONFIG_SCHEMA = vol.Schema({
  67. DOMAIN: vol.Schema({
  68. vol.Inclusive(CONF_EMAIL, CONF_APPSPOT,
  69. msg=CONF_MISSING_OATH_MSG): cv.string,
  70. vol.Inclusive(CONF_PASSWORD, CONF_APPSPOT,
  71. msg=CONF_MISSING_OATH_MSG): cv.string,
  72. vol.Inclusive(CONF_CLIENT_ID, CONF_OATH,
  73. msg=CONF_MISSING_OATH_MSG): cv.string,
  74. vol.Inclusive(CONF_CLIENT_SECRET, CONF_OATH,
  75. msg=CONF_MISSING_OATH_MSG): cv.string,
  76. vol.Exclusive(CONF_EMAIL, CONF_OATH,
  77. msg=CONF_DEFINED_BOTH_MSG): cv.string,
  78. })
  79. }, extra=vol.ALLOW_EXTRA)
  80.  
  81. WINK_COMPONENTS = [
  82. 'binary_sensor', 'sensor', 'light', 'switch', 'lock', 'cover', 'climate',
  83. 'fan', 'alarm_control_panel', 'scene'
  84. ]
  85.  
  86.  
  87. def _write_config_file(file_path, config):
  88. try:
  89. with open(file_path, 'w') as conf_file:
  90. conf_file.write(json.dumps(config, sort_keys=True, indent=4))
  91. except IOError as error:
  92. _LOGGER.error("Saving config file failed: %s", error)
  93. raise IOError("Saving Wink config file failed")
  94. return config
  95.  
  96.  
  97. def _read_config_file(file_path):
  98. try:
  99. with open(file_path, 'r') as conf_file:
  100. return json.loads(conf_file.read())
  101. except IOError as error:
  102. _LOGGER.error("Reading config file failed: %s", error)
  103. raise IOError("Reading Wink config file failed")
  104.  
  105.  
  106. def _request_app_setup(hass, config, config_path):
  107. """Assist user with configuring the Wink dev application."""
  108. configurator = get_component('configurator')
  109.  
  110. # pylint: disable=unused-argument
  111. def wink_configuration_callback(callback_data):
  112. """Handle configuration updates."""
  113. _config_path = hass.config.path(WINK_CONFIG_FILE)
  114. if os.path.isfile(_config_path):
  115. config_file = _read_config_file(_config_path)
  116. if config_file == DEFAULT_CONFIG:
  117. error_msg = ("You didn't correctly modify wink.conf",
  118. " please try again")
  119. configurator.notify_errors(_CONFIGURING['wink'], error_msg)
  120. else:
  121. setup(hass, config)
  122. else:
  123. # Shouldn't get here, but if we do, call setup again and create
  124. # default config again
  125. setup(hass, config)
  126.  
  127. start_url = "{}{}".format(hass.config.api.base_url,
  128. WINK_AUTH_CALLBACK_PATH)
  129.  
  130. description = """If you haven't done so already.
  131. Please create a Wink developer app at
  132. https://developer.wink.com.
  133. Add a Redirect URI of {}.
  134. They will provide you a Client ID and secret
  135. after reviewing your request. (This can take several days)
  136. These need to be saved into the file located at: {}.
  137. Then come back here and hit the below button.
  138. """.format(start_url, config_path)
  139.  
  140. submit = "I have saved my Client ID and Client Secret into wink.conf."
  141.  
  142. _CONFIGURING['wink'] = configurator.request_config(
  143. hass, 'Wink', wink_configuration_callback,
  144. description=description, submit_caption=submit,
  145. description_image="/static/images/config_wink.png"
  146. )
  147.  
  148.  
  149. def _request_oauth_completion(hass, config):
  150. """Request user complete Wink OAuth2 flow."""
  151. configurator = get_component('configurator')
  152. if "wink" in _CONFIGURING:
  153. configurator.notify_errors(
  154. _CONFIGURING['wink'], "Failed to register, please try again.")
  155. return
  156.  
  157. # pylint: disable=unused-argument
  158. def wink_configuration_callback(callback_data):
  159. """Call setup again."""
  160. setup(hass, config)
  161.  
  162. start_url = '{}{}'.format(hass.config.api.base_url, WINK_AUTH_START)
  163.  
  164. description = "Please authorize Wink by visiting {}".format(start_url)
  165.  
  166. _CONFIGURING['wink'] = configurator.request_config(
  167. hass, 'Wink', wink_configuration_callback,
  168. description=description,
  169. submit_caption="I have authorized Wink."
  170. )
  171.  
  172.  
  173. def setup(hass, config):
  174. """Set up the Wink component."""
  175. import pywink
  176. from pubnubsubhandler import PubNubSubscriptionHandler
  177.  
  178. hass.data[DOMAIN] = {
  179. 'unique_ids': [],
  180. 'entities': {},
  181. 'oauth': {}
  182. }
  183.  
  184. user_agent = USER_AGENT % (MINOR_VERSION, MAJOR_VERSION, PATCH_VERSION)
  185. pywink.set_user_agent(user_agent)
  186. _LOGGER.info(user_agent)
  187.  
  188. def _get_wink_token_from_web():
  189. _email = hass.data[DOMAIN]["oauth"]["email"]
  190. _password = hass.data[DOMAIN]["oauth"]["password"]
  191.  
  192. payload = {'username': _email, 'password': _password}
  193. token_response = requests.post(CONF_TOKEN_URL, data=payload)
  194. try:
  195. token = token_response.text.split(':')[1].split()[0].rstrip('<br')
  196. except IndexError:
  197. _LOGGER.error("Error getting token. Please check email/password.")
  198. return False
  199. pywink.set_bearer_token(token)
  200.  
  201. client_id = config[DOMAIN].get(ATTR_CLIENT_ID)
  202. client_secret = config[DOMAIN].get(ATTR_CLIENT_SECRET)
  203. email = config[DOMAIN].get(CONF_EMAIL)
  204. password = config[DOMAIN].get(CONF_PASSWORD)
  205. if None not in [client_id, client_secret]:
  206. _LOGGER.info("Using legacy oauth authentication")
  207. hass.data[DOMAIN]["oauth"]["client_id"] = client_id
  208. hass.data[DOMAIN]["oauth"]["client_secret"] = client_secret
  209. hass.data[DOMAIN]["oauth"]["email"] = email
  210. hass.data[DOMAIN]["oauth"]["password"] = password
  211. pywink.legacy_set_wink_credentials(email, password,
  212. client_id, client_secret)
  213. elif None not in [email, password]:
  214. _LOGGER.info("Using web form authentication")
  215. hass.data[DOMAIN]["oauth"]["email"] = email
  216. hass.data[DOMAIN]["oauth"]["password"] = password
  217. _get_wink_token_from_web()
  218. else:
  219. _LOGGER.info("Using new oauth authentication")
  220. config_path = hass.config.path(WINK_CONFIG_FILE)
  221. if os.path.isfile(config_path):
  222. config_file = _read_config_file(config_path)
  223. if config_file == DEFAULT_CONFIG:
  224. _request_app_setup(
  225. hass, config, config_path)
  226. return True
  227. # else move on because the user modified the file
  228. else:
  229. _write_config_file(config_path, DEFAULT_CONFIG)
  230. _request_app_setup(
  231. hass, config, config_path)
  232. return True
  233.  
  234. if "wink" in _CONFIGURING:
  235. get_component('configurator').request_done(_CONFIGURING.pop(
  236. "wink"))
  237.  
  238. # Using oauth
  239. access_token = config_file.get(ATTR_ACCESS_TOKEN)
  240. refresh_token = config_file.get(ATTR_REFRESH_TOKEN)
  241.  
  242. # This will be called after authorizing Home-Assistant
  243. if None not in (access_token, refresh_token):
  244. pywink.set_wink_credentials(config_file.get(ATTR_CLIENT_ID),
  245. config_file.get(ATTR_CLIENT_SECRET),
  246. access_token=access_token,
  247. refresh_token=refresh_token)
  248. # This is called to create the redirect so the user can Authorize
  249. # Home-Assistant
  250. else:
  251.  
  252. redirect_uri = '{}{}'.format(hass.config.api.base_url,
  253. WINK_AUTH_CALLBACK_PATH)
  254.  
  255. wink_auth_start_url = pywink.get_authorization_url(
  256. config_file.get(ATTR_CLIENT_ID), redirect_uri)
  257. hass.http.register_redirect(WINK_AUTH_START, wink_auth_start_url)
  258. hass.http.register_view(WinkAuthCallbackView(config,
  259. config_file,
  260. pywink.request_token))
  261. _request_oauth_completion(hass, config)
  262. return True
  263.  
  264. hass.data[DOMAIN]['pubnub'] = PubNubSubscriptionHandler(
  265. pywink.get_subscription_key())
  266. hass.data[DOMAIN]['pubnub'].subscribe()
  267.  
  268. def keep_alive_call(event_time):
  269. """Call the Wink API endpoints to keep PubNub working."""
  270. _user_agent = USER_AGENT % (MINOR_VERSION, MAJOR_VERSION,
  271. PATCH_VERSION)
  272. pywink.set_user_agent(time.time())
  273. _LOGGER.info("Wink user-agent: " + _user_agent)
  274. time.sleep(1)
  275. _LOGGER.info("Polling the Wink API to keep PubNub updates flowing.")
  276. _LOGGER.debug(str(json.dumps(pywink.wink_api_fetch())))
  277. time.sleep(1)
  278. _LOGGER.debug(str(json.dumps(pywink.get_user())))
  279. pywink.set_user_agent(_user_agent)
  280.  
  281. # Call the Wink API every hour to keep PubNub updates flowing
  282. track_time_interval(hass, keep_alive_call, timedelta(minutes=120))
  283.  
  284. def stop_subscription(event):
  285. """Stop the pubnub subscription."""
  286. hass.data[DOMAIN]['pubnub'].unsubscribe()
  287.  
  288. hass.bus.listen(EVENT_HOMEASSISTANT_STOP, stop_subscription)
  289.  
  290. def force_update(call):
  291. """Force all devices to poll the Wink API."""
  292. _LOGGER.info("Refreshing Wink states from API")
  293. for entity_list in hass.data[DOMAIN]['entities'].values():
  294. # Throttle the calls to Wink API
  295. for entity in entity_list:
  296. time.sleep(1)
  297. entity.schedule_update_ha_state(True)
  298. hass.services.register(DOMAIN, SERVICE_REFRESH_STATES, force_update)
  299.  
  300. def pull_new_devices(call):
  301. """Pull new devices added to users Wink account since startup."""
  302. _LOGGER.info("Getting new devices from Wink API")
  303. for _component in WINK_COMPONENTS:
  304. discovery.load_platform(hass, _component, DOMAIN, {}, config)
  305.  
  306. hass.services.register(DOMAIN, SERVICE_ADD_NEW_DEVICES, pull_new_devices)
  307.  
  308. # Load components for the devices in Wink that we support
  309. for component in WINK_COMPONENTS:
  310. hass.data[DOMAIN]['entities'][component] = []
  311. discovery.load_platform(hass, component, DOMAIN, {}, config)
  312.  
  313. return True
  314.  
  315.  
  316. class WinkAuthCallbackView(HomeAssistantView):
  317. """Handle OAuth finish callback requests."""
  318.  
  319. url = '/auth/wink/callback'
  320. name = 'auth:wink:callback'
  321.  
  322. def __init__(self, config, config_file, request_token):
  323. """Initialize the OAuth callback view."""
  324. self.config = config
  325. self.config_file = config_file
  326. self.request_token = request_token
  327.  
  328. @callback
  329. def get(self, request):
  330. """Finish OAuth callback request."""
  331. from aiohttp import web
  332.  
  333. hass = request.app['hass']
  334. data = request.GET
  335.  
  336. response_message = """Wink has been successfully authorized!
  337. You can close this window now!"""
  338.  
  339. if data.get('code') is not None:
  340. response = self.request_token(data.get('code'),
  341. self.config_file["client_secret"])
  342.  
  343. html_response = """<html><head><title>Wink Auth</title></head>
  344. <body><h1>{}</h1></body></html>""".format(response_message)
  345.  
  346. config_contents = {
  347. ATTR_ACCESS_TOKEN: response['access_token'],
  348. ATTR_REFRESH_TOKEN: response['refresh_token'],
  349. ATTR_CLIENT_ID: self.config_file["client_id"],
  350. ATTR_CLIENT_SECRET: self.config_file["client_secret"]
  351. }
  352. _write_config_file(hass.config.path(WINK_CONFIG_FILE),
  353. config_contents)
  354.  
  355. hass.async_add_job(setup, hass, self.config)
  356.  
  357. return web.Response(text=html_response, content_type='text/html')
  358. else:
  359. _LOGGER.error("No code returned from Wink API")
  360.  
  361.  
  362. class WinkDevice(Entity):
  363. """Representation a base Wink device."""
  364.  
  365. def __init__(self, wink, hass):
  366. """Initialize the Wink device."""
  367. self.hass = hass
  368. self.wink = wink
  369. hass.data[DOMAIN]['pubnub'].add_subscription(
  370. self.wink.pubnub_channel, self._pubnub_update)
  371. hass.data[DOMAIN]['unique_ids'].append(self.wink.object_id() +
  372. self.wink.name())
  373.  
  374. def _pubnub_update(self, message):
  375. try:
  376. if message is None:
  377. _LOGGER.error("Error on pubnub update for %s "
  378. "polling API for current state", self.name)
  379. self.schedule_update_ha_state(True)
  380. else:
  381. self.wink.pubnub_update(message)
  382. self.schedule_update_ha_state()
  383. except (ValueError, KeyError, AttributeError):
  384. _LOGGER.error("Error in pubnub JSON for %s "
  385. "polling API for current state", self.name)
  386. self.schedule_update_ha_state(True)
  387.  
  388. @property
  389. def name(self):
  390. """Return the name of the device."""
  391. return self.wink.name()
  392.  
  393. @property
  394. def available(self):
  395. """Return true if connection == True."""
  396. return self.wink.available()
  397.  
  398. def update(self):
  399. """Update state of the device."""
  400. self.wink.update_state()
  401.  
  402. @property
  403. def should_poll(self):
  404. """Only poll if we are not subscribed to pubnub."""
  405. return self.wink.pubnub_channel is None
  406.  
  407. @property
  408. def device_state_attributes(self):
  409. """Return the state attributes."""
  410. attributes = {}
  411. battery = self._battery_level
  412. if battery:
  413. attributes[ATTR_BATTERY_LEVEL] = battery
  414. man_dev_model = self._manufacturer_device_model
  415. if man_dev_model:
  416. attributes["manufacturer_device_model"] = man_dev_model
  417. man_dev_id = self._manufacturer_device_id
  418. if man_dev_id:
  419. attributes["manufacturer_device_id"] = man_dev_id
  420. dev_man = self._device_manufacturer
  421. if dev_man:
  422. attributes["device_manufacturer"] = dev_man
  423. model_name = self._model_name
  424. if model_name:
  425. attributes["model_name"] = model_name
  426. tamper = self._tamper
  427. if tamper is not None:
  428. attributes["tamper_detected"] = tamper
  429. return attributes
  430.  
  431. @property
  432. def _battery_level(self):
  433. """Return the battery level."""
  434. if self.wink.battery_level() is not None:
  435. return self.wink.battery_level() * 100
  436.  
  437. @property
  438. def _manufacturer_device_model(self):
  439. """Return the manufacturer device model."""
  440. return self.wink.manufacturer_device_model()
  441.  
  442. @property
  443. def _manufacturer_device_id(self):
  444. """Return the manufacturer device id."""
  445. return self.wink.manufacturer_device_id()
  446.  
  447. @property
  448. def _device_manufacturer(self):
  449. """Return the device manufacturer."""
  450. return self.wink.device_manufacturer()
  451.  
  452. @property
  453. def _model_name(self):
  454. """Return the model name."""
  455. return self.wink.model_name()
  456.  
  457. @property
  458. def _tamper(self):
  459. """Return the devices tamper status."""
  460. if hasattr(self.wink, 'tamper_detected'):
  461. return self.wink.tamper_detected()
  462. else:
  463. return None
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement