diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 1460181ddc2..c0dd231ef95 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -80,7 +80,6 @@ set_swing_mode: example: 'climate.nest' swing_mode: description: New value of swing mode. - example: turn_on: description: Turn climate device on. diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml index a39bbc01ea1..4d77101cf0d 100644 --- a/homeassistant/components/deconz/services.yaml +++ b/homeassistant/components/deconz/services.yaml @@ -19,6 +19,7 @@ configure: device_refresh: description: Refresh device lists from deCONZ. - bridgeid: - description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name. - example: '00212EFFFF012345' \ No newline at end of file + fields: + bridgeid: + description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name. + example: '00212EFFFF012345' diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index 7436bbd6ea4..938e9c8e324 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -25,40 +25,39 @@ see: description: Battery level of device. example: '100' -icloud: - icloud_lost_iphone: - description: Service to play the lost iphone sound on an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account. - example: 'iphonebart' - icloud_set_interval: - description: Service to set the interval of an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account. - example: 'iphonebart' - interval: - description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state. - example: 1 - icloud_update: - description: Service to ask for an update of an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account. - example: 'iphonebart' - icloud_reset_account: - description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device. - fields: - account_name: - description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts. - example: 'bart' +icloud_lost_iphone: + description: Service to play the lost iphone sound on an iDevice. + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account. + example: 'iphonebart' +icloud_set_interval: + description: Service to set the interval of an iDevice. + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account. + example: 'iphonebart' + interval: + description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state. + example: 1 +icloud_update: + description: Service to ask for an update of an iDevice. + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account. + example: 'iphonebart' +icloud_reset_account: + description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device. + fields: + account_name: + description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts. + example: 'bart' diff --git a/homeassistant/components/hdmi_cec/services.yaml b/homeassistant/components/hdmi_cec/services.yaml index bb0f5f932ae..f2e5f0b837a 100644 --- a/homeassistant/components/hdmi_cec/services.yaml +++ b/homeassistant/components/hdmi_cec/services.yaml @@ -19,7 +19,7 @@ send_command: are source and destination, second byte is command and optional other bytes are command parameters. If raw command specified, other params are ignored.', example: '"10:36"'} - src: {desctiption: 'Source of command. Could be decimal number or string with + src: {description: 'Source of command. Could be decimal number or string with hexadeximal notation: "0x10".', example: 12 or "0xc"} standby: {description: Standby all devices which supports it.} update: {description: Update devices state from network.} diff --git a/homeassistant/components/system_log/services.yaml b/homeassistant/components/system_log/services.yaml index c168185c9b3..2545d47c825 100644 --- a/homeassistant/components/system_log/services.yaml +++ b/homeassistant/components/system_log/services.yaml @@ -1,15 +1,15 @@ -system_log: - clear: - description: Clear all log entries. - write: - description: Write log entry. - fields: - message: - description: Message to log. [Required] - example: Something went wrong - level: - description: "Log level: debug, info, warning, error, critical. Defaults to 'error'." - example: debug - logger: - description: Logger name under which to log the message. Defaults to 'system_log.external'. - example: mycomponent.myplatform +clear: + description: Clear all log entries. + +write: + description: Write log entry. + fields: + message: + description: Message to log. [Required] + example: Something went wrong + level: + description: "Log level: debug, info, warning, error, critical. Defaults to 'error'." + example: debug + logger: + description: Logger name under which to log the message. Defaults to 'system_log.external'. + example: mycomponent.myplatform diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 7c926a5a879..83e6ea2533b 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -30,15 +30,15 @@ heal_network: description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW_Log.txt for progress. fields: return_routes: - description: Whether or not to update the return routes from the nodes to the controller. Defaults to False. - example: True + description: Whether or not to update the return routes from the nodes to the controller. Defaults to False. + example: True heal_node: description: Start a Z-Wave node heal. Refer to OZW_Log.txt for progress. fields: return_routes: - description: Whether or not to update the return routes from the node to the controller. Defaults to False. - example: True + description: Whether or not to update the return routes from the node to the controller. Defaults to False. + example: True remove_node: description: Remove a node from the Z-Wave network. Refer to OZW_Log.txt for progress. @@ -160,7 +160,7 @@ test_node: example: 10 messages: description: Optional. Amount of test messages to send. - example: 3 + example: 3 rename_node: description: Set the name of a node. This will also affect the IDs of all entities in the node. diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 2514db6314d..b555f98d883 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -3,12 +3,13 @@ import pathlib import sys from .model import Integration, Config -from . import dependencies, manifest, codeowners +from . import dependencies, manifest, codeowners, services PLUGINS = [ manifest, dependencies, codeowners, + services, ] @@ -37,6 +38,7 @@ def main(): manifest.validate(integrations, config) dependencies.validate(integrations, config) codeowners.validate(integrations, config) + services.validate(integrations, config) # When we generate, all errors that are fixable will be ignored, # as generating them will be fixed. diff --git a/script/hassfest/model.py b/script/hassfest/model.py index c2a72ebd509..059231cf954 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -61,26 +61,22 @@ class Integration: """Integration domain.""" return self.path.name - @property - def manifest_path(self) -> pathlib.Path: - """Integration manifest path.""" - return self.path / 'manifest.json' - def add_error(self, *args, **kwargs): """Add an error.""" self.errors.append(Error(*args, **kwargs)) def load_manifest(self) -> None: """Load manifest.""" - if not self.manifest_path.is_file(): + manifest_path = self.path / 'manifest.json' + if not manifest_path.is_file(): self.add_error( 'model', - "Manifest file {} not found".format(self.manifest_path) + "Manifest file {} not found".format(manifest_path) ) return try: - manifest = json.loads(self.manifest_path.read_text()) + manifest = json.loads(manifest_path.read_text()) except ValueError as err: self.add_error( 'model', diff --git a/script/hassfest/services.py b/script/hassfest/services.py new file mode 100644 index 00000000000..9765eff1d36 --- /dev/null +++ b/script/hassfest/services.py @@ -0,0 +1,104 @@ +"""Validate dependencies.""" +import pathlib +from typing import Dict + +import re +import voluptuous as vol +from voluptuous.humanize import humanize_error + +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv +from homeassistant.util.yaml import load_yaml + +from .model import Integration + + +def exists(value): + """Check if value exists.""" + if value is None: + raise vol.Invalid("Value cannot be None") + return value + + +FIELD_SCHEMA = vol.Schema({ + vol.Required('description'): str, + vol.Optional('example'): exists, + vol.Optional('default'): exists, + vol.Optional('values'): exists, + vol.Optional('required'): bool, +}) + +SERVICE_SCHEMA = vol.Schema({ + vol.Required('description'): str, + vol.Optional('fields'): vol.Schema({ + str: FIELD_SCHEMA + }) +}) + +SERVICES_SCHEMA = vol.Schema({ + cv.slug: SERVICE_SCHEMA +}) + + +def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) \ + -> bool: + """Recursively go through a dir and it's children and find the regex.""" + pattern = re.compile(search_pattern) + + for fil in path.glob(glob_pattern): + if not fil.is_file(): + continue + + if pattern.search(fil.read_text()): + return True + + return False + + +def validate_services(integration: Integration): + """Validate services.""" + # Find if integration uses services + has_services = grep_dir(integration.path, "**/*.py", + r"hass\.(services|async_register)") + + if not has_services: + return + + try: + data = load_yaml(str(integration.path / 'services.yaml')) + except FileNotFoundError: + print( + "Warning: {} registeres services but has no services.yaml".format( + integration.domain)) + # integration.add_error( + # 'services', 'Registers services but has no services.yaml') + return + except HomeAssistantError: + integration.add_error( + 'services', 'Registers services but unable to load services.yaml') + return + + try: + SERVICES_SCHEMA(data) + except vol.Invalid as err: + integration.add_error( + 'services', + "Invalid services.yaml: {}".format(humanize_error(data, err))) + + +def validate(integrations: Dict[str, Integration], config): + """Handle dependencies for integrations.""" + # check services.yaml is cool + for integration in integrations.values(): + if not integration.manifest: + continue + + validate_services(integration) + + # check that all referenced dependencies exist + for dep in integration.manifest['dependencies']: + if dep not in integrations: + integration.add_error( + 'dependencies', + "Dependency {} does not exist" + )