From 1c3ef8be55fa739e049904805f5e47d3dba3b72c Mon Sep 17 00:00:00 2001 From: Andrea Tosatto Date: Mon, 5 Nov 2018 02:09:29 +0100 Subject: [PATCH] Implemented tplink_lte components and notify service via SMS (#17111) * Implemented tplink_lte components and notify service * Device discovery for the notify component * Improved the config schema. Small fixes * Improved login retry mechanism * Log successful connection only on retries * Removed CancelledError handlers and small fixes --- .coveragerc | 3 + homeassistant/components/notify/tplink_lte.py | 50 ++++++ homeassistant/components/tplink_lte.py | 150 ++++++++++++++++++ requirements_all.txt | 3 + 4 files changed, 206 insertions(+) create mode 100644 homeassistant/components/notify/tplink_lte.py create mode 100644 homeassistant/components/tplink_lte.py diff --git a/.coveragerc b/.coveragerc index f5e9545625b..f5103867688 100644 --- a/.coveragerc +++ b/.coveragerc @@ -333,6 +333,9 @@ omit = homeassistant/components/toon.py homeassistant/components/*/toon.py + homeassistant/components/tplink_lte.py + homeassistant/components/*/tplink_lte.py + homeassistant/components/tradfri.py homeassistant/components/*/tradfri.py diff --git a/homeassistant/components/notify/tplink_lte.py b/homeassistant/components/notify/tplink_lte.py new file mode 100644 index 00000000000..9bb80e2591c --- /dev/null +++ b/homeassistant/components/notify/tplink_lte.py @@ -0,0 +1,50 @@ +"""TP-Link LTE platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.tplink_lte/ +""" + +import logging + +import attr + +from homeassistant.components.notify import ( + ATTR_TARGET, BaseNotificationService) + +from ..tplink_lte import DATA_KEY + +DEPENDENCIES = ['tplink_lte'] + +_LOGGER = logging.getLogger(__name__) + + +async def async_get_service(hass, config, discovery_info=None): + """Get the notification service.""" + if discovery_info is None: + return + return TplinkNotifyService(hass, discovery_info) + + +@attr.s +class TplinkNotifyService(BaseNotificationService): + """Implementation of a notification service.""" + + hass = attr.ib() + config = attr.ib() + + async def async_send_message(self, message="", **kwargs): + """Send a message to a user.""" + import tp_connected + modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config) + if not modem_data: + _LOGGER.error("No modem available") + return + + phone = self.config[ATTR_TARGET] + targets = kwargs.get(ATTR_TARGET, phone) + if targets and message: + for target in targets: + try: + await modem_data.modem.sms(target, message) + except tp_connected.Error: + _LOGGER.error("Unable to send to %s", target) diff --git a/homeassistant/components/tplink_lte.py b/homeassistant/components/tplink_lte.py new file mode 100644 index 00000000000..17288a881aa --- /dev/null +++ b/homeassistant/components/tplink_lte.py @@ -0,0 +1,150 @@ +""" +Support for TP-Link LTE modems. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/tplink_lte/ +""" +import asyncio +import logging + +import aiohttp +import attr +import voluptuous as vol + +from homeassistant.components.notify import ATTR_TARGET +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.aiohttp_client import async_create_clientsession + +REQUIREMENTS = ['tp-connected==0.0.4'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'tplink_lte' +DATA_KEY = 'tplink_lte' + +CONF_NOTIFY = "notify" + +_NOTIFY_SCHEMA = vol.All(vol.Schema({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), +})) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NOTIFY): + vol.All(cv.ensure_list, [_NOTIFY_SCHEMA]), + })]) +}, extra=vol.ALLOW_EXTRA) + + +@attr.s +class ModemData: + """Class for modem state.""" + + host = attr.ib() + modem = attr.ib() + + connected = attr.ib(init=False, default=True) + + +@attr.s +class LTEData: + """Shared state.""" + + websession = attr.ib() + modem_data = attr.ib(init=False, factory=dict) + + def get_modem_data(self, config): + """Get the requested or the only modem_data value.""" + if CONF_HOST in config: + return self.modem_data.get(config[CONF_HOST]) + if len(self.modem_data) == 1: + return next(iter(self.modem_data.values())) + + return None + + +async def async_setup(hass, config): + """Set up TP-Link LTE component.""" + if DATA_KEY not in hass.data: + websession = async_create_clientsession( + hass, cookie_jar=aiohttp.CookieJar(unsafe=True)) + hass.data[DATA_KEY] = LTEData(websession) + + domain_config = config.get(DOMAIN, []) + + tasks = [_setup_lte(hass, conf) for conf in domain_config] + if tasks: + await asyncio.wait(tasks) + + for conf in domain_config: + for notify_conf in conf.get(CONF_NOTIFY, []): + hass.async_create_task(discovery.async_load_platform( + hass, 'notify', DOMAIN, notify_conf, config)) + + return True + + +async def _setup_lte(hass, lte_config, delay=0): + """Set up a TP-Link LTE modem.""" + import tp_connected + + host = lte_config[CONF_HOST] + password = lte_config[CONF_PASSWORD] + + websession = hass.data[DATA_KEY].websession + modem = tp_connected.Modem(hostname=host, websession=websession) + + modem_data = ModemData(host, modem) + + try: + await _login(hass, modem_data, password) + except tp_connected.Error: + retry_task = hass.loop.create_task( + _retry_login(hass, modem_data, password)) + + @callback + def cleanup_retry(event): + """Clean up retry task resources.""" + if not retry_task.done(): + retry_task.cancel() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_retry) + + +async def _login(hass, modem_data, password): + """Log in and complete setup.""" + await modem_data.modem.login(password=password) + modem_data.connected = True + hass.data[DATA_KEY].modem_data[modem_data.host] = modem_data + + async def cleanup(event): + """Clean up resources.""" + await modem_data.modem.logout() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) + + +async def _retry_login(hass, modem_data, password): + """Sleep and retry setup.""" + import tp_connected + + _LOGGER.warning( + "Could not connect to %s. Will keep trying.", modem_data.host) + + modem_data.connected = False + delay = 15 + + while not modem_data.connected: + await asyncio.sleep(delay) + + try: + await _login(hass, modem_data, password) + _LOGGER.warning("Connected to %s", modem_data.host) + except tp_connected.Error: + delay = min(2*delay, 300) diff --git a/requirements_all.txt b/requirements_all.txt index 9b646695a11..9f376bb0d41 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1493,6 +1493,9 @@ toonlib==1.1.3 # homeassistant.components.alarm_control_panel.totalconnect total_connect_client==0.20 +# homeassistant.components.tplink_lte +tp-connected==0.0.4 + # homeassistant.components.device_tracker.tplink tplink==0.2.1