diff --git a/migrate_templates.py b/migrate_templates.py new file mode 100644 index 0000000..b494f1b --- /dev/null +++ b/migrate_templates.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 +""" +Home Assistant Legacy Template Migration Script +Automatically converts legacy platform: template syntax to modern template: syntax +""" + +import yaml +import os +import re +import shutil +from datetime import datetime +from pathlib import Path + +# Configuration +CONFIG_DIR = Path("/config") +BACKUP_TIMESTAMP = datetime.now().strftime("%Y%m%d_%H%M%S") +BACKUP_DIR = CONFIG_DIR / "backups" / f"template_migration_{BACKUP_TIMESTAMP}" + +# Colors for output +GREEN = '\033[0;32m' +BLUE = '\033[0;34m' +YELLOW = '\033[1;33m' +RED = '\033[0;31m' +NC = '\033[0m' + +class TemplateMigrator: + def __init__(self): + self.legacy_sensors = [] + self.legacy_binary_sensors = [] + self.files_to_update = {} + + def find_legacy_templates(self): + """Find all legacy template definitions in YAML files""" + print(f"{BLUE}ℹ{NC} Searching for legacy template definitions...") + + # Search common locations + yaml_files = [] + for pattern in ['*.yaml', '*.yml']: + yaml_files.extend(CONFIG_DIR.glob(pattern)) + yaml_files.extend((CONFIG_DIR / 'packages').glob(f'**/{pattern}')) + + for yaml_file in yaml_files: + try: + with open(yaml_file, 'r') as f: + content = f.read() + + # Check for legacy sensor templates + if re.search(r'^\s*-?\s*platform:\s*template', content, re.MULTILINE): + self.analyze_file(yaml_file) + except Exception as e: + print(f"{YELLOW}⚠{NC} Error reading {yaml_file}: {e}") + + def analyze_file(self, filepath): + """Analyze a YAML file for legacy templates""" + try: + with open(filepath, 'r') as f: + data = yaml.safe_load(f) + + if not data: + return + + file_info = { + 'path': filepath, + 'sensors': [], + 'binary_sensors': [] + } + + # Check for legacy sensor templates + if 'sensor' in data and isinstance(data['sensor'], list): + for sensor in data['sensor']: + if isinstance(sensor, dict) and sensor.get('platform') == 'template': + if 'sensors' in sensor: + for name, config in sensor['sensors'].items(): + file_info['sensors'].append({ + 'name': name, + 'config': config + }) + + # Check for legacy binary_sensor templates + if 'binary_sensor' in data and isinstance(data['binary_sensor'], list): + for sensor in data['binary_sensor']: + if isinstance(sensor, dict) and sensor.get('platform') == 'template': + if 'sensors' in sensor: + for name, config in sensor['sensors'].items(): + file_info['binary_sensors'].append({ + 'name': name, + 'config': config + }) + + if file_info['sensors'] or file_info['binary_sensors']: + self.files_to_update[str(filepath)] = file_info + print(f" {GREEN}✓{NC} Found legacy templates in: {filepath.name}") + if file_info['sensors']: + print(f" - {len(file_info['sensors'])} sensor(s)") + if file_info['binary_sensors']: + print(f" - {len(file_info['binary_sensors'])} binary_sensor(s)") + + except Exception as e: + print(f"{YELLOW}⚠{NC} Error analyzing {filepath}: {e}") + + def convert_to_modern_syntax(self, name, config, sensor_type='sensor'): + """Convert legacy template config to modern syntax""" + modern = {} + + # Required fields + modern['name'] = config.get('friendly_name', name.replace('_', ' ').title()) + modern['state'] = config.get('value_template', '') + + # Optional fields + if 'icon_template' in config or 'icon' in config: + modern['icon'] = config.get('icon_template', config.get('icon', '')) + + if 'entity_picture_template' in config or 'entity_picture' in config: + modern['picture'] = config.get('entity_picture_template', config.get('entity_picture', '')) + + if 'availability_template' in config or 'availability' in config: + modern['availability'] = config.get('availability_template', config.get('availability', '')) + + if 'attribute_templates' in config: + modern['attributes'] = config['attribute_templates'] + + # Sensor-specific fields + if sensor_type == 'sensor': + if 'unit_of_measurement' in config: + modern['unit_of_measurement'] = config['unit_of_measurement'] + if 'device_class' in config: + modern['device_class'] = config['device_class'] + if 'state_class' in config: + modern['state_class'] = config['state_class'] + + # Binary sensor specific fields + if sensor_type == 'binary_sensor': + if 'device_class' in config: + modern['device_class'] = config['device_class'] + + # Entity ID preservation + modern['unique_id'] = name + + return modern + + def generate_modern_template_yaml(self): + """Generate the modern template: section YAML""" + template_config = [] + + # Process all sensors + all_sensors = [] + all_binary_sensors = [] + + for file_path, info in self.files_to_update.items(): + for sensor in info['sensors']: + all_sensors.append( + self.convert_to_modern_syntax(sensor['name'], sensor['config'], 'sensor') + ) + + for binary_sensor in info['binary_sensors']: + all_binary_sensors.append( + self.convert_to_modern_syntax(binary_sensor['name'], binary_sensor['config'], 'binary_sensor') + ) + + # Build template structure + if all_sensors: + template_config.append({'sensor': all_sensors}) + + if all_binary_sensors: + template_config.append({'binary_sensor': all_binary_sensors}) + + return template_config + + def backup_files(self): + """Backup all files that will be modified""" + print(f"\n{BLUE}ℹ{NC} Creating backups...") + BACKUP_DIR.mkdir(parents=True, exist_ok=True) + + for file_path in self.files_to_update.keys(): + src = Path(file_path) + dst = BACKUP_DIR / src.name + shutil.copy2(src, dst) + print(f" {GREEN}✓{NC} Backed up: {src.name}") + + def remove_legacy_definitions(self): + """Remove legacy template definitions from files""" + print(f"\n{BLUE}ℹ{NC} Removing legacy template definitions...") + + for file_path, info in self.files_to_update.items(): + try: + with open(file_path, 'r') as f: + data = yaml.safe_load(f) + + modified = False + + # Remove legacy sensor templates + if 'sensor' in data and isinstance(data['sensor'], list): + data['sensor'] = [ + s for s in data['sensor'] + if not (isinstance(s, dict) and s.get('platform') == 'template') + ] + if not data['sensor']: + del data['sensor'] + modified = True + + # Remove legacy binary_sensor templates + if 'binary_sensor' in data and isinstance(data['binary_sensor'], list): + data['binary_sensor'] = [ + s for s in data['binary_sensor'] + if not (isinstance(s, dict) and s.get('platform') == 'template') + ] + if not data['binary_sensor']: + del data['binary_sensor'] + modified = True + + if modified: + with open(file_path, 'w') as f: + yaml.dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True) + print(f" {GREEN}✓{NC} Updated: {Path(file_path).name}") + + except Exception as e: + print(f"{RED}✗{NC} Error updating {file_path}: {e}") + + def add_modern_templates(self, template_yaml): + """Add modern template definitions to configuration.yaml""" + print(f"\n{BLUE}ℹ{NC} Adding modern template definitions...") + + config_file = CONFIG_DIR / "configuration.yaml" + + try: + with open(config_file, 'r') as f: + config = yaml.safe_load(f) or {} + + # Check if template: already exists + if 'template' in config: + print(f" {YELLOW}ℹ{NC} Existing template: section found, merging...") + existing_template = config['template'] + if not isinstance(existing_template, list): + existing_template = [existing_template] + + # Merge new templates + for new_template in template_yaml: + existing_template.append(new_template) + + config['template'] = existing_template + else: + config['template'] = template_yaml + + # Write updated configuration + with open(config_file, 'w') as f: + yaml.dump(config, f, default_flow_style=False, sort_keys=False, allow_unicode=True) + + print(f" {GREEN}✓{NC} Added modern template definitions to configuration.yaml") + + except Exception as e: + print(f"{RED}✗{NC} Error updating configuration.yaml: {e}") + raise + + def create_restore_script(self): + """Create a restore script in the backup directory""" + restore_script = BACKUP_DIR / "restore.sh" + + with open(restore_script, 'w') as f: + f.write(f"""#!/bin/bash +echo "Restoring template migration backup..." +cp "{BACKUP_DIR}"/*.yaml "{CONFIG_DIR}/" +echo "Files restored!" +echo "Restart Home Assistant: ha core restart" +""") + + restore_script.chmod(0o755) + print(f"\n{GREEN}✓{NC} Restore script created: {restore_script}") + +def main(): + print("=" * 70) + print("Home Assistant Legacy Template Migration") + print("=" * 70) + print() + + migrator = TemplateMigrator() + + # Step 1: Find all legacy templates + migrator.find_legacy_templates() + + if not migrator.files_to_update: + print(f"\n{GREEN}✓{NC} No legacy template definitions found!") + return + + # Count total entities + total_sensors = sum(len(info['sensors']) for info in migrator.files_to_update.values()) + total_binary = sum(len(info['binary_sensors']) for info in migrator.files_to_update.values()) + total = total_sensors + total_binary + + print(f"\n{BLUE}ℹ{NC} Found {total} legacy template entities:") + print(f" - {total_sensors} sensor(s)") + print(f" - {total_binary} binary_sensor(s)") + + # Step 2: Generate modern template YAML + modern_template = migrator.generate_modern_template_yaml() + + # Step 3: Create backups + migrator.backup_files() + + # Step 4: Remove legacy definitions + migrator.remove_legacy_definitions() + + # Step 5: Add modern template definitions + migrator.add_modern_templates(modern_template) + + # Step 6: Create restore script + migrator.create_restore_script() + + print("\n" + "=" * 70) + print("MIGRATION COMPLETE") + print("=" * 70) + print(f"\nBackup location: {BACKUP_DIR}") + print(f"Migrated: {total} template entities") + print() + print("NEXT STEPS:") + print("1. Review configuration.yaml for the new template: section") + print("2. Check config: ha core check") + print("3. Restart HA: ha core restart") + print(f"4. If issues: bash {BACKUP_DIR}/restore.sh") + print() + print("=" * 70) + +if __name__ == "__main__": + main()