Files
ha-template-migration/migrate_templates.py
jessikitty 4e3201c604 Add automated template migration script
Automatically migrates legacy template syntax to modern template: syntax:
- Scans all YAML files for platform: template definitions
- Converts sensors and binary_sensors to modern format
- Creates timestamped backups
- Removes legacy definitions
- Adds modern template: section to configuration.yaml
- Preserves all template features (icons, attributes, device_class, etc)
- Generates restore script for rollback

Fixes all 67 template deprecation warnings automatically.
2025-12-23 11:27:24 +11:00

324 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()