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
Converted SABnzbd to a component #12915
Conversation
|
||
@asyncio.coroutine | ||
def async_update(self): | ||
def update_state(self, args): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mark this as @callback
if it's an async method or call schedule_update_ha_state
instead of the async version.
self._state = self.sabnzbd_api.queue.get('noofslots_total') | ||
else: | ||
self._state = 'Unknown' | ||
self._state = self._sabnzbd_api.get_queue_field(self._field_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this doing I/O ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No I/O, just a dict lookup
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use None
as default value to represent unknown state.
homeassistant/components/sabnzbd.py
Outdated
schema=SPEED_LIMIT_SCHEMA) | ||
|
||
|
||
def request_configuration(hass, config, host): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Config entries is the new shizzle, configurator is old. You don't have to upgrade it in this PR, would be nice for a future PR ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do. When I started this config entries wasn't a thing yet, but I'll definitely convert this in the future.
homeassistant/components/sabnzbd.py
Outdated
else: | ||
await sab_api_data.async_set_queue_speed() | ||
|
||
hass.services.async_register(DOMAIN, SERVICE_PAUSE, async_service_handler) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you don't expect data, pass in vol.Schema({})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do. One question though just for my understanding: why does async_register()
have a default value of schema=None
? Would it make more sense for the default to be schema=vol.Schema({})
or to not have a default at all? Does schema=None
have a special meaning?
homeassistant/components/sabnzbd.py
Outdated
configurator = hass.components.configurator | ||
# We got an error if this method is called while we are configuring | ||
if host in _CONFIGURING: | ||
configurator.notify_errors(_CONFIGURING[host], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are inside an async context as you call this from async_setup_platform
, this needs to be async_notify_errors
homeassistant/components/sabnzbd.py
Outdated
schema=SPEED_LIMIT_SCHEMA) | ||
|
||
|
||
def request_configuration(hass, config, host): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is supposed to be called from an async context, prefix the name with async_
homeassistant/components/sabnzbd.py
Outdated
uri_scheme = 'https' if use_ssl else 'http' | ||
base_url = BASE_URL_FORMAT.format(uri_scheme, host, port) | ||
if api_key is None: | ||
conf = load_json(hass.config.path(CONFIG_FILE)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You cannot do I/O inside an async context 😮
Yield to the executor: conf = await hass.async_add_job(load_json, hass.config.path(CONFIG_FILE)
Same goes for writing.
homeassistant/components/sabnzbd.py
Outdated
req_config = _CONFIGURING.pop(host) | ||
configurator.async_request_done(req_config) | ||
|
||
success() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You removed the call to hass.async_add_job
from the previous code, and so you're doing I/O inside the event loop 😞
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quite some comments. Might want to brush up on asyncio
These should (hopefully) all be fixed now. I do think I may have screwed up your review a little bit by force pushing the updates. I added a commit a while back by fixing a merge conflict directly in the github ui and forgot I didn't have it locally. Then after rebasing upstream and adding commits I ended up with a bunch of conflicts so I just force pushed it. I didn't think it would be an issue since that one commit only touched |
homeassistant/components/sabnzbd.py
Outdated
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, | ||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, | ||
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): | ||
cv.time_period_seconds, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use cv.time_period
homeassistant/components/sabnzbd.py
Outdated
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, | ||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, | ||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, | ||
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any reason to make this configurable. Let's pick a sane default and that's it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@balloob do you think 30 seconds is a good default? I made it configurable because, for me, SABnzbd is idle probably 90% of the time and I would be happy only getting updates every 10 minutes or so. However, I can imagine some of the more "power users" would rather have more up-to-date download statistics for use in their automations to, for instance, determine when to reduce download speed or pause downloading altogether to prevent exceeding data caps. With the availability of gigabit+ residential connections, a 30 second update interval could mean downloading 3-4 GB between updates.
homeassistant/components/sabnzbd.py
Outdated
conf[host] = {CONF_API_KEY: api_key} | ||
save_json(hass.config.path(CONFIG_FILE), conf) | ||
req_config = _CONFIGURING.pop(host) | ||
configurator.async_request_done(req_config) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is synchronous, so you cannot call an async method.
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] | ||
|
||
async_dispatcher_connect(hass, SIGNAL_SABNZBD_UPDATED, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this to async_added_to_hass
. Calling the state update before the entity has been added to home assistant will error.
|
||
|
||
class SabnzbdSensor(Entity): | ||
"""Representation of an SABnzbd sensor.""" | ||
|
||
def __init__(self, sensor_type, sabnzbd_api, client_name): | ||
def __init__(self, hass, sensor_type, sabnzbd_api_data, client_name): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't pass in hass
. It will be set on the entity when it has been added to home assistant.
@balloob @MartinHjelmare I am ready to push changes, but my build is going to fail due to #14281. Should I push as-is so I don't screw up the review, or go ahead and rebase first? |
It's ok to rebase. Try to avoid squashing the commits though. Then review will not be affected. |
Thanks @MartinHjelmare. This should be cleaned up now. I think I finally have my head around the async stuff |
homeassistant/components/sabnzbd.py
Outdated
conf = await hass.async_add_job(load_json, | ||
hass.config.path(CONFIG_FILE)) | ||
|
||
if conf.get(base_url, {}).get(CONF_API_KEY): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
api_key = conf.get(base_url, {}).get(CONF_API_KEY, '')
homeassistant/components/sabnzbd.py
Outdated
"""Handle configuration changes.""" | ||
api_key = data.get(CONF_API_KEY) | ||
sab_api = SabnzbdApi(host, api_key) | ||
if await async_check_sabnzbd(sab_api): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Invert this check and return if it's true, to make it a guard clause. Then you can outdent the following code.
homeassistant/components/sabnzbd.py
Outdated
elif service.service == SERVICE_RESUME: | ||
await sab_api_data.async_resume_queue() | ||
elif service.service == SERVICE_SET_SPEED: | ||
speed = service.data.get(ATTR_SPEED, None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None
is the default value returned if the key is missing in the dict.
homeassistant/components/sabnzbd.py
Outdated
elif service.service == SERVICE_SET_SPEED: | ||
speed = service.data.get(ATTR_SPEED, None) | ||
if speed is None: | ||
speed = '100' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this default value to the service schema.
self._state = self.sabnzbd_api.queue.get('noofslots_total') | ||
else: | ||
self._state = 'Unknown' | ||
self._state = self._sabnzbd_api.get_queue_field(self._field_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use None
as default value to represent unknown state.
self._state = self._sabnzbd_api.get_queue_field(self._field_name) | ||
|
||
if self._type == 'speed': | ||
self._state = round(float(self._state) / 1024, 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will error if state is not a number.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MartinHjelmare I can add error handling here to be safe, but this should always be a number. This method will only get called by the dispatcher signal after a successful update and the library will always populate these numeric fields with a numeric value. They will always either be a number from the SABnzbd API or get converted to a numeric value by the pysabnzbd library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, fine to keep it as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok cool thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Final comment. Otherwise looks good!
async def async_setup_platform(hass, config, async_add_devices, | ||
discovery_info=None): | ||
"""Set up the SABnzbd sensors.""" | ||
sab_api_data = hass.data[DATA_SABNZBD] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only set up the sensor platform from the component now, correct? So otherwise we don't want to do anything here. Add a guard clause that checks if discovery_info is None and returns if so.
if discovery_info is None:
return
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that's correct. Fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great!
Description:
Converted SABnzbd sensor platform to a component with services that can be called to interact with the SABnzbd queue. This is a breaking change.
Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.io#4836
Example entry for
configuration.yaml
(if applicable):Checklist:
If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
tox
run successfully. Your PR cannot be merged unless tests passREQUIREMENTS
variable (example).requirements_all.txt
by runningscript/gen_requirements_all.py
..coveragerc
.