#!/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()