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