"""UI Layout models and loader for config/layout.yaml.""" import logging from pathlib import Path from typing import Optional import yaml from pydantic import BaseModel, Field, field_validator logger = logging.getLogger(__name__) class DeviceTile(BaseModel): """Represents a device tile in the UI. Attributes: device_id: Unique identifier of the device title: Display title for the device icon: Icon name or emoji for the device rank: Sort order within the room (lower = first) """ device_id: str = Field( ..., description="Unique device identifier" ) title: str = Field( ..., description="Display title for the device" ) icon: str = Field( ..., description="Icon name or emoji" ) rank: int = Field( ..., ge=0, description="Sort order (lower values appear first)" ) class Room(BaseModel): """Represents a room containing devices. Attributes: name: Room name (e.g., "Wohnzimmer", "Küche") devices: List of device tiles in this room """ name: str = Field( ..., description="Room name" ) devices: list[DeviceTile] = Field( default_factory=list, description="Device tiles in this room" ) @field_validator('devices') @classmethod def validate_devices(cls, v: list[DeviceTile]) -> list[DeviceTile]: """Validate that devices list is not empty if provided.""" if v is not None and len(v) == 0: logger.warning("Room has empty devices list") return v class UiLayout(BaseModel): """Represents the complete UI layout configuration. Attributes: rooms: List of rooms in the layout """ rooms: list[Room] = Field( default_factory=list, description="Rooms in the layout" ) @field_validator('rooms') @classmethod def validate_rooms(cls, v: list[Room]) -> list[Room]: """Validate that rooms list is not empty.""" if not v or len(v) == 0: raise ValueError("Layout must contain at least one room") return v def total_devices(self) -> int: """Calculate total number of devices across all rooms. Returns: int: Total device count """ return sum(len(room.devices) for room in self.rooms) def load_layout(path: Optional[str] = None) -> UiLayout: """Load UI layout from YAML configuration file. Args: path: Optional path to layout.yaml. If None, uses default path (apps/ui/config/layout.yaml relative to workspace root) Returns: UiLayout: Parsed and validated layout configuration Raises: FileNotFoundError: If layout file doesn't exist ValueError: If layout validation fails yaml.YAMLError: If YAML parsing fails """ # Determine config path if path is None: # Default: config/layout.yaml at workspace root # This file is assumed to be in packages/home_capabilities/ workspace_root = Path(__file__).parent.parent.parent config_path = workspace_root / "config" / "layout.yaml" else: config_path = Path(path) # Check if file exists if not config_path.exists(): raise FileNotFoundError( f"Layout configuration not found: {config_path}. " f"Please create a layout.yaml file with room and device definitions." ) # Load YAML try: with open(config_path, "r", encoding="utf-8") as f: data = yaml.safe_load(f) except yaml.YAMLError as e: raise yaml.YAMLError(f"Failed to parse YAML in {config_path}: {e}") if data is None: raise ValueError(f"Layout file is empty: {config_path}") # Validate and parse with Pydantic try: layout = UiLayout(**data) except Exception as e: raise ValueError( f"Invalid layout configuration in {config_path}: {e}" ) # Log summary total_devices = layout.total_devices() room_names = [room.name for room in layout.rooms] logger.info( f"Loaded layout: {len(layout.rooms)} rooms, " f"{total_devices} total devices (Rooms: {', '.join(room_names)})" ) return layout