Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion homeassistant/components/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
__version__)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import AsyncTrackStates
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers import template
from homeassistant.components.http import HomeAssistantView

Expand Down Expand Up @@ -358,7 +359,7 @@ def post(self, request):
def async_services_json(hass):
"""Generate services data to JSONify."""
return [{"domain": key, "services": value}
for key, value in hass.services.async_services().items()]
for key, value in async_get_all_descriptions(hass).items()]


def async_events_json(hass):
Expand Down
17 changes: 4 additions & 13 deletions homeassistant/components/light/lifx.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import asyncio
import sys
import math
from os import path
from functools import partial
from datetime import timedelta

Expand All @@ -22,7 +21,6 @@
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT,
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT,
preprocess_turn_on_alternatives)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant import util
from homeassistant.core import callback
Expand Down Expand Up @@ -210,13 +208,10 @@ def __init__(self, hass, async_add_devices):
self.async_add_devices = async_add_devices
self.effects_conductor = aiolifx_effects().Conductor(loop=hass.loop)

descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
self.register_set_state()
self.register_effects()

self.register_set_state(descriptions)
self.register_effects(descriptions)

def register_set_state(self, descriptions):
def register_set_state(self):
"""Register the LIFX set_state service call."""
@asyncio.coroutine
def async_service_handle(service):
Expand All @@ -231,10 +226,9 @@ def async_service_handle(service):

self.hass.services.async_register(
DOMAIN, SERVICE_LIFX_SET_STATE, async_service_handle,
descriptions.get(SERVICE_LIFX_SET_STATE),
schema=LIFX_SET_STATE_SCHEMA)

def register_effects(self, descriptions):
def register_effects(self):
"""Register the LIFX effects as hass service calls."""
@asyncio.coroutine
def async_service_handle(service):
Expand All @@ -246,17 +240,14 @@ def async_service_handle(service):

self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_PULSE, async_service_handle,
descriptions.get(SERVICE_EFFECT_PULSE),
schema=LIFX_EFFECT_PULSE_SCHEMA)

self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_COLORLOOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_COLORLOOP),
schema=LIFX_EFFECT_COLORLOOP_SCHEMA)

self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_STOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_STOP),
schema=LIFX_EFFECT_STOP_SCHEMA)

@asyncio.coroutine
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/scene/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Describes the format for available scene services

turn_on:
description: Activate a scene.
fields:
entity_id:
description: Name(s) of scenes to turn on
example: 'scene.romantic'
5 changes: 3 additions & 2 deletions homeassistant/components/websocket_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from homeassistant.core import callback
from homeassistant.remote import JSONEncoder
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.auth import validate_password
from homeassistant.components.http.const import KEY_AUTHENTICATED
Expand Down Expand Up @@ -466,8 +467,8 @@ def handle_get_services(self, msg):
"""
msg = GET_SERVICES_MESSAGE_SCHEMA(msg)

self.to_write.put_nowait(result_message(
msg['id'], self.hass.services.async_services()))
descriptions = async_get_all_descriptions(self.hass)
self.to_write.put_nowait(result_message(msg['id'], descriptions))

def handle_get_config(self, msg):
"""Handle get config command.
Expand Down
28 changes: 4 additions & 24 deletions homeassistant/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,25 +754,15 @@ def async_set(self, entity_id, new_state, attributes=None,
class Service(object):
"""Representation of a callable service."""

__slots__ = ['func', 'description', 'fields', 'schema',
'is_callback', 'is_coroutinefunction']
__slots__ = ['func', 'schema', 'is_callback', 'is_coroutinefunction']

def __init__(self, func, description, fields, schema):
def __init__(self, func, schema):
"""Initialize a service."""
self.func = func
self.description = description or ''
self.fields = fields or {}
self.schema = schema
self.is_callback = is_callback(func)
self.is_coroutinefunction = asyncio.iscoroutinefunction(func)

def as_dict(self):
"""Return dictionary representation of this service."""
return {
'description': self.description,
'fields': self.fields,
}


class ServiceCall(object):
"""Representation of a call to a service."""
Expand Down Expand Up @@ -826,9 +816,7 @@ def async_services(self):

This method must be run in the event loop.
"""
return {domain: {key: value.as_dict() for key, value
in self._services[domain].items()}
for domain in self._services}
return {domain: self._services[domain] for domain in self._services}

def has_service(self, domain, service):
"""Test if specified service exists.
Expand All @@ -842,9 +830,6 @@ def register(self, domain, service, service_func, description=None,
"""
Register a service.

Description is a dict containing key 'description' to describe
the service and a key 'fields' to describe the fields.

Schema is called to coerce and validate the service data.
"""
run_callback_threadsafe(
Expand All @@ -859,18 +844,13 @@ def async_register(self, domain, service, service_func, description=None,
"""
Register a service.

Description is a dict containing key 'description' to describe
the service and a key 'fields' to describe the fields.

Schema is called to coerce and validate the service data.

This method must be run in the event loop.
"""
domain = domain.lower()
service = service.lower()
description = description or {}
service_obj = Service(service_func, description.get('description'),
description.get('fields', {}), schema)
service_obj = Service(service_func, schema)

if domain in self._services:
self._services[domain][service] = service_obj
Expand Down
47 changes: 46 additions & 1 deletion homeassistant/helpers/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import logging
# pylint: disable=unused-import
from typing import Optional # NOQA
from os import path

import voluptuous as vol

from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant # NOQA
import homeassistant.core as ha
from homeassistant.exceptions import TemplateError
from homeassistant.loader import get_component, bind_hass
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe

Expand All @@ -21,6 +23,8 @@

_LOGGER = logging.getLogger(__name__)

DESCRIPTION_CACHE = {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Don't store the description cache in a global. Instead store it in hass.data, which is a dict for globals during the runtime of a hass instance.



@bind_hass
def call_from_config(hass, config, blocking=False, variables=None,
Expand Down Expand Up @@ -112,3 +116,44 @@ def extract_entity_ids(hass, service_call, expand_group=True):
return [service_ent_id]

return service_ent_id

@bind_hass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1

def get_description(hass, domain, service):
"""Return the description (i.e. user documentation) for a service call."""

if domain == ha.DOMAIN:
import homeassistant.components as components
file = components.__file__

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

file is a python builtin. Please use another variable name, eg fil, comp_file or similar.

else:
file = get_component(domain).__file__

descriptions = DESCRIPTION_CACHE.get(file)
if descriptions is None:
descriptions = load_yaml_config_file(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This call will do I/O. Since get_description will be called from async_get_all_descriptions, the function that does I/O needs to be scheduled on the executor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh. This was moved from the LIFX async_setup_platform but I guess that was also wrong.

I am a bit confused by the main/executor transitions available. What is the proper way to schedule the function in this situation?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the function that schedules the job for the executor, via hass.async_add_job, needs to be a coroutine to be able to yield and wait for the result, ie to have the descriptions ready. Not 💯 though. I'd wait for a core review before changing anything.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You are correct Martin

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this file might not exist

path.join(path.dirname(file), 'services.yaml'))
DESCRIPTION_CACHE[file] = descriptions

if domain == ha.DOMAIN:
description = descriptions[domain].get(service, {})
else:
description = descriptions.get(service, {})

return {
'description': description.get('description', ''),
'fields': description.get('fields', {})
}

@bind_hass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1

def async_get_all_descriptions(hass):
"""Return the descriptions for all service calls."""

services = hass.services.async_services()
descriptions = {}
for domain in services:
descriptions[domain] = {}
for service in services[domain]:
description = get_description(hass, domain, service)
if description:
descriptions[domain][service] = description

return descriptions