159 lines
4.3 KiB
Python
159 lines
4.3 KiB
Python
"""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
|