excluded
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
This commit is contained in:
219
apps/api/routes/rooms.py
Normal file
219
apps/api/routes/rooms.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
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_name: str) -> list[dict[str, Any]]:
|
||||
"""Get all devices in a specific room from layout.
|
||||
|
||||
Args:
|
||||
room_name: Name 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.name == room_name:
|
||||
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_name}' not found"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/rooms/{room_name}/lights", status_code=status.HTTP_202_ACCEPTED)
|
||||
async def control_room_lights(room_name: str, request: LightsControlRequest) -> dict[str, Any]:
|
||||
"""Control all lights (light and relay devices) in a room.
|
||||
|
||||
Args:
|
||||
room_name: Name 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_name)
|
||||
|
||||
# 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_name}'"
|
||||
)
|
||||
|
||||
# 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_name,
|
||||
"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_name}/heating", status_code=status.HTTP_202_ACCEPTED)
|
||||
async def control_room_heating(room_name: str, request: HeatingControlRequest) -> dict[str, Any]:
|
||||
"""Control all thermostats in a room.
|
||||
|
||||
Args:
|
||||
room_name: Name 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_name)
|
||||
|
||||
# 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_name,
|
||||
"command": "heating",
|
||||
"payload": payload,
|
||||
"affected_devices": affected_ids,
|
||||
"success_count": len(affected_ids),
|
||||
"error_count": len(errors),
|
||||
"errors": errors if errors else None
|
||||
}
|
||||
@@ -139,6 +139,7 @@ rooms:
|
||||
title: Küche Putzlicht
|
||||
icon: 💡
|
||||
rank: 143
|
||||
excluded: true
|
||||
- device_id: kueche_fensterbank_licht
|
||||
title: Küche Fensterbank
|
||||
icon: 💡
|
||||
|
||||
@@ -18,6 +18,7 @@ class DeviceTile(BaseModel):
|
||||
title: Display title for the device
|
||||
icon: Icon name or emoji for the device
|
||||
rank: Sort order within the room (lower = first)
|
||||
excluded: Optional flag to exclude device from certain operations
|
||||
"""
|
||||
|
||||
device_id: str = Field(
|
||||
@@ -40,6 +41,11 @@ class DeviceTile(BaseModel):
|
||||
ge=0,
|
||||
description="Sort order (lower values appear first)"
|
||||
)
|
||||
|
||||
excluded: bool = Field(
|
||||
default=False,
|
||||
description="Exclude device from bulk operations"
|
||||
)
|
||||
|
||||
|
||||
class Room(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user