Compare commits

..

3 Commits

Author SHA1 Message Date
d39bcfce26 excluded 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 17:38:46 +01:00
1fd275186a 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
2025-12-09 17:28:32 +01:00
da370c9050 room id
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/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-09 17:13:51 +01:00
5 changed files with 263 additions and 15 deletions

View File

@@ -8,9 +8,9 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
MQTT_BROKER=172.16.2.16 \ MQTT_BROKER=172.16.2.16 \
MQTT_PORT=1883 \ MQTT_PORT=1883 \
REDIS_HOST=localhost \ REDIS_HOST=172.23.1.116 \
REDIS_PORT=6379 \ REDIS_PORT=6379 \
REDIS_DB=0 \ REDIS_DB=8 \
REDIS_CHANNEL=ui:updates REDIS_CHANNEL=ui:updates
# Create non-root user # Create non-root user

View File

@@ -121,7 +121,10 @@ async def get_device_layout(device_id: str):
async def startup_event(): async def startup_event():
"""Include routers after app is initialized to avoid circular imports.""" """Include routers after app is initialized to avoid circular imports."""
from apps.api.routes.groups_scenes import router as groups_scenes_router from apps.api.routes.groups_scenes import router as groups_scenes_router
from apps.api.routes.rooms import router as rooms_router
app.include_router(groups_scenes_router, prefix="") app.include_router(groups_scenes_router, prefix="")
app.include_router(rooms_router, prefix="")
@app.get("/health") @app.get("/health")

219
apps/api/routes/rooms.py Normal file
View 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_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
}

View File

@@ -1,5 +1,6 @@
rooms: rooms:
- name: Schlafzimmer - id: schlafzimmer
name: Schlafzimmer
devices: devices:
- device_id: bettlicht_patty - device_id: bettlicht_patty
title: Bettlicht Patty title: Bettlicht Patty
@@ -33,7 +34,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 47 rank: 47
- name: Esszimmer - id: esszimmer
name: Esszimmer
devices: devices:
- device_id: deckenlampe_esszimmer - device_id: deckenlampe_esszimmer
title: Deckenlampe Esszimmer title: Deckenlampe Esszimmer
@@ -79,7 +81,8 @@ rooms:
title: Kontakt Straße links title: Kontakt Straße links
icon: 🪟 icon: 🪟
rank: 97 rank: 97
- name: Wohnzimmer - id: wohnzimmer
name: Wohnzimmer
devices: devices:
- device_id: lampe_naehtischchen_wohnzimmer - device_id: lampe_naehtischchen_wohnzimmer
title: Lampe Naehtischchen Wohnzimmer title: Lampe Naehtischchen Wohnzimmer
@@ -121,7 +124,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 138 rank: 138
- name: che - id: kueche
name: Küche
devices: devices:
- device_id: kueche_deckenlampe - device_id: kueche_deckenlampe
title: Küche Deckenlampe title: Küche Deckenlampe
@@ -135,6 +139,7 @@ rooms:
title: Küche Putzlicht title: Küche Putzlicht
icon: 💡 icon: 💡
rank: 143 rank: 143
excluded: true
- device_id: kueche_fensterbank_licht - device_id: kueche_fensterbank_licht
title: Küche Fensterbank title: Küche Fensterbank
icon: 💡 icon: 💡
@@ -163,7 +168,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 155 rank: 155
- name: Arbeitszimmer Patty - id: arbeitszimmer_patty
name: Arbeitszimmer Patty
devices: devices:
- device_id: leselampe_patty - device_id: leselampe_patty
title: Leselampe Patty title: Leselampe Patty
@@ -205,7 +211,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 189 rank: 189
- name: Arbeitszimmer Wolfgang - id: arbeitszimmer_wolfgang
name: Arbeitszimmer Wolfgang
devices: devices:
- device_id: thermostat_wolfgang - device_id: thermostat_wolfgang
title: Wolfgang title: Wolfgang
@@ -223,7 +230,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 202 rank: 202
- name: Flur - id: flur
name: Flur
devices: devices:
- device_id: deckenlampe_flur_oben - device_id: deckenlampe_flur_oben
title: Deckenlampe Flur oben title: Deckenlampe Flur oben
@@ -249,7 +257,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 235 rank: 235
- name: Sportzimmer - id: sportzimmer
name: Sportzimmer
devices: devices:
- device_id: sportlicht_regal - device_id: sportlicht_regal
title: Sportlicht Regal title: Sportlicht Regal
@@ -267,7 +276,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 265 rank: 265
- name: Bad Oben - id: bad_oben
name: Bad Oben
devices: devices:
- device_id: thermostat_bad_oben - device_id: thermostat_bad_oben
title: Thermostat Bad Oben title: Thermostat Bad Oben
@@ -281,7 +291,8 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 272 rank: 272
- name: Bad Unten - id: bad_unten
name: Bad Unten
devices: devices:
- device_id: thermostat_bad_unten - device_id: thermostat_bad_unten
title: Thermostat Bad Unten title: Thermostat Bad Unten
@@ -295,13 +306,15 @@ rooms:
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 282 rank: 282
- name: Waschküche - id: waschkueche
name: Waschküche
devices: devices:
- device_id: sensor_waschkueche - device_id: sensor_waschkueche
title: Temperatur & Luftfeuchte title: Temperatur & Luftfeuchte
icon: 🌡️ icon: 🌡️
rank: 290 rank: 290
- name: Outdoor - id: outdoor
name: Outdoor
devices: devices:
- device_id: licht_terasse - device_id: licht_terasse
title: Licht Terasse title: Licht Terasse
@@ -311,7 +324,8 @@ rooms:
title: Gartenlicht vorne title: Gartenlicht vorne
icon: 💡 icon: 💡
rank: 291 rank: 291
- name: Garage - id: garage
name: Garage
devices: devices:
- device_id: power_relay_caroutlet - device_id: power_relay_caroutlet
title: Ladestrom title: Ladestrom

View File

@@ -18,6 +18,7 @@ class DeviceTile(BaseModel):
title: Display title for the device title: Display title for the device
icon: Icon name or emoji for the device icon: Icon name or emoji for the device
rank: Sort order within the room (lower = first) rank: Sort order within the room (lower = first)
excluded: Optional flag to exclude device from certain operations
""" """
device_id: str = Field( device_id: str = Field(
@@ -40,16 +41,27 @@ class DeviceTile(BaseModel):
ge=0, ge=0,
description="Sort order (lower values appear first)" description="Sort order (lower values appear first)"
) )
excluded: bool = Field(
default=False,
description="Exclude device from bulk operations"
)
class Room(BaseModel): class Room(BaseModel):
"""Represents a room containing devices. """Represents a room containing devices.
Attributes: Attributes:
id: Unique room identifier (used for API endpoints)
name: Room name (e.g., "Wohnzimmer", "Küche") name: Room name (e.g., "Wohnzimmer", "Küche")
devices: List of device tiles in this room devices: List of device tiles in this room
""" """
id: str = Field(
...,
description="Unique room identifier"
)
name: str = Field( name: str = Field(
..., ...,
description="Room name" description="Room name"