Initial commit: Home Assistant chore cleanup script
- Removes 19 unused input_boolean entities - Removes 10 unused script entities - Creates automatic backups before modifications - Generates restore script for easy rollback - Handles YAML files with !include directives - Supports multiple configuration layouts
This commit is contained in:
356
ha_chore_cleanup.py
Normal file
356
ha_chore_cleanup.py
Normal file
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Home Assistant Chore Cleanup Script
|
||||
Removes unused chore-related entities from YAML configuration files
|
||||
Creates backups before making any changes
|
||||
"""
|
||||
|
||||
import os
|
||||
import yaml
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Set, Tuple
|
||||
|
||||
# Entities to remove
|
||||
INPUT_BOOLEANS_TO_REMOVE = {
|
||||
'task_lou_clean_desk_pending',
|
||||
'task_jess_clean_desk_pending',
|
||||
'task_william_clean_desk_pending',
|
||||
'task_xander_clean_desk_pending',
|
||||
'task_bella_clean_desk_pending',
|
||||
'task_lou_clean_room_pending',
|
||||
'task_jess_clean_room_pending',
|
||||
'task_william_clean_room_pending',
|
||||
'task_xander_clean_room_pending',
|
||||
'task_bella_clean_room_pending',
|
||||
'task_tidy_lounge_pending',
|
||||
'task_vacuum_room_pending',
|
||||
'task_get_school_ready_pending',
|
||||
'task_clean_desks_done_this_week',
|
||||
'task_clean_rooms_done_this_week',
|
||||
'task_tidy_lounge_done_this_week',
|
||||
'task_tidy_kitchen_done_today',
|
||||
'task_clean_dining_table_done_today',
|
||||
'task_vacuum_room_done_today',
|
||||
}
|
||||
|
||||
SCRIPTS_TO_REMOVE = {
|
||||
'complete_task_dishwasher_unload',
|
||||
'complete_task_washing_machine_unload',
|
||||
'complete_task_dryer_unload',
|
||||
'complete_task_bins_out',
|
||||
'complete_task_bins_in',
|
||||
'complete_task_kitty_litter_clean',
|
||||
'complete_task_kitty_litter_change',
|
||||
'complete_tidy_lounge',
|
||||
'complete_vacuum_room',
|
||||
'complete_get_school_ready',
|
||||
}
|
||||
|
||||
|
||||
class YAMLCleaner:
|
||||
def __init__(self, config_dir: str = "/config"):
|
||||
self.config_dir = Path(config_dir)
|
||||
self.backup_dir = self.config_dir / "backups" / f"cleanup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
self.removed_items: Dict[str, List[str]] = {
|
||||
'input_booleans': [],
|
||||
'scripts': []
|
||||
}
|
||||
self.errors: List[str] = []
|
||||
|
||||
def create_backup_dir(self):
|
||||
"""Create backup directory"""
|
||||
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
print(f"✓ Created backup directory: {self.backup_dir}")
|
||||
|
||||
def backup_file(self, file_path: Path) -> Path:
|
||||
"""Create backup of a file before modifying"""
|
||||
if not file_path.exists():
|
||||
return None
|
||||
|
||||
backup_path = self.backup_dir / file_path.name
|
||||
shutil.copy2(file_path, backup_path)
|
||||
print(f" ✓ Backed up: {file_path.name}")
|
||||
return backup_path
|
||||
|
||||
def load_yaml_file(self, file_path: Path) -> Tuple[Dict, bool]:
|
||||
"""Load YAML file with proper handling"""
|
||||
if not file_path.exists():
|
||||
return None, False
|
||||
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if file uses !include or other special directives
|
||||
has_directives = '!include' in content or '!secret' in content
|
||||
|
||||
if has_directives:
|
||||
# For files with directives, we'll need to be more careful
|
||||
return content, True
|
||||
else:
|
||||
data = yaml.safe_load(content)
|
||||
return data, False
|
||||
|
||||
except Exception as e:
|
||||
self.errors.append(f"Error loading {file_path.name}: {str(e)}")
|
||||
return None, False
|
||||
|
||||
def save_yaml_file(self, file_path: Path, data, is_raw: bool = False):
|
||||
"""Save YAML file"""
|
||||
try:
|
||||
with open(file_path, 'w') as f:
|
||||
if is_raw:
|
||||
f.write(data)
|
||||
else:
|
||||
yaml.dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
||||
print(f" ✓ Saved: {file_path.name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.errors.append(f"Error saving {file_path.name}: {str(e)}")
|
||||
return False
|
||||
|
||||
def clean_input_booleans_from_config(self, config_data: Dict) -> Dict:
|
||||
"""Remove input_boolean entries from configuration.yaml structure"""
|
||||
if not config_data or not isinstance(config_data, dict):
|
||||
return config_data
|
||||
|
||||
if 'input_boolean' in config_data:
|
||||
original_count = len(config_data['input_boolean']) if isinstance(config_data['input_boolean'], dict) else 0
|
||||
|
||||
if isinstance(config_data['input_boolean'], dict):
|
||||
for key in list(config_data['input_boolean'].keys()):
|
||||
if key in INPUT_BOOLEANS_TO_REMOVE:
|
||||
del config_data['input_boolean'][key]
|
||||
self.removed_items['input_booleans'].append(key)
|
||||
|
||||
new_count = len(config_data['input_boolean']) if isinstance(config_data['input_boolean'], dict) else 0
|
||||
print(f" Removed {original_count - new_count} input_boolean entries")
|
||||
|
||||
return config_data
|
||||
|
||||
def clean_scripts_from_config(self, config_data: Dict) -> Dict:
|
||||
"""Remove script entries from configuration.yaml structure"""
|
||||
if not config_data or not isinstance(config_data, dict):
|
||||
return config_data
|
||||
|
||||
if 'script' in config_data:
|
||||
original_count = len(config_data['script']) if isinstance(config_data['script'], dict) else 0
|
||||
|
||||
if isinstance(config_data['script'], dict):
|
||||
for key in list(config_data['script'].keys()):
|
||||
if key in SCRIPTS_TO_REMOVE:
|
||||
del config_data['script'][key]
|
||||
self.removed_items['scripts'].append(key)
|
||||
|
||||
new_count = len(config_data['script']) if isinstance(config_data['script'], dict) else 0
|
||||
print(f" Removed {original_count - new_count} script entries")
|
||||
|
||||
return config_data
|
||||
|
||||
def clean_standalone_file(self, file_path: Path, entity_type: str):
|
||||
"""Clean a standalone YAML file (input_boolean.yaml or scripts.yaml)"""
|
||||
print(f"\nProcessing {file_path.name}...")
|
||||
|
||||
data, is_raw = self.load_yaml_file(file_path)
|
||||
if data is None:
|
||||
print(f" ! File not found or empty, skipping")
|
||||
return
|
||||
|
||||
# Backup original
|
||||
self.backup_file(file_path)
|
||||
|
||||
if is_raw:
|
||||
print(f" ! File contains directives (!include, etc.), manual review required")
|
||||
return
|
||||
|
||||
if not isinstance(data, dict):
|
||||
print(f" ! Unexpected format, skipping")
|
||||
return
|
||||
|
||||
# Remove items
|
||||
original_count = len(data)
|
||||
to_remove = INPUT_BOOLEANS_TO_REMOVE if entity_type == 'input_boolean' else SCRIPTS_TO_REMOVE
|
||||
|
||||
for key in list(data.keys()):
|
||||
if key in to_remove:
|
||||
del data[key]
|
||||
self.removed_items[entity_type + 's'].append(key)
|
||||
|
||||
new_count = len(data)
|
||||
removed_count = original_count - new_count
|
||||
|
||||
if removed_count > 0:
|
||||
self.save_yaml_file(file_path, data)
|
||||
print(f" ✓ Removed {removed_count} {entity_type} entries")
|
||||
else:
|
||||
print(f" No items to remove")
|
||||
|
||||
def process_configuration_yaml(self):
|
||||
"""Process main configuration.yaml file"""
|
||||
config_path = self.config_dir / "configuration.yaml"
|
||||
print(f"\nProcessing configuration.yaml...")
|
||||
|
||||
data, is_raw = self.load_yaml_file(config_path)
|
||||
if data is None:
|
||||
print(f" ! File not found or empty")
|
||||
return
|
||||
|
||||
if is_raw:
|
||||
print(f" ! File contains directives, will check for standalone files instead")
|
||||
return
|
||||
|
||||
# Backup original
|
||||
self.backup_file(config_path)
|
||||
|
||||
# Clean both types
|
||||
data = self.clean_input_booleans_from_config(data)
|
||||
data = self.clean_scripts_from_config(data)
|
||||
|
||||
# Save
|
||||
self.save_yaml_file(config_path, data)
|
||||
|
||||
def find_config_files(self) -> Dict[str, List[Path]]:
|
||||
"""Find all potential configuration files"""
|
||||
files = {
|
||||
'input_boolean': [],
|
||||
'script': []
|
||||
}
|
||||
|
||||
# Check for standalone files
|
||||
for pattern in ['input_boolean.yaml', 'input_booleans.yaml', 'helpers.yaml']:
|
||||
matches = list(self.config_dir.glob(pattern))
|
||||
files['input_boolean'].extend(matches)
|
||||
|
||||
for pattern in ['scripts.yaml', 'script.yaml']:
|
||||
matches = list(self.config_dir.glob(pattern))
|
||||
files['script'].extend(matches)
|
||||
|
||||
# Check packages directory
|
||||
packages_dir = self.config_dir / "packages"
|
||||
if packages_dir.exists():
|
||||
for yaml_file in packages_dir.glob("**/*.yaml"):
|
||||
# Check if file might contain our entities
|
||||
try:
|
||||
with open(yaml_file, 'r') as f:
|
||||
content = f.read().lower()
|
||||
if 'input_boolean' in content and any(key in content for key in INPUT_BOOLEANS_TO_REMOVE):
|
||||
files['input_boolean'].append(yaml_file)
|
||||
if 'script' in content and any(key in content for key in SCRIPTS_TO_REMOVE):
|
||||
files['script'].append(yaml_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
return files
|
||||
|
||||
def run_cleanup(self):
|
||||
"""Execute the cleanup process"""
|
||||
print("=" * 70)
|
||||
print("Home Assistant Chore Cleanup Script")
|
||||
print("=" * 70)
|
||||
|
||||
# Create backup directory
|
||||
self.create_backup_dir()
|
||||
|
||||
# Process main configuration file
|
||||
self.process_configuration_yaml()
|
||||
|
||||
# Find and process standalone files
|
||||
config_files = self.find_config_files()
|
||||
|
||||
# Process input_boolean files
|
||||
for file_path in config_files['input_boolean']:
|
||||
self.clean_standalone_file(file_path, 'input_boolean')
|
||||
|
||||
# Process script files
|
||||
for file_path in config_files['script']:
|
||||
self.clean_standalone_file(file_path, 'script')
|
||||
|
||||
# Print summary
|
||||
self.print_summary()
|
||||
|
||||
def print_summary(self):
|
||||
"""Print cleanup summary"""
|
||||
print("\n" + "=" * 70)
|
||||
print("CLEANUP SUMMARY")
|
||||
print("=" * 70)
|
||||
|
||||
print(f"\n✓ Backups saved to: {self.backup_dir}")
|
||||
|
||||
print(f"\n📋 Input Booleans Removed: {len(self.removed_items['input_booleans'])}")
|
||||
for item in sorted(self.removed_items['input_booleans']):
|
||||
print(f" - {item}")
|
||||
|
||||
print(f"\n📜 Scripts Removed: {len(self.removed_items['scripts'])}")
|
||||
for item in sorted(self.removed_items['scripts']):
|
||||
print(f" - {item}")
|
||||
|
||||
if self.errors:
|
||||
print(f"\n⚠️ Errors encountered: {len(self.errors)}")
|
||||
for error in self.errors:
|
||||
print(f" - {error}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("NEXT STEPS:")
|
||||
print("=" * 70)
|
||||
print("1. Review the changes in your YAML files")
|
||||
print("2. Run 'ha core check' to verify configuration")
|
||||
print("3. If everything looks good, restart Home Assistant")
|
||||
print("4. If issues occur, restore from backups in:")
|
||||
print(f" {self.backup_dir}")
|
||||
print("=" * 70)
|
||||
|
||||
# Create a restoration script
|
||||
self.create_restore_script()
|
||||
|
||||
def create_restore_script(self):
|
||||
"""Create a script to restore from backups if needed"""
|
||||
restore_script = self.backup_dir / "restore.sh"
|
||||
|
||||
script_content = f"""#!/bin/bash
|
||||
# Restore script - Run this if you need to undo the cleanup
|
||||
|
||||
echo "Restoring files from backup..."
|
||||
|
||||
"""
|
||||
|
||||
for backup_file in self.backup_dir.glob("*.yaml"):
|
||||
original = self.config_dir / backup_file.name
|
||||
script_content += f'cp "{backup_file}" "{original}"\n'
|
||||
|
||||
script_content += """
|
||||
echo "Files restored!"
|
||||
echo "Please restart Home Assistant for changes to take effect"
|
||||
"""
|
||||
|
||||
with open(restore_script, 'w') as f:
|
||||
f.write(script_content)
|
||||
|
||||
restore_script.chmod(0o755)
|
||||
print(f"\n✓ Created restore script: {restore_script}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
import sys
|
||||
|
||||
# Check if running in Home Assistant environment
|
||||
config_dir = "/config"
|
||||
|
||||
# Allow override via command line
|
||||
if len(sys.argv) > 1:
|
||||
config_dir = sys.argv[1]
|
||||
|
||||
if not os.path.exists(config_dir):
|
||||
print(f"Error: Config directory not found: {config_dir}")
|
||||
print("Usage: python3 ha_chore_cleanup.py [config_directory]")
|
||||
print("Default: /config")
|
||||
sys.exit(1)
|
||||
|
||||
cleaner = YAMLCleaner(config_dir)
|
||||
cleaner.run_cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user