""" Room-based device control endpoints. Provides bulk control operations for devices within rooms: - /rooms/{room_name}/lights - Control all lights in a room - /rooms/{room_name}/heating - Control all thermostats in a room """ import logging from typing import Any from fastapi import APIRouter, HTTPException, status from pydantic import BaseModel from packages.home_capabilities import load_layout logger = logging.getLogger(__name__) router = APIRouter(tags=["Rooms"]) @router.get("/rooms") async def get_rooms() -> list[dict[str, str]]: """Get list of all room IDs and names. Returns: List of dicts with room id and name """ layout = load_layout() return [ { "id": room.id, "name": room.name } for room in layout.rooms ] class LightsControlRequest(BaseModel): """Request model for controlling lights in a room.""" power: str # "on" or "off" brightness: int | None = None # Optional brightness 0-100 class HeatingControlRequest(BaseModel): """Request model for controlling heating in a room.""" target: float # Target temperature def get_room_devices(room_id: str) -> list[dict[str, Any]]: """Get all devices in a specific room from layout. Args: room_id: ID of the room Returns: List of device dicts with device_id, title, icon, rank, excluded Raises: HTTPException: If room not found """ layout = load_layout() for room in layout.rooms: if room.id == room_id: return [ { "device_id": device.device_id, "title": device.title, "icon": device.icon, "rank": device.rank, "excluded": device.excluded } for device in room.devices ] raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Room '{room_id}' not found" ) @router.post("/rooms/{room_id}/lights", status_code=status.HTTP_202_ACCEPTED) async def control_room_lights(room_id: str, request: LightsControlRequest) -> dict[str, Any]: """Control all lights (light and relay devices) in a room. Args: room_id: ID of the room request: Light control parameters Returns: dict with affected device_ids and command summary """ from apps.api.main import load_devices, publish_abstract_set # Get all devices in room room_devices = get_room_devices(room_id) # Filter out excluded devices room_device_ids = {d["device_id"] for d in room_devices if not d.get("excluded", False)} # Load all devices to filter by type all_devices = load_devices() # Filter for light/relay devices in this room light_devices = [ d for d in all_devices if d["device_id"] in room_device_ids and d["type"] in ("light", "relay") ] if not light_devices: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"No light devices found in room '{room_id}'" ) # Build payload payload = {"power": request.power} if request.brightness is not None and request.power == "on": payload["brightness"] = request.brightness # Send commands to all light devices affected_ids = [] errors = [] for device in light_devices: try: await publish_abstract_set( device_type=device["type"], device_id=device["device_id"], payload=payload ) affected_ids.append(device["device_id"]) logger.info(f"Sent command to {device['device_id']}: {payload}") except Exception as e: logger.error(f"Failed to control {device['device_id']}: {e}") errors.append({ "device_id": device["device_id"], "error": str(e) }) return { "room": room_id, "command": "lights", "payload": payload, "affected_devices": affected_ids, "success_count": len(affected_ids), "error_count": len(errors), "errors": errors if errors else None } @router.post("/rooms/{room_id}/heating", status_code=status.HTTP_202_ACCEPTED) async def control_room_heating(room_id: str, request: HeatingControlRequest) -> dict[str, Any]: """Control all thermostats in a room. Args: room_id: ID of the room request: Heating control parameters Returns: dict with affected device_ids and command summary """ from apps.api.main import load_devices, publish_abstract_set # Get all devices in room room_devices = get_room_devices(room_id) # Filter out excluded devices room_device_ids = {d["device_id"] for d in room_devices if not d.get("excluded", False)} # Load all devices to filter by type all_devices = load_devices() # Filter for thermostat devices in this room thermostat_devices = [ d for d in all_devices if d["device_id"] in room_device_ids and d["type"] == "thermostat" ] if not thermostat_devices: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"No thermostat devices found in room '{room_name}'" ) # Build payload payload = {"target": request.target} # Send commands to all thermostat devices affected_ids = [] errors = [] for device in thermostat_devices: try: await publish_abstract_set( device_type="thermostat", device_id=device["device_id"], payload=payload ) affected_ids.append(device["device_id"]) logger.info(f"Sent heating command to {device['device_id']}: {payload}") except Exception as e: logger.error(f"Failed to control {device['device_id']}: {e}") errors.append({ "device_id": device["device_id"], "error": str(e) }) return { "room": room_id, "command": "heating", "payload": payload, "affected_devices": affected_ids, "success_count": len(affected_ids), "error_count": len(errors), "errors": errors if errors else None }