From f7d4c48199d8333d9d9397a992a611d7a188f2c4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Apr 2019 10:09:17 -0700 Subject: [PATCH] Convert service helper to use async_get_integration (#23023) * Convert service helper to use async_get_integration * Fix tests --- homeassistant/helpers/service.py | 75 ++++++++++++++++++-------------- homeassistant/loader.py | 10 +++-- tests/common.py | 2 +- tests/test_loader.py | 13 +++--- 4 files changed, 57 insertions(+), 43 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index ea62d12c66c02e..f5de2419fd4416 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -2,7 +2,6 @@ import asyncio from functools import wraps import logging -from os import path from typing import Callable import voluptuous as vol @@ -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' @@ -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', {}) } diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 0b7495bcb6941b..44e5ab23d78ca7 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -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 @@ -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] diff --git a/tests/common.py b/tests/common.py index 4aa13fc9be6ece..255ceacedfbbf5 100644 --- a/tests/common.py +++ b/tests/common.py @@ -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) diff --git a/tests/test_loader.py b/tests/test_loader.py index 9598906a82ba17..a70a34651eb4cb 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -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']