mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Add services.yaml validator (#23205)
* Add services.yaml validator * Fix path
This commit is contained in:
parent
37cd711c96
commit
33b8241d26
@ -80,7 +80,6 @@ set_swing_mode:
|
|||||||
example: 'climate.nest'
|
example: 'climate.nest'
|
||||||
swing_mode:
|
swing_mode:
|
||||||
description: New value of swing mode.
|
description: New value of swing mode.
|
||||||
example:
|
|
||||||
|
|
||||||
turn_on:
|
turn_on:
|
||||||
description: Turn climate device on.
|
description: Turn climate device on.
|
||||||
|
@ -19,6 +19,7 @@ configure:
|
|||||||
|
|
||||||
device_refresh:
|
device_refresh:
|
||||||
description: Refresh device lists from deCONZ.
|
description: Refresh device lists from deCONZ.
|
||||||
|
fields:
|
||||||
bridgeid:
|
bridgeid:
|
||||||
description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name.
|
description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name.
|
||||||
example: '00212EFFFF012345'
|
example: '00212EFFFF012345'
|
@ -25,7 +25,6 @@ see:
|
|||||||
description: Battery level of device.
|
description: Battery level of device.
|
||||||
example: '100'
|
example: '100'
|
||||||
|
|
||||||
icloud:
|
|
||||||
icloud_lost_iphone:
|
icloud_lost_iphone:
|
||||||
description: Service to play the lost iphone sound on an iDevice.
|
description: Service to play the lost iphone sound on an iDevice.
|
||||||
fields:
|
fields:
|
||||||
|
@ -19,7 +19,7 @@ send_command:
|
|||||||
are source and destination, second byte is command and optional other bytes
|
are source and destination, second byte is command and optional other bytes
|
||||||
are command parameters. If raw command specified, other params are ignored.',
|
are command parameters. If raw command specified, other params are ignored.',
|
||||||
example: '"10:36"'}
|
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"}
|
hexadeximal notation: "0x10".', example: 12 or "0xc"}
|
||||||
standby: {description: Standby all devices which supports it.}
|
standby: {description: Standby all devices which supports it.}
|
||||||
update: {description: Update devices state from network.}
|
update: {description: Update devices state from network.}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
system_log:
|
|
||||||
clear:
|
clear:
|
||||||
description: Clear all log entries.
|
description: Clear all log entries.
|
||||||
|
|
||||||
write:
|
write:
|
||||||
description: Write log entry.
|
description: Write log entry.
|
||||||
fields:
|
fields:
|
||||||
|
@ -3,12 +3,13 @@ import pathlib
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from .model import Integration, Config
|
from .model import Integration, Config
|
||||||
from . import dependencies, manifest, codeowners
|
from . import dependencies, manifest, codeowners, services
|
||||||
|
|
||||||
PLUGINS = [
|
PLUGINS = [
|
||||||
manifest,
|
manifest,
|
||||||
dependencies,
|
dependencies,
|
||||||
codeowners,
|
codeowners,
|
||||||
|
services,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ def main():
|
|||||||
manifest.validate(integrations, config)
|
manifest.validate(integrations, config)
|
||||||
dependencies.validate(integrations, config)
|
dependencies.validate(integrations, config)
|
||||||
codeowners.validate(integrations, config)
|
codeowners.validate(integrations, config)
|
||||||
|
services.validate(integrations, config)
|
||||||
|
|
||||||
# When we generate, all errors that are fixable will be ignored,
|
# When we generate, all errors that are fixable will be ignored,
|
||||||
# as generating them will be fixed.
|
# as generating them will be fixed.
|
||||||
|
@ -61,26 +61,22 @@ class Integration:
|
|||||||
"""Integration domain."""
|
"""Integration domain."""
|
||||||
return self.path.name
|
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):
|
def add_error(self, *args, **kwargs):
|
||||||
"""Add an error."""
|
"""Add an error."""
|
||||||
self.errors.append(Error(*args, **kwargs))
|
self.errors.append(Error(*args, **kwargs))
|
||||||
|
|
||||||
def load_manifest(self) -> None:
|
def load_manifest(self) -> None:
|
||||||
"""Load manifest."""
|
"""Load manifest."""
|
||||||
if not self.manifest_path.is_file():
|
manifest_path = self.path / 'manifest.json'
|
||||||
|
if not manifest_path.is_file():
|
||||||
self.add_error(
|
self.add_error(
|
||||||
'model',
|
'model',
|
||||||
"Manifest file {} not found".format(self.manifest_path)
|
"Manifest file {} not found".format(manifest_path)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manifest = json.loads(self.manifest_path.read_text())
|
manifest = json.loads(manifest_path.read_text())
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
self.add_error(
|
self.add_error(
|
||||||
'model',
|
'model',
|
||||||
|
104
script/hassfest/services.py
Normal file
104
script/hassfest/services.py
Normal 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"
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user