Add services.yaml validator (#23205)

* Add services.yaml validator

* Fix path
pull/23297/head
Paulus Schoutsen 2019-04-18 13:40:46 -07:00
parent 2bb772bbdc
commit 1a4a9532dd
9 changed files with 172 additions and 71 deletions

View File

@ -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.

View File

@ -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'

View File

@ -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'

View File

@ -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.}

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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',

104
script/hassfest/services.py Normal file
View File

@ -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"
)