commit 46d8339cef6316a8f4a8dbc03597c89eb48f2be4 Author: jessikitty Date: Tue Dec 23 01:12:48 2025 +1100 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 diff --git a/ha_chore_cleanup.py b/ha_chore_cleanup.py new file mode 100644 index 0000000..245aceb --- /dev/null +++ b/ha_chore_cleanup.py @@ -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()