dynamic dashboard initial
This commit is contained in:
158
packages/home_capabilities/layout.py
Normal file
158
packages/home_capabilities/layout.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user