2025-12-11 18:32:55 -07:00
|
|
|
"""
|
|
|
|
|
Admin API endpoints for WROLPi Controller.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
|
|
|
|
|
|
from controller.api.schemas import (
|
2026-03-28 10:09:58 -06:00
|
|
|
BluetoothActionResponse,
|
|
|
|
|
BluetoothStatusResponse,
|
2026-07-29 14:57:28 -06:00
|
|
|
DesktopActionResponse,
|
|
|
|
|
DesktopStatusResponse,
|
2025-12-11 18:32:55 -07:00
|
|
|
HotspotActionResponse,
|
2026-07-04 08:22:24 -06:00
|
|
|
HotspotDevicesResponse,
|
2026-07-28 19:52:39 -06:00
|
|
|
HotspotProtocolsResponse,
|
2026-07-04 08:22:24 -06:00
|
|
|
HotspotSettingsRequest,
|
|
|
|
|
HotspotSettingsResponse,
|
2025-12-11 18:32:55 -07:00
|
|
|
HotspotStatusResponse,
|
|
|
|
|
RestartServicesResponse,
|
2026-03-28 16:28:47 -06:00
|
|
|
SambaShareAddRequest,
|
|
|
|
|
SambaShareRemoveResponse,
|
|
|
|
|
SambaShareResponse,
|
|
|
|
|
SambaStatusResponse,
|
2025-12-11 18:32:55 -07:00
|
|
|
ServiceRestartResult,
|
|
|
|
|
SystemActionResponse,
|
|
|
|
|
ThrottleActionResponse,
|
|
|
|
|
ThrottleStatusResponse,
|
2026-03-13 15:56:21 -06:00
|
|
|
TimezoneSetRequest,
|
|
|
|
|
TimezoneSetResponse,
|
|
|
|
|
TimezoneStatusResponse,
|
controller: VNC toggle, gated on the Desktop running
Add a VNC toggle to the Controller (both UIs) that starts/stops whichever VNC
backend is installed: wayvnc for Wayland (Raspberry Pi OS), RealVNC for X11.
Runtime-only, like the Desktop toggle: the unit is never enabled, so VNC does
not return after a reboot. Users who want it permanently on can enable it with
raspi-config.
VNC serves the desktop session, so starting it without a desktop gives clients
a blank screen. The gate lives in the backend, not just the UI, so curl callers
get it too (409, distinct from a systemctl failure). Stopping is never gated so
a running VNC server can't be stranded on with no way to turn it off.
Backend selection deliberately avoids raspi-config's live-compositor probe
(pgrep labwc), which reports X11 — and would pick the wrong backend — whenever
the desktop is stopped. wayvnc also requires its config to exist; that is
checked directly rather than via the unit's ConditionResult, which reports 'no'
for any unit that has never been started.
Extracts _unit_properties()/_systemctl_unit() so Desktop and VNC share one
systemd probe and start/stop path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 16:53:45 -06:00
|
|
|
VncActionResponse,
|
|
|
|
|
VncStatusResponse,
|
2025-12-11 18:32:55 -07:00
|
|
|
)
|
|
|
|
|
from controller.lib.admin import (
|
2026-07-29 14:57:28 -06:00
|
|
|
block_bluetooth,
|
2025-12-11 18:32:55 -07:00
|
|
|
disable_throttle,
|
|
|
|
|
enable_throttle,
|
2026-03-28 10:09:58 -06:00
|
|
|
get_bluetooth_status_dict,
|
2026-07-29 14:57:28 -06:00
|
|
|
get_desktop_status_dict,
|
controller: VNC toggle, gated on the Desktop running
Add a VNC toggle to the Controller (both UIs) that starts/stops whichever VNC
backend is installed: wayvnc for Wayland (Raspberry Pi OS), RealVNC for X11.
Runtime-only, like the Desktop toggle: the unit is never enabled, so VNC does
not return after a reboot. Users who want it permanently on can enable it with
raspi-config.
VNC serves the desktop session, so starting it without a desktop gives clients
a blank screen. The gate lives in the backend, not just the UI, so curl callers
get it too (409, distinct from a systemctl failure). Stopping is never gated so
a running VNC server can't be stranded on with no way to turn it off.
Backend selection deliberately avoids raspi-config's live-compositor probe
(pgrep labwc), which reports X11 — and would pick the wrong backend — whenever
the desktop is stopped. wayvnc also requires its config to exist; that is
checked directly rather than via the unit's ConditionResult, which reports 'no'
for any unit that has never been started.
Extracts _unit_properties()/_systemctl_unit() so Desktop and VNC share one
systemd probe and start/stop path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 16:53:45 -06:00
|
|
|
get_vnc_status_dict,
|
2026-07-28 19:52:39 -06:00
|
|
|
get_device_hotspot_protocols,
|
2026-07-04 08:22:24 -06:00
|
|
|
get_hotspot_device,
|
|
|
|
|
get_hotspot_password,
|
2026-07-28 19:52:39 -06:00
|
|
|
get_hotspot_protocol,
|
2026-07-04 08:22:24 -06:00
|
|
|
get_hotspot_ssid,
|
2025-12-11 18:32:55 -07:00
|
|
|
get_hotspot_status_dict,
|
|
|
|
|
get_throttle_status_dict,
|
2026-03-13 15:56:21 -06:00
|
|
|
get_timezone_status_dict,
|
2026-07-04 08:22:24 -06:00
|
|
|
get_wifi_devices,
|
2025-12-11 18:32:55 -07:00
|
|
|
reboot_system,
|
|
|
|
|
restart_all_services,
|
2026-03-13 15:56:21 -06:00
|
|
|
set_timezone,
|
2025-12-11 18:32:55 -07:00
|
|
|
shutdown_system,
|
2026-07-29 14:57:28 -06:00
|
|
|
start_desktop,
|
|
|
|
|
start_hotspot,
|
controller: VNC toggle, gated on the Desktop running
Add a VNC toggle to the Controller (both UIs) that starts/stops whichever VNC
backend is installed: wayvnc for Wayland (Raspberry Pi OS), RealVNC for X11.
Runtime-only, like the Desktop toggle: the unit is never enabled, so VNC does
not return after a reboot. Users who want it permanently on can enable it with
raspi-config.
VNC serves the desktop session, so starting it without a desktop gives clients
a blank screen. The gate lives in the backend, not just the UI, so curl callers
get it too (409, distinct from a systemctl failure). Stopping is never gated so
a running VNC server can't be stranded on with no way to turn it off.
Backend selection deliberately avoids raspi-config's live-compositor probe
(pgrep labwc), which reports X11 — and would pick the wrong backend — whenever
the desktop is stopped. wayvnc also requires its config to exist; that is
checked directly rather than via the unit's ConditionResult, which reports 'no'
for any unit that has never been started.
Extracts _unit_properties()/_systemctl_unit() so Desktop and VNC share one
systemd probe and start/stop path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 16:53:45 -06:00
|
|
|
start_vnc,
|
2026-07-29 14:57:28 -06:00
|
|
|
stop_desktop,
|
|
|
|
|
stop_hotspot,
|
controller: VNC toggle, gated on the Desktop running
Add a VNC toggle to the Controller (both UIs) that starts/stops whichever VNC
backend is installed: wayvnc for Wayland (Raspberry Pi OS), RealVNC for X11.
Runtime-only, like the Desktop toggle: the unit is never enabled, so VNC does
not return after a reboot. Users who want it permanently on can enable it with
raspi-config.
VNC serves the desktop session, so starting it without a desktop gives clients
a blank screen. The gate lives in the backend, not just the UI, so curl callers
get it too (409, distinct from a systemctl failure). Stopping is never gated so
a running VNC server can't be stranded on with no way to turn it off.
Backend selection deliberately avoids raspi-config's live-compositor probe
(pgrep labwc), which reports X11 — and would pick the wrong backend — whenever
the desktop is stopped. wayvnc also requires its config to exist; that is
checked directly rather than via the unit's ConditionResult, which reports 'no'
for any unit that has never been started.
Extracts _unit_properties()/_systemctl_unit() so Desktop and VNC share one
systemd probe and start/stop path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 16:53:45 -06:00
|
|
|
stop_vnc,
|
2026-07-29 14:57:28 -06:00
|
|
|
unblock_bluetooth,
|
2026-07-04 08:22:24 -06:00
|
|
|
update_hotspot_settings,
|
2025-12-11 18:32:55 -07:00
|
|
|
)
|
|
|
|
|
from controller.lib.config import is_docker_mode
|
2026-03-28 16:28:47 -06:00
|
|
|
from controller.lib.samba import (
|
|
|
|
|
add_share,
|
|
|
|
|
get_samba_status_dict,
|
|
|
|
|
remove_share,
|
|
|
|
|
)
|
2025-12-11 18:32:55 -07:00
|
|
|
|
|
|
|
|
router = APIRouter(tags=["admin"])
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 18:21:03 -06:00
|
|
|
def _raise_for_action(result: dict):
|
|
|
|
|
"""
|
|
|
|
|
Turn a failed subsystem action into an HTTPException.
|
|
|
|
|
|
|
|
|
|
Actions all report `{"success": bool, "error": str}`. A precondition failure
|
|
|
|
|
(VNC needs a running desktop) is a 409 so callers can tell it from a command
|
|
|
|
|
that was attempted and failed.
|
|
|
|
|
"""
|
|
|
|
|
if result.get("success"):
|
|
|
|
|
return
|
|
|
|
|
status_code = 409 if result.get("precondition_failed") else 500
|
|
|
|
|
raise HTTPException(status_code=status_code, detail=result.get("error", "Failed"))
|
|
|
|
|
|
|
|
|
|
|
2025-12-11 18:32:55 -07:00
|
|
|
# --- Hotspot ---
|
|
|
|
|
|
|
|
|
|
@router.get("/api/hotspot/status", response_model=HotspotStatusResponse)
|
|
|
|
|
async def hotspot_status() -> HotspotStatusResponse:
|
|
|
|
|
"""Get WiFi hotspot status."""
|
|
|
|
|
return HotspotStatusResponse(**get_hotspot_status_dict())
|
|
|
|
|
|
|
|
|
|
|
2026-07-04 08:22:24 -06:00
|
|
|
@router.get("/api/hotspot/devices", response_model=HotspotDevicesResponse)
|
|
|
|
|
async def hotspot_devices() -> HotspotDevicesResponse:
|
|
|
|
|
"""List WiFi devices which could host the hotspot."""
|
|
|
|
|
return HotspotDevicesResponse(devices=get_wifi_devices())
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 19:52:39 -06:00
|
|
|
@router.get("/api/hotspot/protocols", response_model=HotspotProtocolsResponse)
|
|
|
|
|
async def hotspot_protocols(device: str = None) -> HotspotProtocolsResponse:
|
|
|
|
|
"""List the hotspot protocols a WiFi device can host; defaults to the configured device."""
|
|
|
|
|
device = device or get_hotspot_device()
|
|
|
|
|
return HotspotProtocolsResponse(device=device, protocols=get_device_hotspot_protocols(device))
|
|
|
|
|
|
|
|
|
|
|
2026-07-04 08:22:24 -06:00
|
|
|
@router.get("/api/hotspot/settings", response_model=HotspotSettingsResponse)
|
|
|
|
|
async def hotspot_settings() -> HotspotSettingsResponse:
|
|
|
|
|
"""Get WiFi hotspot settings."""
|
|
|
|
|
return HotspotSettingsResponse(
|
|
|
|
|
device=get_hotspot_device(),
|
|
|
|
|
ssid=get_hotspot_ssid(),
|
|
|
|
|
password=get_hotspot_password(),
|
2026-07-28 19:52:39 -06:00
|
|
|
protocol=get_hotspot_protocol(),
|
2026-07-04 08:22:24 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/hotspot/settings", response_model=HotspotSettingsResponse)
|
|
|
|
|
async def hotspot_settings_update(request: HotspotSettingsRequest) -> HotspotSettingsResponse:
|
|
|
|
|
"""Update WiFi hotspot settings; they are saved in controller.yaml."""
|
2026-07-04 08:56:12 -06:00
|
|
|
# 500 matches the other subsystem endpoints (hotspot/bluetooth/throttle) in Docker mode.
|
|
|
|
|
if is_docker_mode():
|
|
|
|
|
raise HTTPException(status_code=500, detail="Not available in Docker mode")
|
2026-07-28 19:52:39 -06:00
|
|
|
result = update_hotspot_settings(device=request.device, ssid=request.ssid, password=request.password,
|
|
|
|
|
protocol=request.protocol)
|
2026-07-04 08:22:24 -06:00
|
|
|
if not result.get("success"):
|
2026-07-04 08:56:12 -06:00
|
|
|
raise HTTPException(status_code=400, detail=result.get("error", "Failed"))
|
2026-07-28 19:52:39 -06:00
|
|
|
return HotspotSettingsResponse(device=result["device"], ssid=result["ssid"], password=result["password"],
|
|
|
|
|
protocol=result["protocol"])
|
2026-07-04 08:22:24 -06:00
|
|
|
|
|
|
|
|
|
2026-07-29 14:57:28 -06:00
|
|
|
@router.post("/api/hotspot/start", response_model=HotspotActionResponse)
|
|
|
|
|
async def hotspot_start() -> HotspotActionResponse:
|
|
|
|
|
"""Start the WiFi hotspot."""
|
|
|
|
|
result = start_hotspot()
|
2026-07-29 18:21:03 -06:00
|
|
|
_raise_for_action(result)
|
2025-12-11 18:32:55 -07:00
|
|
|
return HotspotActionResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 14:57:28 -06:00
|
|
|
@router.post("/api/hotspot/stop", response_model=HotspotActionResponse)
|
|
|
|
|
async def hotspot_stop() -> HotspotActionResponse:
|
|
|
|
|
"""Stop the WiFi hotspot by turning the WiFi radio off."""
|
|
|
|
|
result = stop_hotspot()
|
2026-07-29 18:21:03 -06:00
|
|
|
_raise_for_action(result)
|
2025-12-11 18:32:55 -07:00
|
|
|
return HotspotActionResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
2026-03-28 10:09:58 -06:00
|
|
|
# --- Bluetooth ---
|
|
|
|
|
|
|
|
|
|
@router.get("/api/bluetooth/status", response_model=BluetoothStatusResponse)
|
|
|
|
|
async def bluetooth_status() -> BluetoothStatusResponse:
|
|
|
|
|
"""Get Bluetooth radio status."""
|
|
|
|
|
return BluetoothStatusResponse(**await get_bluetooth_status_dict())
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 14:57:28 -06:00
|
|
|
@router.post("/api/bluetooth/unblock", response_model=BluetoothActionResponse)
|
|
|
|
|
async def bluetooth_unblock() -> BluetoothActionResponse:
|
|
|
|
|
"""Unblock the Bluetooth radio (turn it on)."""
|
|
|
|
|
result = await unblock_bluetooth()
|
2026-07-29 18:21:03 -06:00
|
|
|
_raise_for_action(result)
|
2026-03-28 10:09:58 -06:00
|
|
|
return BluetoothActionResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 14:57:28 -06:00
|
|
|
@router.post("/api/bluetooth/block", response_model=BluetoothActionResponse)
|
|
|
|
|
async def bluetooth_block() -> BluetoothActionResponse:
|
|
|
|
|
"""Block the Bluetooth radio (turn it off)."""
|
|
|
|
|
result = await block_bluetooth()
|
2026-07-29 18:21:03 -06:00
|
|
|
_raise_for_action(result)
|
2026-03-28 10:09:58 -06:00
|
|
|
return BluetoothActionResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 14:57:28 -06:00
|
|
|
# --- Desktop ---
|
|
|
|
|
|
|
|
|
|
@router.get("/api/desktop/status", response_model=DesktopStatusResponse)
|
|
|
|
|
async def desktop_status() -> DesktopStatusResponse:
|
|
|
|
|
"""Get graphical desktop (display manager) status."""
|
|
|
|
|
return DesktopStatusResponse(**await get_desktop_status_dict())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/desktop/start", response_model=DesktopActionResponse)
|
|
|
|
|
async def desktop_start() -> DesktopActionResponse:
|
|
|
|
|
"""Start the graphical desktop until stopped or reboot."""
|
|
|
|
|
result = await start_desktop()
|
2026-07-29 18:21:03 -06:00
|
|
|
_raise_for_action(result)
|
2026-07-29 14:57:28 -06:00
|
|
|
return DesktopActionResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/desktop/stop", response_model=DesktopActionResponse)
|
|
|
|
|
async def desktop_stop() -> DesktopActionResponse:
|
|
|
|
|
"""Stop the graphical desktop; it returns on the next boot (fail open)."""
|
|
|
|
|
result = await stop_desktop()
|
2026-07-29 18:21:03 -06:00
|
|
|
_raise_for_action(result)
|
2026-07-29 14:57:28 -06:00
|
|
|
return DesktopActionResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
controller: VNC toggle, gated on the Desktop running
Add a VNC toggle to the Controller (both UIs) that starts/stops whichever VNC
backend is installed: wayvnc for Wayland (Raspberry Pi OS), RealVNC for X11.
Runtime-only, like the Desktop toggle: the unit is never enabled, so VNC does
not return after a reboot. Users who want it permanently on can enable it with
raspi-config.
VNC serves the desktop session, so starting it without a desktop gives clients
a blank screen. The gate lives in the backend, not just the UI, so curl callers
get it too (409, distinct from a systemctl failure). Stopping is never gated so
a running VNC server can't be stranded on with no way to turn it off.
Backend selection deliberately avoids raspi-config's live-compositor probe
(pgrep labwc), which reports X11 — and would pick the wrong backend — whenever
the desktop is stopped. wayvnc also requires its config to exist; that is
checked directly rather than via the unit's ConditionResult, which reports 'no'
for any unit that has never been started.
Extracts _unit_properties()/_systemctl_unit() so Desktop and VNC share one
systemd probe and start/stop path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 16:53:45 -06:00
|
|
|
# --- VNC ---
|
|
|
|
|
|
|
|
|
|
@router.get("/api/vnc/status", response_model=VncStatusResponse)
|
|
|
|
|
async def vnc_status() -> VncStatusResponse:
|
|
|
|
|
"""Get VNC server status."""
|
|
|
|
|
return VncStatusResponse(**await get_vnc_status_dict())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/vnc/start", response_model=VncActionResponse)
|
|
|
|
|
async def vnc_start() -> VncActionResponse:
|
|
|
|
|
"""Start the VNC server; requires a running desktop."""
|
|
|
|
|
result = await start_vnc()
|
2026-07-29 18:21:03 -06:00
|
|
|
_raise_for_action(result)
|
controller: VNC toggle, gated on the Desktop running
Add a VNC toggle to the Controller (both UIs) that starts/stops whichever VNC
backend is installed: wayvnc for Wayland (Raspberry Pi OS), RealVNC for X11.
Runtime-only, like the Desktop toggle: the unit is never enabled, so VNC does
not return after a reboot. Users who want it permanently on can enable it with
raspi-config.
VNC serves the desktop session, so starting it without a desktop gives clients
a blank screen. The gate lives in the backend, not just the UI, so curl callers
get it too (409, distinct from a systemctl failure). Stopping is never gated so
a running VNC server can't be stranded on with no way to turn it off.
Backend selection deliberately avoids raspi-config's live-compositor probe
(pgrep labwc), which reports X11 — and would pick the wrong backend — whenever
the desktop is stopped. wayvnc also requires its config to exist; that is
checked directly rather than via the unit's ConditionResult, which reports 'no'
for any unit that has never been started.
Extracts _unit_properties()/_systemctl_unit() so Desktop and VNC share one
systemd probe and start/stop path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 16:53:45 -06:00
|
|
|
return VncActionResponse(success=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/vnc/stop", response_model=VncActionResponse)
|
|
|
|
|
async def vnc_stop() -> VncActionResponse:
|
|
|
|
|
"""Stop the VNC server; allowed even when the desktop is stopped."""
|
|
|
|
|
result = await stop_vnc()
|
2026-07-29 18:21:03 -06:00
|
|
|
_raise_for_action(result)
|
controller: VNC toggle, gated on the Desktop running
Add a VNC toggle to the Controller (both UIs) that starts/stops whichever VNC
backend is installed: wayvnc for Wayland (Raspberry Pi OS), RealVNC for X11.
Runtime-only, like the Desktop toggle: the unit is never enabled, so VNC does
not return after a reboot. Users who want it permanently on can enable it with
raspi-config.
VNC serves the desktop session, so starting it without a desktop gives clients
a blank screen. The gate lives in the backend, not just the UI, so curl callers
get it too (409, distinct from a systemctl failure). Stopping is never gated so
a running VNC server can't be stranded on with no way to turn it off.
Backend selection deliberately avoids raspi-config's live-compositor probe
(pgrep labwc), which reports X11 — and would pick the wrong backend — whenever
the desktop is stopped. wayvnc also requires its config to exist; that is
checked directly rather than via the unit's ConditionResult, which reports 'no'
for any unit that has never been started.
Extracts _unit_properties()/_systemctl_unit() so Desktop and VNC share one
systemd probe and start/stop path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 16:53:45 -06:00
|
|
|
return VncActionResponse(success=True)
|
|
|
|
|
|
|
|
|
|
|
2026-03-28 16:28:47 -06:00
|
|
|
# --- Samba ---
|
|
|
|
|
|
|
|
|
|
@router.get("/api/samba/status", response_model=SambaStatusResponse)
|
|
|
|
|
async def samba_status() -> SambaStatusResponse:
|
|
|
|
|
"""Get Samba sharing status."""
|
|
|
|
|
return SambaStatusResponse(**get_samba_status_dict())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/samba/shares", response_model=SambaShareResponse)
|
|
|
|
|
async def samba_add_share(request: SambaShareAddRequest) -> SambaShareResponse:
|
|
|
|
|
"""Add a Samba share."""
|
|
|
|
|
result = add_share(request.name, request.path, request.read_only, request.comment or "")
|
|
|
|
|
if not result.get("success"):
|
|
|
|
|
raise HTTPException(status_code=400, detail=result.get("error", "Failed"))
|
|
|
|
|
return SambaShareResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/api/samba/shares/{share_name}", response_model=SambaShareRemoveResponse)
|
|
|
|
|
async def samba_remove_share(share_name: str) -> SambaShareRemoveResponse:
|
|
|
|
|
"""Remove a Samba share."""
|
|
|
|
|
result = remove_share(share_name)
|
|
|
|
|
if not result.get("success"):
|
|
|
|
|
raise HTTPException(status_code=404, detail=result.get("error", "Failed"))
|
|
|
|
|
return SambaShareRemoveResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
2025-12-11 18:32:55 -07:00
|
|
|
# --- Throttle ---
|
|
|
|
|
|
|
|
|
|
@router.get("/api/throttle/status", response_model=ThrottleStatusResponse)
|
|
|
|
|
async def throttle_status() -> ThrottleStatusResponse:
|
|
|
|
|
"""Get CPU throttle status."""
|
|
|
|
|
return ThrottleStatusResponse(**get_throttle_status_dict())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/throttle/enable", response_model=ThrottleActionResponse)
|
|
|
|
|
async def throttle_enable() -> ThrottleActionResponse:
|
|
|
|
|
"""Enable CPU throttle (powersave mode)."""
|
|
|
|
|
result = enable_throttle()
|
2026-07-29 18:21:03 -06:00
|
|
|
_raise_for_action(result)
|
2025-12-11 18:32:55 -07:00
|
|
|
return ThrottleActionResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/throttle/disable", response_model=ThrottleActionResponse)
|
|
|
|
|
async def throttle_disable() -> ThrottleActionResponse:
|
|
|
|
|
"""Disable CPU throttle (normal performance)."""
|
|
|
|
|
result = disable_throttle()
|
2026-07-29 18:21:03 -06:00
|
|
|
_raise_for_action(result)
|
2025-12-11 18:32:55 -07:00
|
|
|
return ThrottleActionResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
2026-03-13 15:56:21 -06:00
|
|
|
# --- Timezone ---
|
|
|
|
|
|
|
|
|
|
@router.get("/api/timezone/status", response_model=TimezoneStatusResponse)
|
|
|
|
|
async def timezone_status() -> TimezoneStatusResponse:
|
|
|
|
|
"""Get current system timezone."""
|
|
|
|
|
return TimezoneStatusResponse(**get_timezone_status_dict())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/timezone/set", response_model=TimezoneSetResponse)
|
|
|
|
|
async def timezone_set(request: TimezoneSetRequest) -> TimezoneSetResponse:
|
|
|
|
|
"""Set the system timezone."""
|
|
|
|
|
result = set_timezone(request.timezone)
|
2026-07-29 18:21:03 -06:00
|
|
|
_raise_for_action(result)
|
2026-03-13 15:56:21 -06:00
|
|
|
return TimezoneSetResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
2025-12-11 18:32:55 -07:00
|
|
|
# --- System Control ---
|
|
|
|
|
|
|
|
|
|
@router.post("/api/shutdown", response_model=SystemActionResponse)
|
|
|
|
|
async def system_shutdown() -> SystemActionResponse:
|
|
|
|
|
"""Shut down the system."""
|
|
|
|
|
if is_docker_mode():
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=501,
|
|
|
|
|
detail="Cannot shutdown from Docker container"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result = shutdown_system()
|
2026-07-29 18:21:03 -06:00
|
|
|
_raise_for_action(result)
|
2025-12-11 18:32:55 -07:00
|
|
|
return SystemActionResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/reboot", response_model=SystemActionResponse)
|
|
|
|
|
async def system_reboot() -> SystemActionResponse:
|
|
|
|
|
"""Reboot the system."""
|
|
|
|
|
if is_docker_mode():
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=501,
|
|
|
|
|
detail="Cannot reboot from Docker container"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result = reboot_system()
|
2026-07-29 18:21:03 -06:00
|
|
|
_raise_for_action(result)
|
2025-12-11 18:32:55 -07:00
|
|
|
return SystemActionResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/api/restart", response_model=RestartServicesResponse)
|
|
|
|
|
async def services_restart() -> RestartServicesResponse:
|
|
|
|
|
"""Restart all WROLPi services including Controller."""
|
|
|
|
|
if is_docker_mode():
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=501,
|
|
|
|
|
detail="Service restart via systemctl not available in Docker mode"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result = await restart_all_services()
|
|
|
|
|
# Convert nested dicts to Pydantic models
|
|
|
|
|
services = {
|
|
|
|
|
name: ServiceRestartResult(**status)
|
|
|
|
|
for name, status in result.get("services", {}).items()
|
|
|
|
|
}
|
|
|
|
|
return RestartServicesResponse(
|
|
|
|
|
success=result.get("success", False),
|
|
|
|
|
services=services,
|
|
|
|
|
error=result.get("error"),
|
|
|
|
|
)
|