Implemented tplink_lte components and notify service via SMS#17111
Implemented tplink_lte components and notify service via SMS#17111MartinHjelmare merged 6 commits intohome-assistant:devfrom
Conversation
| CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP) | ||
| from homeassistant.helpers import config_validation as cv | ||
| from homeassistant.helpers.aiohttp_client import async_create_clientsession | ||
| from homeassistant.util import Throttle |
There was a problem hiding this comment.
'homeassistant.util.Throttle' imported but unused
|
Docs updated |
| @attr.s | ||
| class ModemData: | ||
| """Class for modem state.""" | ||
|
|
| targets = kwargs.get(ATTR_TARGET, phone) | ||
| if targets and message: | ||
| for target in targets: | ||
| import tp_connected |
There was a problem hiding this comment.
At least move it out of the loop. Usually we put imports in the beginning of the method.
There was a problem hiding this comment.
You're right, totally missed this
There was a problem hiding this comment.
@amelchio I'm not skilled at Python so maybe I miss something here. There's any reason why this import is placed inside the loop? (that's the same code as netgear_lte notify)
I would rather place the import between the line 51 and 52 or the begging of the method as suggested by @MartinHjelmare
| from datetime import timedelta | ||
| import logging | ||
|
|
||
| import voluptuous as vol |
There was a problem hiding this comment.
Please sort 🔡 within groups standard library, 3rd party and homeassistant imports.
|
@amelchio is the author of the netgear lte component. |
0503190 to
c2e4d18
Compare
|
@MartinHjelmare Travis completed successfully the job but it's still show as pending here |
|
I've restarted a short job to see if we can get a response from Travis upon completion. |
| import tp_connected | ||
|
|
||
| if delay: | ||
| await asyncio.sleep(delay) |
There was a problem hiding this comment.
We should probably cancel the _setup_lte task, that we create on failure, upon home assistant stop event and catch CancelledError here and around modem.login. Otherwise there will be a stacktrace when closing home assistant if we're still in this delay loop.
There was a problem hiding this comment.
So, if I understand it correctly:
try:
if delay:
await asyncio.sleep(delay)
except asyncio.CancelledError:
returnand
try:
await modem.login(password=password)
except asyncio.CancelledError:
returnshould do the thing, right?
There was a problem hiding this comment.
We also need to store the task that is created on failure so we can register a listener to home assistant stop event that can call a function that calls task.cancel if the task isn't done.
There was a problem hiding this comment.
The updated code I would write is:
try:
if delay:
task = await asyncio.sleep(delay)
await task
except asyncio.CancelledError:
returnand
try:
task = await modem.login(password=password)
await task
except asyncio.CancelledError:
returnand cancel the task inside the cleanup function
There was a problem hiding this comment.
I don't think that works. Coroutines don't return tasks by default. We have to use a function from the asyncio library to wrap a coroutine in a task.
https://docs.python.org/3/library/asyncio-task.html#asyncio.Task
I would just save the task that is returned here:
https://github.com/home-assistant/home-assistant/pull/17111/files#diff-5410e9b3cb190ba123d66d6583931ad4R93
There was a problem hiding this comment.
Oh yes you're right about coroutines but I'm not sure about storing only the task from here: https://github.com/home-assistant/home-assistant/pull/17111/files#diff-5410e9b3cb190ba123d66d6583931ad4R93
Doesn't mean that we'll miss the first iteration for each modem setup? I'm talking about the tasks created here:
https://github.com/home-assistant/home-assistant/pull/17111/files#diff-5410e9b3cb190ba123d66d6583931ad4R68
There was a problem hiding this comment.
We won't miss that since those tasks are already tracked by the home assistant core. There is also no delay for those tasks. So unless modem.login takes a very long time to return or error, we should be fine.
|
I've nothing to add, it should be ok now |
|
Please don't squash commits after review has started. Squashing makes it hard to follow the changes. Now I have to read all the code again before I can approve. We squash commits upon merge. |
| hass, cookie_jar=aiohttp.CookieJar(unsafe=True)) | ||
| hass.data[DATA_KEY] = LTEData(websession) | ||
|
|
||
| tasks = [_setup_lte(hass, conf) for conf in config.get(DOMAIN, [])] |
There was a problem hiding this comment.
Move default list to config schema.
There was a problem hiding this comment.
Could you please be more specific or give me some indication to other components that do this?
There was a problem hiding this comment.
I think we can just use dict[key] here instead. The domain key will be required to set up the component so we shouldn't need any defaults here or in the schema. Sorry for my confusion.
There was a problem hiding this comment.
I was mistaken. We don't yet validate that the 'tplink_lte' key for the component is present in the config. Since that is required for a functioning integration, we should check that early and fail component setup if the config isn't ok.
Maybe it would make sense to move the whole config to the component and set up the notify platform via discovery instead. This will releave the user from having to juggle multiple config sections.
There was a problem hiding this comment.
For the netgear_lte component I opted out of discovery because I saw no way to set things like notify name and sensor scan_interval. Was I mistaken?
There was a problem hiding this comment.
I think devices is odd here, isn't the host the device?
There was a problem hiding this comment.
@amelchio you're right, the host is the physical device but I'm not sure about the right solution here, something like this is enough?
tplink_lte:
- host: 192.168.5.1
password: *********
targets:
- name: sms1
target: "+3955555555"
- name: sms2
target: "+3955555555"There was a problem hiding this comment.
I am not sure myself. But once we decide, I want to do this for netgear_lte as well :)
I was thinking about using the component name of the platform that we setup ... so notify and at least netgear_lte will also need configuration for sensor...
netgear_lte:
- host: 192.168.5.1
password: *********
notify:
- name: sms1
target: "+3955555555"
- name: sms2
target: "+3955555555"
sensor:
- usage
- sms
This has some precedence, for example with the Cast component media_player configuration. But I am starting to wonder whether this is actually an architectural question. It would be really good to have guidelines for this quite common situation.
BTW @andtos90, it's awesome to see you keep positive during that rather tough review 👍
There was a problem hiding this comment.
I like this solution but should it be sensors instead of sensor? Just to know for future developments.
Your team is doing a great work and in the process I'm learning a lot, thanks for your work 😄
There was a problem hiding this comment.
I am not sure and there are no rules :-/
We create sensor entities, that's why I picked singular. I agree that it looks a bit odd if one reads it just as yaml.
|
|
||
| async def cleanup(event): | ||
| """Clean up resources.""" | ||
| task.cancel() |
There was a problem hiding this comment.
This will error if task doesn't exist, eg if there was never a connection error. Also probably check that task is not done before cancelling it.
| _LOGGER.error("No modem available") | ||
| return | ||
|
|
||
| phone = self.config.get(ATTR_TARGET) |
There was a problem hiding this comment.
Target is required in the config schema so use dict[key].
|
|
||
| for conf in config.get(DOMAIN, []): | ||
| for notify_conf in conf[CONF_DEVICES].get(ATTR_TARGETS): | ||
| discovery.load_platform(hass, 'notify', DOMAIN, notify_conf, conf) |
There was a problem hiding this comment.
Create tasks with hass.async_create_task and use discovery.async_load_platform.
hass.async_create_task(discovery.async_load_platform(
hass, 'notify', DOMAIN, notify_conf, config))Note that we need to send the original whole config as last argument to load platform.
| DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ | ||
| vol.Required(CONF_HOST): cv.string, | ||
| vol.Required(CONF_PASSWORD): cv.string, | ||
| vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, |
| vol.Required(CONF_HOST): cv.string, | ||
| vol.Required(CONF_PASSWORD): cv.string, | ||
| vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, | ||
| vol.Required(CONF_DEVICES): { |
|
|
||
| async def cleanup(event): | ||
| """Clean up resources.""" | ||
| if task is not None and not task.done(): |
There was a problem hiding this comment.
Init task to None in the beginning of _setup_lte. Otherwise there could be an error.
| if tasks: | ||
| await asyncio.wait(tasks) | ||
|
|
||
| for conf in config.get(DOMAIN, []): |
There was a problem hiding this comment.
We could store the domain config in a variable early since we use it above too.
| setup_task.cancel() | ||
| await modem.logout() | ||
|
|
||
| hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@amelchio thanks for the hint, I reused your solution with the addition of two CancelledError exception handlers, hope that are useful there
There was a problem hiding this comment.
I believe catching CancelledError is only necessary if we want to do something, like release resources.
There was a problem hiding this comment.
As @MartinHjelmare pointed out in a older commit with a CancelledError handler we avoid to print a not useful stack trace in some cases. Does homeassistant loop handle this for us?
There was a problem hiding this comment.
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| except asyncio.CancelledError: | ||
| return | ||
|
|
||
| _LOGGER.warning("Connected to %s", modem_data.host) |
There was a problem hiding this comment.
We shouldn't warn the first time we login if we never failed to login. It's ok to log at info level the first time if we never failed.
MartinHjelmare
left a comment
There was a problem hiding this comment.
Looks good! Last fix and we can merge. Ok with you @amelchio?
| DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ | ||
| vol.Required(CONF_HOST): cv.string, | ||
| vol.Required(CONF_PASSWORD): cv.string, | ||
| vol.Optional(SERVICE_NOTIFY): |
There was a problem hiding this comment.
Define CONF_NOTIFY in this component. Config keys should be stored in constants called CONF_*.
There was a problem hiding this comment.
Do you mean just rename _NOTIFY_SCHEMA to CONF_NOTIFY or something else?
There was a problem hiding this comment.
Replace SERVICE_NOTIFY with a locally defined CONF_NOTIFY.
MartinHjelmare
left a comment
There was a problem hiding this comment.
Great! Can be merged when build passes.
|
Thanks! 🎉 |
Description:
A new component to handle TP-Link LTE routers. The only feature implemented is a notify service to send SMS trough the the router. I plan to add new features like router status and SMS inbox.
This component has a lot of similarities with the
netgear_ltecomponent and my code is heavily based on it. I think that the 2 components could be merged together as an abstract "LTE modem" handler. I'm going to find out where is the best place to talk about this proposal but for now I think that two separate components is the right solution.Pull request in home-assistant.io with documentation (if applicable): home-assistant/home-assistant.io#6485
Example entry for
configuration.yaml(if applicable):Checklist:
tox. Your PR cannot be merged unless tests passTox with virtualenv seems to be broken in my local environment, I'll figure out a solution but for now I'm going to check the results on Travis
If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
REQUIREMENTSvariable (example).requirements_all.txtby runningscript/gen_requirements_all.py..coveragerc.