Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
75 changes: 42 additions & 33 deletions homeassistant/helpers/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import asyncio
from functools import wraps
import logging
from os import path
from typing import Callable

import voluptuous as vol
Expand All @@ -11,12 +10,14 @@
from homeassistant.const import (
ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ATTR_AREA_ID)
import homeassistant.core as ha
from homeassistant.exceptions import TemplateError, Unauthorized, UnknownUser
from homeassistant.exceptions import (
HomeAssistantError, TemplateError, Unauthorized, UnknownUser)
from homeassistant.helpers import template, typing
from homeassistant.loader import get_component, bind_hass
from homeassistant.loader import async_get_integration, bind_hass
from homeassistant.util.yaml import load_yaml
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async_ import run_coroutine_threadsafe
from homeassistant.helpers.typing import HomeAssistantType

CONF_SERVICE = 'service'
CONF_SERVICE_TEMPLATE = 'service_template'
Expand Down Expand Up @@ -152,60 +153,68 @@ async def async_extract_entity_ids(hass, service_call, expand_group=True):
return extracted


async def _load_services_file(hass: HomeAssistantType, domain: str):
"""Load services file for an integration."""
integration = await async_get_integration(hass, domain)
try:
return await hass.async_add_executor_job(
load_yaml, str(integration.file_path / 'services.yaml'))
except FileNotFoundError:
_LOGGER.warning("Unable to find services.yaml for the %s integration",
domain)
return {}
except HomeAssistantError:
_LOGGER.warning("Unable to parse services.yaml for the %s integration",
domain)
return {}


@bind_hass
async def async_get_all_descriptions(hass):
"""Return descriptions (i.e. user documentation) for all service calls."""
if SERVICE_DESCRIPTION_CACHE not in hass.data:
hass.data[SERVICE_DESCRIPTION_CACHE] = {}
description_cache = hass.data[SERVICE_DESCRIPTION_CACHE]

descriptions_cache = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {})
format_cache_key = '{}.{}'.format

def domain_yaml_file(domain):
"""Return the services.yaml location for a domain."""
component_path = path.dirname(get_component(hass, domain).__file__)
return path.join(component_path, 'services.yaml')

def load_services_files(yaml_files):
"""Load and parse services.yaml files."""
loaded = {}
for yaml_file in yaml_files:
try:
loaded[yaml_file] = load_yaml(yaml_file)
except FileNotFoundError:
loaded[yaml_file] = {}

return loaded

services = hass.services.async_services()

# Load missing files
# See if there are new services not seen before.
# Any service that we saw before already has an entry in description_cache.
missing = set()
for domain in services:
for service in services[domain]:
if format_cache_key(domain, service) not in description_cache:
missing.add(domain_yaml_file(domain))
if format_cache_key(domain, service) not in descriptions_cache:
missing.add(domain)
break

# Files we loaded for missing descriptions
loaded = {}

if missing:
loaded = await hass.async_add_job(load_services_files, missing)
contents = await asyncio.gather(*[
_load_services_file(hass, domain) for domain in missing
])

for domain, content in zip(missing, contents):
loaded[domain] = content

# Build response
descriptions = {}
for domain in services:
descriptions[domain] = {}
yaml_file = domain_yaml_file(domain)

for service in services[domain]:
cache_key = format_cache_key(domain, service)
description = description_cache.get(cache_key)
description = descriptions_cache.get(cache_key)

# Cache missing descriptions
if description is None:
yaml_services = loaded[yaml_file]
yaml_description = yaml_services.get(service, {})
domain_yaml = loaded[domain]
yaml_description = domain_yaml.get(service, {})

if not yaml_description:
_LOGGER.warning("Missing service description for %s/%s",
domain, service)

description = description_cache[cache_key] = {
description = descriptions_cache[cache_key] = {
'description': yaml_description.get('description', ''),
'fields': yaml_description.get('fields', {})
}
Expand Down
10 changes: 7 additions & 3 deletions homeassistant/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ def resolve_from_root(cls, hass: 'HomeAssistant', root_module: ModuleType,
continue

return cls(
hass, "{}.{}".format(root_module.__name__, domain), manifest
hass, "{}.{}".format(root_module.__name__, domain),
manifest_path.parent, manifest
)

return None
Expand All @@ -105,13 +106,16 @@ def resolve_legacy(cls, hass: 'HomeAssistant', domain: str) \
return None

return cls(
hass, comp.__name__, manifest_from_legacy_module(comp)
hass, comp.__name__, pathlib.Path(comp.__file__).parent,
manifest_from_legacy_module(comp)
)

def __init__(self, hass: 'HomeAssistant', pkg_path: str, manifest: Dict):
def __init__(self, hass: 'HomeAssistant', pkg_path: str,
file_path: pathlib.Path, manifest: Dict):
"""Initialize an integration."""
self.hass = hass
self.pkg_path = pkg_path
self.file_path = file_path
self.name = manifest['name'] # type: str
self.domain = manifest['domain'] # type: str
self.dependencies = manifest['dependencies'] # type: List[str]
Expand Down
2 changes: 1 addition & 1 deletion tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,7 @@ async def get_system_health_info(hass, domain):
def mock_integration(hass, module):
"""Mock an integration."""
integration = loader.Integration(
hass, 'homeassisant.components.{}'.format(module.DOMAIN),
hass, 'homeassisant.components.{}'.format(module.DOMAIN), None,
loader.manifest_from_legacy_module(module))

_LOGGER.info("Adding mock integration: %s", module.DOMAIN)
Expand Down
13 changes: 7 additions & 6 deletions tests/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,13 @@ async def test_get_integration_custom_component(hass):

def test_integration_properties(hass):
"""Test integration properties."""
integration = loader.Integration(hass, 'homeassistant.components.hue', {
'name': 'Philips Hue',
'domain': 'hue',
'dependencies': ['test-dep'],
'requirements': ['test-req==1.0.0'],
})
integration = loader.Integration(
hass, 'homeassistant.components.hue', None, {
'name': 'Philips Hue',
'domain': 'hue',
'dependencies': ['test-dep'],
'requirements': ['test-req==1.0.0'],
})
assert integration.name == "Philips Hue"
assert integration.domain == 'hue'
assert integration.dependencies == ['test-dep']
Expand Down