Critical fixes for real-world HA configurations: - Scans ALL .yaml files in /config/ and /config/packages/ - Handles "- platform: template" (list item with dash) - Properly extracts sensors from "sensors:" blocks - Handles multiline templates with > operator - Correct indentation for modern template: format - Counts sensors correctly from included files This version will find all 67+ legacy template entities in typical HA setups.
425 lines
14 KiB
Bash
425 lines
14 KiB
Bash
#!/bin/bash
|
||
################################################################################
|
||
# Home Assistant Legacy Template Migration v2 (Bash)
|
||
# Handles templates in included files (sensors.yaml, binary_sensors.yaml)
|
||
################################################################################
|
||
|
||
set -e # Exit on error
|
||
|
||
# Configuration
|
||
CONFIG_DIR="/config"
|
||
BACKUP_TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||
BACKUP_DIR="$CONFIG_DIR/backups/template_migration_$BACKUP_TIMESTAMP"
|
||
TEMP_DIR="/tmp/template_migration_$$"
|
||
|
||
# Colors
|
||
GREEN='\033[0;32m'
|
||
BLUE='\033[0;34m'
|
||
YELLOW='\033[1;33m'
|
||
RED='\033[0;31m'
|
||
NC='\033[0m'
|
||
|
||
# Counters
|
||
TOTAL_SENSORS=0
|
||
TOTAL_BINARY_SENSORS=0
|
||
FILES_MODIFIED=0
|
||
|
||
echo "======================================================================"
|
||
echo "Home Assistant Legacy Template Migration v2 (Bash)"
|
||
echo "======================================================================"
|
||
echo ""
|
||
|
||
# Create temp directory
|
||
mkdir -p "$TEMP_DIR"
|
||
trap "rm -rf $TEMP_DIR" EXIT
|
||
|
||
# Create backup directory
|
||
mkdir -p "$BACKUP_DIR"
|
||
echo -e "${GREEN}✓${NC} Created backup: $BACKUP_DIR"
|
||
echo ""
|
||
|
||
################################################################################
|
||
# Extract legacy template sensors from a file
|
||
################################################################################
|
||
extract_legacy_templates() {
|
||
local file="$1"
|
||
local type="$2" # "sensor" or "binary_sensor"
|
||
local output_file="$TEMP_DIR/${type}_$(basename "$file").extracted"
|
||
|
||
# Check if file has legacy templates
|
||
if ! grep -q "platform: template" "$file" 2>/dev/null; then
|
||
return
|
||
fi
|
||
|
||
echo -e "${BLUE}ℹ${NC} Processing $(basename "$file") for ${type}..."
|
||
|
||
# Extract all sensor definitions from platform: template blocks
|
||
awk '
|
||
BEGIN {
|
||
in_platform = 0
|
||
in_sensors = 0
|
||
platform_indent = 0
|
||
}
|
||
|
||
# Detect "- platform: template" or "platform: template"
|
||
/^[[:space:]]*-?[[:space:]]*platform:[[:space:]]*template/ {
|
||
in_platform = 1
|
||
match($0, /^[[:space:]]*/)
|
||
platform_indent = RLENGTH
|
||
next
|
||
}
|
||
|
||
# Look for "sensors:" within platform block
|
||
in_platform && /^[[:space:]]*sensors:/ {
|
||
in_sensors = 1
|
||
next
|
||
}
|
||
|
||
# Extract sensor definitions
|
||
in_sensors {
|
||
# Check if we hit next platform or top-level item
|
||
if (/^[[:space:]]*-[[:space:]]*platform:/ || /^[[:space:]]*-[[:space:]]*$/ || /^[a-zA-Z]/) {
|
||
in_platform = 0
|
||
in_sensors = 0
|
||
next
|
||
}
|
||
|
||
# Print sensor lines (indented under sensors:)
|
||
if (/^[[:space:]]+[a-zA-Z_]/) {
|
||
print
|
||
}
|
||
}
|
||
' "$file" > "$output_file"
|
||
|
||
if [ -s "$output_file" ]; then
|
||
# Count unique sensor names (lines that start a sensor definition)
|
||
local count=$(grep -c "^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:" "$output_file" 2>/dev/null || echo 0)
|
||
echo " ${GREEN}✓${NC} Found $count ${type}(s)"
|
||
|
||
if [ "$type" = "sensor" ]; then
|
||
TOTAL_SENSORS=$((TOTAL_SENSORS + count))
|
||
else
|
||
TOTAL_BINARY_SENSORS=$((TOTAL_BINARY_SENSORS + count))
|
||
fi
|
||
|
||
# Backup original file (only once)
|
||
if [ ! -f "$BACKUP_DIR/$(basename "$file")" ]; then
|
||
cp "$file" "$BACKUP_DIR/$(basename "$file")"
|
||
echo " ${GREEN}✓${NC} Backed up: $(basename "$file")"
|
||
FILES_MODIFIED=$((FILES_MODIFIED + 1))
|
||
fi
|
||
else
|
||
rm -f "$output_file"
|
||
fi
|
||
}
|
||
|
||
################################################################################
|
||
# Remove legacy template platform blocks from a file
|
||
################################################################################
|
||
remove_legacy_templates() {
|
||
local file="$1"
|
||
|
||
if [ ! -f "$file" ]; then
|
||
return
|
||
fi
|
||
|
||
echo -e "${BLUE}ℹ${NC} Removing legacy templates from $(basename "$file")..."
|
||
|
||
# Use awk to remove entire platform: template blocks
|
||
awk '
|
||
BEGIN {
|
||
in_template_platform = 0
|
||
platform_indent = 0
|
||
skip_block = 0
|
||
}
|
||
|
||
# Detect "- platform: template" (list item)
|
||
/^[[:space:]]*-[[:space:]]*platform:[[:space:]]*template/ {
|
||
in_template_platform = 1
|
||
match($0, /^[[:space:]]*/)
|
||
platform_indent = RLENGTH
|
||
skip_block = 1
|
||
next
|
||
}
|
||
|
||
# Skip lines in template platform block
|
||
skip_block {
|
||
# Check if we hit next list item at same or lower indent
|
||
if (/^[[:space:]]*-[[:space:]]/ || /^[a-zA-Z]/) {
|
||
match($0, /^[[:space:]]*/)
|
||
current_indent = RLENGTH
|
||
|
||
# If at same or lower indent level, end of block
|
||
if (current_indent <= platform_indent || /^[a-zA-Z]/) {
|
||
skip_block = 0
|
||
in_template_platform = 0
|
||
print
|
||
next
|
||
}
|
||
}
|
||
# Skip this line (its part of the template block)
|
||
next
|
||
}
|
||
|
||
# Print non-skipped lines
|
||
{ print }
|
||
' "$file" > "$file.tmp"
|
||
|
||
mv "$file.tmp" "$file"
|
||
echo " ${GREEN}✓${NC} Removed legacy template blocks"
|
||
}
|
||
|
||
################################################################################
|
||
# Convert extracted templates to modern syntax
|
||
################################################################################
|
||
convert_to_modern_syntax() {
|
||
local type="$1" # "sensor" or "binary_sensor"
|
||
local output_file="$TEMP_DIR/modern_${type}.yaml"
|
||
|
||
echo " - ${type}:" > "$output_file"
|
||
|
||
# Process all extracted files for this type
|
||
for extracted in "$TEMP_DIR/${type}_"*.extracted; do
|
||
[ -f "$extracted" ] || continue
|
||
|
||
# Parse each sensor and convert to modern syntax
|
||
awk -v type="$type" '
|
||
BEGIN {
|
||
sensor_name = ""
|
||
in_sensor = 0
|
||
indent = " " # 4 spaces for items under - binary_sensor:/sensor:
|
||
field_indent = " " # 6 spaces for fields
|
||
in_multiline = 0
|
||
multiline_field = ""
|
||
}
|
||
|
||
# Detect sensor name (start of new sensor)
|
||
/^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*$/ {
|
||
if (sensor_name != "") {
|
||
# End previous sensor
|
||
print ""
|
||
}
|
||
|
||
# Start new sensor
|
||
match($0, /[a-zA-Z_][a-zA-Z0-9_]*/)
|
||
sensor_name = substr($0, RSTART, RLENGTH)
|
||
print indent "- unique_id: " sensor_name
|
||
in_sensor = 1
|
||
in_multiline = 0
|
||
next
|
||
}
|
||
|
||
in_sensor {
|
||
# Handle multiline templates (lines starting with more indent after >)
|
||
if (in_multiline && /^[[:space:]]+[^a-zA-Z_]/) {
|
||
# Continue multiline value
|
||
print
|
||
next
|
||
} else if (in_multiline) {
|
||
in_multiline = 0
|
||
}
|
||
|
||
# Convert field names
|
||
if (/friendly_name:/) {
|
||
sub(/^[[:space:]]*friendly_name:/, field_indent "name:")
|
||
print
|
||
}
|
||
else if (/value_template:/) {
|
||
sub(/^[[:space:]]*value_template:/, field_indent "state:")
|
||
print
|
||
if (/>[[:space:]]*$/) {
|
||
in_multiline = 1
|
||
}
|
||
}
|
||
else if (/icon_template:/) {
|
||
sub(/^[[:space:]]*icon_template:/, field_indent "icon:")
|
||
print
|
||
if (/>[[:space:]]*$/) {
|
||
in_multiline = 1
|
||
}
|
||
}
|
||
else if (/entity_picture_template:/) {
|
||
sub(/^[[:space:]]*entity_picture_template:/, field_indent "picture:")
|
||
print
|
||
if (/>[[:space:]]*$/) {
|
||
in_multiline = 1
|
||
}
|
||
}
|
||
else if (/availability_template:/) {
|
||
sub(/^[[:space:]]*availability_template:/, field_indent "availability:")
|
||
print
|
||
if (/>[[:space:]]*$/) {
|
||
in_multiline = 1
|
||
}
|
||
}
|
||
else if (/attribute_templates:/) {
|
||
sub(/^[[:space:]]*attribute_templates:/, field_indent "attributes:")
|
||
print
|
||
}
|
||
else if (/^[[:space:]]+[a-zA-Z_]/) {
|
||
# Keep other fields (unit_of_measurement, device_class, etc)
|
||
# Adjust indentation to match field_indent
|
||
sub(/^[[:space:]]+/, field_indent)
|
||
print
|
||
}
|
||
else if (/^[[:space:]]+/) {
|
||
# Indented content (like multiline template continuation)
|
||
print
|
||
}
|
||
}
|
||
' "$extracted" >> "$output_file"
|
||
done
|
||
}
|
||
|
||
################################################################################
|
||
# Add modern templates to configuration.yaml
|
||
################################################################################
|
||
add_to_configuration() {
|
||
local config_file="$CONFIG_DIR/configuration.yaml"
|
||
|
||
echo -e "\n${BLUE}ℹ${NC} Adding modern template definitions to configuration.yaml..."
|
||
|
||
# Backup configuration.yaml
|
||
cp "$config_file" "$BACKUP_DIR/configuration.yaml"
|
||
|
||
# Check if template: section already exists
|
||
if grep -q "^template:" "$config_file"; then
|
||
echo -e " ${YELLOW}ℹ${NC} Existing template: section found, appending..."
|
||
|
||
echo "" >> "$config_file"
|
||
echo "# Migrated templates from legacy syntax ($BACKUP_TIMESTAMP)" >> "$config_file"
|
||
echo "# NOTE: Merge these into your existing template: section" >> "$config_file"
|
||
echo "template:" >> "$config_file"
|
||
|
||
[ -f "$TEMP_DIR/modern_sensor.yaml" ] && cat "$TEMP_DIR/modern_sensor.yaml" >> "$config_file"
|
||
[ -f "$TEMP_DIR/modern_binary_sensor.yaml" ] && cat "$TEMP_DIR/modern_binary_sensor.yaml" >> "$config_file"
|
||
|
||
echo -e " ${YELLOW}⚠${NC} Manual merge required - see end of configuration.yaml"
|
||
else
|
||
echo -e " ${GREEN}✓${NC} Adding new template: section..."
|
||
|
||
echo "" >> "$config_file"
|
||
echo "# Modern template syntax (migrated $BACKUP_TIMESTAMP)" >> "$config_file"
|
||
echo "template:" >> "$config_file"
|
||
|
||
[ -f "$TEMP_DIR/modern_sensor.yaml" ] && cat "$TEMP_DIR/modern_sensor.yaml" >> "$config_file"
|
||
[ -f "$TEMP_DIR/modern_binary_sensor.yaml" ] && cat "$TEMP_DIR/modern_binary_sensor.yaml" >> "$config_file"
|
||
fi
|
||
|
||
echo -e " ${GREEN}✓${NC} Updated configuration.yaml"
|
||
}
|
||
|
||
################################################################################
|
||
# Create restore script
|
||
################################################################################
|
||
create_restore_script() {
|
||
local restore_script="$BACKUP_DIR/restore.sh"
|
||
|
||
cat > "$restore_script" << 'EOF'
|
||
#!/bin/bash
|
||
echo "Restoring from template migration backup..."
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
CONFIG_DIR="/config"
|
||
|
||
# Restore all backed up files
|
||
for file in "$SCRIPT_DIR"/*.yaml; do
|
||
[ -f "$file" ] || continue
|
||
filename=$(basename "$file")
|
||
cp "$file" "$CONFIG_DIR/$filename"
|
||
echo "✓ Restored: $filename"
|
||
done
|
||
|
||
echo ""
|
||
echo "Files restored!"
|
||
echo "Restart Home Assistant: ha core restart"
|
||
EOF
|
||
|
||
chmod +x "$restore_script"
|
||
echo -e "\n${GREEN}✓${NC} Restore script created: $restore_script"
|
||
}
|
||
|
||
################################################################################
|
||
# Main migration process
|
||
################################################################################
|
||
main() {
|
||
echo -e "${BLUE}ℹ${NC} Scanning for legacy template definitions..."
|
||
echo ""
|
||
|
||
# Find all YAML files in config directory
|
||
yaml_files=()
|
||
for file in "$CONFIG_DIR"/*.yaml; do
|
||
[ -f "$file" ] && yaml_files+=("$file")
|
||
done
|
||
|
||
# Also check packages directory
|
||
if [ -d "$CONFIG_DIR/packages" ]; then
|
||
for file in "$CONFIG_DIR/packages"/*.yaml; do
|
||
[ -f "$file" ] && yaml_files+=("$file")
|
||
done
|
||
fi
|
||
|
||
# Extract templates from all files
|
||
for file in "${yaml_files[@]}"; do
|
||
extract_legacy_templates "$file" "sensor"
|
||
extract_legacy_templates "$file" "binary_sensor"
|
||
done
|
||
|
||
# Check if we found anything
|
||
if [ $TOTAL_SENSORS -eq 0 ] && [ $TOTAL_BINARY_SENSORS -eq 0 ]; then
|
||
echo -e "\n${GREEN}✓${NC} No legacy template definitions found!"
|
||
echo "All your templates are already using modern syntax!"
|
||
rm -rf "$BACKUP_DIR"
|
||
exit 0
|
||
fi
|
||
|
||
TOTAL=$((TOTAL_SENSORS + TOTAL_BINARY_SENSORS))
|
||
|
||
echo ""
|
||
echo -e "${BLUE}ℹ${NC} Found $TOTAL legacy template entities:"
|
||
echo " - $TOTAL_SENSORS sensor(s)"
|
||
echo " - $TOTAL_BINARY_SENSORS binary_sensor(s)"
|
||
echo ""
|
||
|
||
# Convert to modern syntax
|
||
echo -e "${BLUE}ℹ${NC} Converting to modern template: syntax..."
|
||
[ $TOTAL_SENSORS -gt 0 ] && convert_to_modern_syntax "sensor"
|
||
[ $TOTAL_BINARY_SENSORS -gt 0 ] && convert_to_modern_syntax "binary_sensor"
|
||
echo -e " ${GREEN}✓${NC} Conversion complete"
|
||
echo ""
|
||
|
||
# Remove legacy definitions from original files
|
||
for file in "${yaml_files[@]}"; do
|
||
if grep -q "platform: template" "$file" 2>/dev/null; then
|
||
remove_legacy_templates "$file"
|
||
fi
|
||
done
|
||
echo ""
|
||
|
||
# Add modern templates to configuration.yaml
|
||
add_to_configuration
|
||
|
||
# Create restore script
|
||
create_restore_script
|
||
|
||
echo ""
|
||
echo "======================================================================"
|
||
echo "MIGRATION COMPLETE"
|
||
echo "======================================================================"
|
||
echo ""
|
||
echo "Backup: $BACKUP_DIR"
|
||
echo "Migrated: $TOTAL template entities"
|
||
echo "Files modified: $FILES_MODIFIED (+ configuration.yaml)"
|
||
echo ""
|
||
echo "NEXT STEPS:"
|
||
echo "1. Review configuration.yaml template: section"
|
||
echo "2. Check config: ha core check"
|
||
echo "3. Restart HA: ha core restart"
|
||
echo "4. If issues: bash $BACKUP_DIR/restore.sh"
|
||
echo ""
|
||
echo "======================================================================"
|
||
}
|
||
|
||
# Run main function
|
||
main
|