-
-
Notifications
You must be signed in to change notification settings - Fork 37.2k
Implemented tplink_lte components and notify service via SMS #17111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1137561
3765b44
ca97241
2ea92a4
8a41b0b
72e6205
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to move this up after creating the retry setup task and before returning. Otherwise we will never register the listener until we've succeeded with setup. We should also remove a potential existing listener, by calling the return value of the registration, each time before registering a new one.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI, I added the task cleanup to netgear_lte now: #18163 My proposal avoids removal of the existing listener by running a loop. I found that simpler to get working.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @amelchio thanks for the hint, I reused your solution with the addition of two
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe catching
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As @MartinHjelmare pointed out in a older commit with a
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I think @amelchio is correct. Eg: In [22]: import asyncio
In [23]: async def running_task():
...: while True:
...: print('running')
...: print('sleeping')
...: await asyncio.sleep(5)
...:
In [24]: def task_canceller(task):
...: print('in task_canceller')
...: task.cancel()
...: print('canceled the task')
...:
...:
In [25]: async def main(loop):
...: print('creating task')
...: task = loop.create_task(running_task())
...: loop.call_soon(task_canceller, task)
...: print('sleeping 2 secs in main')
...: await asyncio.sleep(2)
...: print('finished main')
...:
...:
In [26]: loop = asyncio.new_event_loop()
In [27]: try:
...: loop.run_until_complete(main(loop))
...: finally:
...: loop.close()
...:
creating task
sleeping 2 secs in main
running
sleeping
in task_canceller
canceled the task
finished main |
||
|
|
||
|
|
||
| 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) | ||
Uh oh!
There was an error while loading. Please reload this page.