Skip to content

Commit

Permalink
Convert service helper to use async_get_integration (#23023)
Browse files Browse the repository at this point in the history
* Convert service helper to use async_get_integration

* Fix tests
  • Loading branch information
balloob committed Apr 12, 2019
1 parent c8375be commit f7d4c48
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 43 deletions.
75 changes: 42 additions & 33 deletions homeassistant/helpers/service.py
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
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
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
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

0 comments on commit f7d4c48

Please sign in to comment.