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

Convert service helper to use async_get_integration #23023

Merged
merged 2 commits into from Apr 12, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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