Skip to content

Commit

Permalink
Add services.yaml validator (home-assistant#23205)
Browse files Browse the repository at this point in the history
* Add services.yaml validator

* Fix path
  • Loading branch information
balloob authored Apr 18, 2019
1 parent 37cd711 commit 33b8241
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 71 deletions.
1 change: 0 additions & 1 deletion homeassistant/components/climate/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions homeassistant/components/deconz/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
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'
73 changes: 36 additions & 37 deletions homeassistant/components/device_tracker/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
2 changes: 1 addition & 1 deletion homeassistant/components/hdmi_cec/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.}
Expand Down
30 changes: 15 additions & 15 deletions homeassistant/components/system_log/services.yaml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 5 additions & 5 deletions homeassistant/components/zwave/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion script/hassfest/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import sys

from .model import Integration, Config
from . import dependencies, manifest, codeowners
from . import dependencies, manifest, codeowners, services

PLUGINS = [
manifest,
dependencies,
codeowners,
services,
]


Expand Down Expand Up @@ -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.
Expand Down
12 changes: 4 additions & 8 deletions script/hassfest/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,26 +61,22 @@ def domain(self) -> str:
"""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',
Expand Down
104 changes: 104 additions & 0 deletions script/hassfest/services.py
Original file line number Diff line number Diff line change
@@ -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"
)

0 comments on commit 33b8241

Please sign in to comment.