Skip to content
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

Merged
merged 5 commits into from May 7, 2018
Merged

Conversation

jeradM
Copy link
Member

@jeradM jeradM commented Mar 5, 2018

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):

sabnzbd:
  api_key: YOUR_SABNZBD_API_KEY

Checklist:

  • The code change is tested and works locally.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • Local tests with tox run successfully. Your PR cannot be merged unless tests pass
  • New dependencies have been added to the REQUIREMENTS variable (example).
  • New dependencies are only imported inside functions that use them (example).
  • New dependencies have been added to requirements_all.txt by running script/gen_requirements_all.py.
  • New files were added to .coveragerc.


@asyncio.coroutine
def async_update(self):
def update_state(self, args):
Copy link
Member

@balloob balloob May 1, 2018

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)
Copy link
Member

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 ?

Copy link
Member Author

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

Copy link
Member

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.

schema=SPEED_LIMIT_SCHEMA)


def request_configuration(hass, config, host):
Copy link
Member

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 ;)

Copy link
Member Author

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.

else:
await sab_api_data.async_set_queue_speed()

hass.services.async_register(DOMAIN, SERVICE_PAUSE, async_service_handler)
Copy link
Member

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({})

Copy link
Member Author

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?

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],
Copy link
Member

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

schema=SPEED_LIMIT_SCHEMA)


def request_configuration(hass, config, host):
Copy link
Member

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_

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))
Copy link
Member

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.

req_config = _CONFIGURING.pop(host)
configurator.async_request_done(req_config)

success()
Copy link
Member

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 😞

Copy link
Member

@balloob balloob left a 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

@jeradM
Copy link
Member Author

jeradM commented May 2, 2018

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 discovery.py, but it looks like all your comments were still attached to that commit so I'm sorry if that makes it a PITA for you to review the most recent changes

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use cv.time_period

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):
Copy link
Member

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.

Copy link
Member Author

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.

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)
Copy link
Member

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,
Copy link
Member

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):
Copy link
Member

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.

@jeradM
Copy link
Member Author

jeradM commented May 5, 2018

@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?

@MartinHjelmare
Copy link
Member

It's ok to rebase. Try to avoid squashing the commits though. Then review will not be affected.

@jeradM
Copy link
Member Author

jeradM commented May 6, 2018

Thanks @MartinHjelmare. This should be cleaned up now. I think I finally have my head around the async stuff

conf = await hass.async_add_job(load_json,
hass.config.path(CONFIG_FILE))

if conf.get(base_url, {}).get(CONF_API_KEY):
Copy link
Member

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, '')

"""Handle configuration changes."""
api_key = data.get(CONF_API_KEY)
sab_api = SabnzbdApi(host, api_key)
if await async_check_sabnzbd(sab_api):
Copy link
Member

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.

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)
Copy link
Member

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.

elif service.service == SERVICE_SET_SPEED:
speed = service.data.get(ATTR_SPEED, None)
if speed is None:
speed = '100'
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok cool thanks

Copy link
Member

@MartinHjelmare MartinHjelmare left a 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]
Copy link
Member

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
...

Copy link
Member Author

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

Copy link
Member

@MartinHjelmare MartinHjelmare left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

@MartinHjelmare MartinHjelmare merged commit e60d066 into home-assistant:dev May 7, 2018
@jeradM jeradM deleted the sabnzbd branch May 11, 2018 19:47
@balloob balloob mentioned this pull request May 28, 2018
@home-assistant home-assistant locked and limited conversation to collaborators Sep 5, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants