- Add complete homeassistant skill source to skills/ directory - Includes all scripts, references, and automation templates - Matches format of other skills in repository
187 lines
6.5 KiB
Python
Executable File
187 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Home Assistant YAML Configuration Validator
|
|
|
|
This script validates Home Assistant YAML configuration files for syntax errors
|
|
and common issues.
|
|
|
|
Usage:
|
|
# Validate a single file
|
|
python3 validate_yaml.py configuration.yaml
|
|
|
|
# Validate automation configuration
|
|
python3 validate_yaml.py automations.yaml --type automation
|
|
|
|
# Validate with verbose output
|
|
python3 validate_yaml.py configuration.yaml --verbose
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
import yaml
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
class HomeAssistantYAMLValidator:
|
|
"""Validator for Home Assistant YAML files."""
|
|
|
|
# Common required fields for different config types
|
|
AUTOMATION_REQUIRED = ['trigger']
|
|
AUTOMATION_OPTIONAL = ['alias', 'id', 'description', 'condition', 'action', 'mode', 'variables']
|
|
|
|
def __init__(self, verbose: bool = False):
|
|
self.verbose = verbose
|
|
self.errors: List[str] = []
|
|
self.warnings: List[str] = []
|
|
|
|
def validate_file(self, filepath: Path) -> bool:
|
|
"""Validate a YAML file."""
|
|
self.errors = []
|
|
self.warnings = []
|
|
|
|
if not filepath.exists():
|
|
self.errors.append(f"File not found: {filepath}")
|
|
return False
|
|
|
|
try:
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# Check for tabs (Home Assistant doesn't allow tabs)
|
|
if '\t' in content:
|
|
self.errors.append("File contains tab characters. Use spaces for indentation.")
|
|
return False
|
|
|
|
# Parse YAML
|
|
data = yaml.safe_load(content)
|
|
|
|
if self.verbose:
|
|
print(f"✓ YAML syntax is valid")
|
|
|
|
return True
|
|
|
|
except yaml.YAMLError as e:
|
|
self.errors.append(f"YAML syntax error: {str(e)}")
|
|
return False
|
|
except Exception as e:
|
|
self.errors.append(f"Error reading file: {str(e)}")
|
|
return False
|
|
|
|
def validate_automation(self, filepath: Path) -> bool:
|
|
"""Validate an automation configuration file."""
|
|
if not self.validate_file(filepath):
|
|
return False
|
|
|
|
try:
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
data = yaml.safe_load(f)
|
|
|
|
if not isinstance(data, list):
|
|
# Single automation wrapped in automation: key
|
|
if isinstance(data, dict):
|
|
if 'automation' in data:
|
|
automations = data['automation']
|
|
if not isinstance(automations, list):
|
|
automations = [automations]
|
|
else:
|
|
automations = [data]
|
|
else:
|
|
self.errors.append("Automation file must contain a list or dict")
|
|
return False
|
|
else:
|
|
automations = data
|
|
|
|
# Validate each automation
|
|
for i, automation in enumerate(automations):
|
|
if not isinstance(automation, dict):
|
|
self.errors.append(f"Automation {i+1} is not a dictionary")
|
|
continue
|
|
|
|
# Check required fields
|
|
if 'trigger' not in automation:
|
|
self.errors.append(f"Automation {i+1}: Missing required field 'trigger'")
|
|
|
|
# Check for common issues
|
|
if 'alias' not in automation:
|
|
self.warnings.append(f"Automation {i+1}: No 'alias' field (recommended for readability)")
|
|
|
|
if 'id' not in automation:
|
|
self.warnings.append(f"Automation {i+1}: No 'id' field (required for UI editor)")
|
|
|
|
# Validate trigger structure
|
|
if 'trigger' in automation:
|
|
triggers = automation['trigger']
|
|
if not isinstance(triggers, list):
|
|
triggers = [triggers]
|
|
|
|
for j, trigger in enumerate(triggers):
|
|
if not isinstance(trigger, dict):
|
|
self.errors.append(f"Automation {i+1}, Trigger {j+1}: Must be a dictionary")
|
|
continue
|
|
if 'trigger' not in trigger and 'platform' not in trigger:
|
|
self.errors.append(f"Automation {i+1}, Trigger {j+1}: Missing 'trigger' or 'platform' field")
|
|
|
|
# Validate action structure if present
|
|
if 'action' in automation:
|
|
actions = automation['action']
|
|
if not isinstance(actions, list):
|
|
actions = [actions]
|
|
|
|
for j, action in enumerate(actions):
|
|
if not isinstance(action, dict):
|
|
self.errors.append(f"Automation {i+1}, Action {j+1}: Must be a dictionary")
|
|
|
|
if self.verbose and not self.errors:
|
|
print(f"✓ Validated {len(automations)} automation(s)")
|
|
|
|
return len(self.errors) == 0
|
|
|
|
except Exception as e:
|
|
self.errors.append(f"Error validating automation: {str(e)}")
|
|
return False
|
|
|
|
def print_results(self):
|
|
"""Print validation results."""
|
|
if self.errors:
|
|
print("\n❌ Errors found:")
|
|
for error in self.errors:
|
|
print(f" • {error}")
|
|
|
|
if self.warnings:
|
|
print("\n⚠️ Warnings:")
|
|
for warning in self.warnings:
|
|
print(f" • {warning}")
|
|
|
|
if not self.errors and not self.warnings:
|
|
print("✅ Validation passed with no errors or warnings")
|
|
elif not self.errors:
|
|
print("\n✅ Validation passed (with warnings)")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Validate Home Assistant YAML files')
|
|
parser.add_argument('file', help='YAML file to validate')
|
|
parser.add_argument('--type', choices=['automation', 'general'],
|
|
default='general', help='Type of configuration to validate')
|
|
parser.add_argument('--verbose', '-v', action='store_true',
|
|
help='Verbose output')
|
|
|
|
args = parser.parse_args()
|
|
|
|
filepath = Path(args.file)
|
|
validator = HomeAssistantYAMLValidator(verbose=args.verbose)
|
|
|
|
if args.type == 'automation':
|
|
success = validator.validate_automation(filepath)
|
|
else:
|
|
success = validator.validate_file(filepath)
|
|
|
|
validator.print_results()
|
|
|
|
sys.exit(0 if success else 1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|