"""
MCP 服务器

查询工具（3 个）：
0. get_devices - 列出所有设备
1. get_status - 设备全量状态（合并 10 个 trait 查询）
2. get_map_content - 地图内容（独立重量级请求）

控制工具（10 个）：
3. start_clean - 开始清洁（全屋或指定房间）
4. stop_clean - 停止清洁
5. pause_clean - 暂停清洁
6. resume_clean - 恢复清洁
7. go_home - 回充电座
8. find_robot - 发出声音定位机器人
9. dock_action - 基座操作（集尘/洗拖布）
10. set_volume - 调节音量
11. get_routines - 列出智能场景
12. execute_routine - 执行智能场景
"""

import asyncio
import functools
import logging
from enum import Enum
from typing import Any

from mcp.server.fastmcp import FastMCP
from roborock import RoborockCommand
from roborock.exceptions import RoborockException

from .connection import (
    AmbiguousDeviceError,
    AuthExpiredError,
    ConnectionManager,
    DeviceNotFoundError,
    NoDeviceError,
    lifespan,
)

logger = logging.getLogger(__name__)


def _handle_errors(func):
    """包装工具函数，将已知异常转为结构化错误响应"""
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        try:
            return await func(*args, **kwargs)
        except (AuthExpiredError, NoDeviceError, AmbiguousDeviceError,
                DeviceNotFoundError, ValueError) as e:
            return {"error": str(e)}
        except RoborockException as e:
            logger.warning("工具 %s 设备通信失败: %s", func.__name__, e)
            return {"error": f"设备通信失败: {type(e).__name__}: {e}"}
        except Exception as e:
            logger.exception("工具 %s 执行失败", func.__name__)
            return {"error": f"内部错误: {type(e).__name__}: {e}"}
    return wrapper

# 创建 FastMCP 实例
mcp = FastMCP(
    "roborock-mcp",
    lifespan=lifespan,
)


def get_conn() -> ConnectionManager:
    """从 MCP context 获取 ConnectionManager"""
    return mcp.get_context().request_context.lifespan_context["conn"]


async def _safe_refresh(trait, name: str) -> bool:
    """安全刷新单个 trait，失败不影响其他"""
    try:
        await trait.refresh()
        return True
    except Exception as e:
        logger.warning("Trait %s refresh 失败: %s", name, e)
        return False


def _serialize_value(value: Any) -> Any:
    """将 Enum 等特殊类型转为可序列化值"""
    if value is None:
        return None
    if isinstance(value, Enum):
        return value.name
    if isinstance(value, list):
        return [_serialize_value(v) for v in value]
    return value


def _field(value: Any, description: str) -> dict[str, Any]:
    """包装字段值和描述，供 LLM 理解"""
    return {"value": value, "description": description}


# StatusV2 白名单：仅输出有意义的字段
# state/error_code 改在 computed properties 中输出（避免与 state_name/error_code_name 重复）
_STATUS_FIELDS: dict[str, str] = {
    # 核心状态
    "battery": "电池电量 (%)",
    "charge_status": "充电状态 (0=未充电, 1=充电中, 2=充电完成)",
    # 清洁进度
    "clean_percent": "清洁任务完成百分比 (%)",
    "in_cleaning": "清洁状态 (complete/global_clean_not_complete/zone_clean_not_complete/segment_clean_not_complete)",
    "in_returning": "是否在返回途中 (0=否, 1=是)",
    "avoid_count": "当前任务避障次数",
    "repeat": "当前任务重复遍数",
    # 地图
    "map_present": "是否有地图 (0=无, 1=有)",
    # 基座
    "dock_type": "基座类型 (no_dock/auto_empty_dock/empty_wash_fill_dock/p10_pro_dock 等)",
    "dock_error_status": "基座错误状态 (ok/duct_blockage/water_empty/waste_water_tank_full 等)",
    "dust_collection_status": "集尘状态 (0=未集尘, 1=集尘中)",
    "auto_dust_collection": "自动集尘开关 (0=关闭, 1=开启)",
    # 洗拖布/烘干
    "wash_status": "洗拖布状态 (0=空闲, 1=洗涤中)",
    "wash_phase": "洗拖布阶段 (0=空闲)",
    "wash_ready": "是否准备好洗拖布 (0=否, 1=是)",
    "dry_status": "热风烘干状态 (0=关闭, 1=烘干中)",
    # 水箱
    "water_box_status": "水箱安装状态 (0=未安装, 1=已安装)",
    "water_box_carriage_status": "水箱托架状态 (0=正常)",
    "water_shortage_status": "缺水状态 (0=正常)",
    # 其他
    "is_locating": "是否正在定位 (0=否, 1=是)",
    "lock_status": "童锁状态 (0=未锁定, 1=锁定)",
    "mop_forbidden_enable": "拖地禁区是否启用 (0=关闭, 1=启用)",
    "collision_avoid_status": "避障功能 (0=关闭, 1=开启)",
}


# ============================================================================
# 工具定义
# ============================================================================

@mcp.tool(
    annotations={
        "title": "List Roborock Devices",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    }
)
@_handle_errors
async def get_devices() -> dict[str, Any]:
    """列出账号下所有 Roborock 设备。

    通过 Roborock 云端 API 获取设备列表（不走 MQTT），返回每台设备的基本信息。

    Returns:
        dict: {
            "count": int,           # 设备数量
            "devices": [
                {
                    "name": str,    # 设备名称（用户在 App 中设置的名称）
                    "duid": str,    # 设备唯一标识符
                    "model": str,   # 设备型号（如 "roborock.vacuum.a75"）
                    "firmware": str # 固件版本号
                }
            ]
        }
    """
    conn = get_conn()
    devices = await conn.list_devices()

    result = {
        "count": len(devices),
        "devices": []
    }

    for device in devices:
        info = {
            "name": device.name,
            "duid": device.duid,
            "model": device.product.model if device.product else None,
            "firmware": device.device_info.fv if device.device_info else None,
        }
        result["devices"].append(info)

    return result


@mcp.tool(
    annotations={
        "title": "Get Device Full Status",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    }
)
@_handle_errors
async def get_status(device_name: str | None = None) -> dict[str, Any]:
    """获取 Roborock 设备精简状态（通过 MQTT 实时查询 8 个 trait）。

    每个字段返回 {"value": x, "description": "..."} 结构，方便理解字段含义。
    并行查询设备信息，包含：
    - 实时状态：电量、吸力、清洁进度、基座状态、错误码等（已精简，去除内部字段和重复字段）
    - 勿扰模式：开关状态及时间段
    - 清洁历史：累计清洁时间/面积/次数，最近一次清洁详情
    - 音量、房间列表（含 segment_id）、地图列表、耗材使用时间

    单个 trait 查询失败不影响其他 trait 返回，失败的 trait 对应字段会缺失。

    Args:
        device_name: 设备名称，支持模糊匹配。单设备时可省略，自动选择唯一设备。
    """
    conn = get_conn()
    device = await conn.get_device(device_name)

    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    props = device.v1_properties

    # 并行刷新所有 trait（device_features 保留 refresh 但不输出，fan_speed_name 等依赖它）
    trait_names = [
        "status", "dnd", "clean_summary", "sound_volume",
        "rooms", "maps", "consumables",
        "device_features",
    ]
    refresh_results = await asyncio.gather(
        *[_safe_refresh(getattr(props, name), name) for name in trait_names]
    )
    ok = dict(zip(trait_names, refresh_results))

    result: dict[str, Any] = {"device_name": device.name}

    # --- status trait ---
    if ok["status"]:
        status = props.status

        # 白名单字段
        for fname, desc in _STATUS_FIELDS.items():
            raw = getattr(status, fname, None)
            result[fname] = _field(_serialize_value(raw), desc)

        # computed properties — 用户友好值
        result["state"] = _field(status.state_name, "设备状态 (charging/cleaning/paused/returning_home/idle 等)")
        result["error_code"] = _field(status.error_code_name, "错误状态 (none=正常, lidar_blocked/bumper_stuck 等)")
        result["clean_area_m2"] = _field(status.square_meter_clean_area, "当前/最近清洁面积 (m²)")
        result["clean_time_minutes"] = _field(
            round(status.clean_time / 60, 1) if status.clean_time else 0,
            "当前/最近清洁时长 (分钟)",
        )
        result["fan_speed_name"] = _field(status.fan_speed_name, "吸力档位名称")
        result["water_mode_name"] = _field(status.water_mode_name, "拖布湿度名称")
        result["mop_route_name"] = _field(status.mop_route_name, "拖地路线名称")

        # dss 拆解（不输出原始 dss）
        result["clear_water_box_status"] = _field(
            _serialize_value(status.clear_water_box_status), "清水箱状态 (okay/out_of_water)")
        result["dirty_water_box_status"] = _field(
            _serialize_value(status.dirty_water_box_status), "污水箱状态 (okay/full_not_installed)")
        result["dust_bag_status"] = _field(
            _serialize_value(status.dust_bag_status), "尘袋状态 (okay/not_installed/full)")
        result["water_box_filter_status"] = _field(
            status.water_box_filter_status, "水滤网状态")
        result["clean_fluid_status"] = _field(
            _serialize_value(status.clean_fluid_status), "清洁液状态 (okay/empty_not_installed)")
        result["hatch_door_status"] = _field(
            status.hatch_door_status, "舱门状态")
        result["dock_cool_fan_status"] = _field(
            status.dock_cool_fan_status, "基座散热风扇状态")

    # --- dnd ---
    if ok["dnd"]:
        dnd = props.dnd
        result["dnd_enabled"] = _field(dnd.is_on, "勿扰模式是否启用")
        result["dnd_start_time"] = _field(
            f"{dnd.start_hour:02d}:{dnd.start_minute:02d}" if dnd.is_on else None,
            "勿扰开始时间 (HH:MM)",
        )
        result["dnd_end_time"] = _field(
            f"{dnd.end_hour:02d}:{dnd.end_minute:02d}" if dnd.is_on else None,
            "勿扰结束时间 (HH:MM)",
        )

    # --- clean_summary ---
    if ok["clean_summary"]:
        summary = props.clean_summary
        result["total_clean_time_hours"] = _field(
            round(summary.clean_time / 3600, 1) if summary.clean_time else 0,
            "累计清洁时长 (小时)",
        )
        result["total_clean_area_m2"] = _field(
            round(summary.clean_area / 1_000_000, 1) if summary.clean_area else 0,
            "累计清洁面积 (m²)",
        )
        result["total_clean_count"] = _field(summary.clean_count, "累计清洁次数")
        result["clean_record_count"] = _field(
            len(summary.records) if summary.records else 0, "清洁记录条数")

        if summary.last_clean_record:
            record = summary.last_clean_record
            result["last_clean_begin"] = _field(
                record.begin_datetime.isoformat() if record.begin_datetime else None,
                "最近清洁开始时间 (ISO8601)",
            )
            result["last_clean_end"] = _field(
                record.end_datetime.isoformat() if record.end_datetime else None,
                "最近清洁结束时间 (ISO8601)",
            )
            result["last_clean_duration_seconds"] = _field(
                record.duration, "最近清洁时长 (秒)")
            result["last_clean_area_m2"] = _field(
                record.area / 1_000_000 if record.area else 0,
                "最近清洁面积 (m²)",
            )
            result["last_clean_start_type"] = _field(
                record.start_type.name if record.start_type else None,
                "启动方式 (app/schedule/button/routines 等)",
            )
            result["last_clean_avoid_count"] = _field(
                record.avoid_count, "最近清洁避障次数")
            result["last_clean_wash_count"] = _field(
                record.wash_count, "最近清洁洗拖布次数")

    # --- sound_volume ---
    if ok["sound_volume"]:
        result["volume"] = _field(props.sound_volume.volume, "语音音量 (0-100)")

    # --- rooms ---
    if ok["rooms"]:
        rooms = props.rooms
        room_list = []
        for room in rooms.rooms or []:
            room_list.append({
                "segment_id": room.segment_id,
                "iot_id": room.iot_id,
                "name": room.raw_name or "<未命名>",
            })
        result["room_count"] = _field(len(room_list), "房间数量")
        result["rooms"] = _field(room_list, "房间列表 (含 segment_id 用于指定房间清洁)")

    # --- maps ---
    if ok["maps"]:
        maps = props.maps
        map_list = []
        for map_info in maps.map_info or []:
            map_list.append({
                "map_flag": map_info.map_flag,
                "name": map_info.name or "<未命名>",
                "room_count": len(map_info.rooms) if map_info.rooms else 0,
                "is_current": map_info.map_flag == maps.current_map,
            })
        result["current_map_flag"] = _field(maps.current_map, "当前地图 ID")
        result["map_count"] = _field(len(map_list), "地图数量")
        result["maps"] = _field(map_list, "地图列表")

    # --- consumables ---
    if ok["consumables"]:
        consumables = props.consumables
        SECS_PER_HOUR = 3600
        result["main_brush_hours"] = _field(
            round(consumables.main_brush_work_time / SECS_PER_HOUR, 1)
            if consumables.main_brush_work_time else 0,
            "主刷已使用时长 (小时，建议 300 小时更换)",
        )
        result["side_brush_hours"] = _field(
            round(consumables.side_brush_work_time / SECS_PER_HOUR, 1)
            if consumables.side_brush_work_time else 0,
            "边刷已使用时长 (小时，建议 200 小时更换)",
        )
        result["filter_hours"] = _field(
            round(consumables.filter_work_time / SECS_PER_HOUR, 1)
            if consumables.filter_work_time else 0,
            "滤网已使用时长 (小时，建议 150 小时更换)",
        )
        result["sensor_hours"] = _field(
            round(consumables.sensor_dirty_time / SECS_PER_HOUR, 1)
            if consumables.sensor_dirty_time else 0,
            "传感器清洁后使用时长 (小时，建议 30 小时清洁)",
        )

    return result


@mcp.tool(
    annotations={
        "title": "Get Map Content",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": False,
        "openWorldHint": True,
    }
)
@_handle_errors
async def get_map_content(device_name: str | None = None) -> dict[str, Any]:
    """获取当前地图的元数据（通过 MQTT 拉取，数据量较大）。

    返回地图图片大小、机器人实时坐标、充电座坐标。不返回原始图片字节。
    此工具走 MQTT 拉取地图数据（几百KB~几MB），比 get_status 更重，
    仅在需要位置信息时调用。

    Args:
        device_name: 设备名称，支持模糊匹配。单设备时可省略。

    Returns:
        dict: {
            "device_name": str,
            "image_size_bytes": int,    # 地图图片大小
            "has_map_data": bool,       # 是否有地图数据
            "vacuum_position": {"x": int, "y": int},   # 机器人位置
            "charger_position": {"x": int, "y": int}    # 充电座位置
        }
    """
    conn = get_conn()
    device = await conn.get_device(device_name)

    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    props = device.v1_properties
    await props.map_content.refresh()
    content = props.map_content

    result = {
        "device_name": device.name,
        "image_size_bytes": len(content.image_content) if content.image_content else 0,
        "has_map_data": content.map_data is not None,
    }

    if content.map_data:
        vacuum_pos = content.map_data.vacuum_position
        charger_pos = content.map_data.charger
        result["vacuum_position"] = {
            "x": vacuum_pos.x if vacuum_pos else None,
            "y": vacuum_pos.y if vacuum_pos else None,
        }
        result["charger_position"] = {
            "x": charger_pos.x if charger_pos else None,
            "y": charger_pos.y if charger_pos else None,
        }

    return result


# ============================================================================
# 控制类工具
# ============================================================================

@mcp.tool(
    annotations={
        "title": "Start Cleaning",
        "readOnlyHint": False,
        "destructiveHint": False,
        "idempotentHint": False,
        "openWorldHint": True,
    }
)
@_handle_errors
async def start_clean(
    device_name: str | None = None,
    segments: list[int] | None = None,
    repeat: int = 1,
) -> dict[str, Any]:
    """开始清洁。

    不传 segments 则全屋清洁，传 segments 则清洁指定房间。
    房间 segment_id 可通过 get_status 的 rooms 字段获取。
    命令已发送并确认，但设备执行需要几秒，建议稍后调用 get_status 查看最新状态。

    Args:
        device_name: 设备名称，支持模糊匹配。单设备时可省略。
        segments: 房间 segment_id 列表。不传则全屋清洁。
        repeat: 清洁遍数，默认 1。
    """
    conn = get_conn()
    device = await conn.get_device(device_name)
    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    cmd = device.v1_properties.command
    if segments:
        params = [{"segments": segments, "repeat": repeat}]
        await cmd.send(RoborockCommand.APP_SEGMENT_CLEAN, params=params)
        return {"status": "ok", "action": "segment_clean", "segments": segments, "repeat": repeat}
    else:
        await cmd.send(RoborockCommand.APP_START)
        return {"status": "ok", "action": "full_clean"}


@mcp.tool(
    annotations={
        "title": "Stop Cleaning",
        "readOnlyHint": False,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    }
)
@_handle_errors
async def stop_clean(device_name: str | None = None) -> dict[str, Any]:
    """停止清洁。机器人将原地停止。

    命令已发送并确认，但设备执行需要几秒，建议稍后调用 get_status 查看最新状态。

    Args:
        device_name: 设备名称，支持模糊匹配。单设备时可省略。
    """
    conn = get_conn()
    device = await conn.get_device(device_name)
    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    await device.v1_properties.command.send(RoborockCommand.APP_STOP)
    return {"status": "ok", "action": "stop"}


@mcp.tool(
    annotations={
        "title": "Pause Cleaning",
        "readOnlyHint": False,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    }
)
@_handle_errors
async def pause_clean(device_name: str | None = None) -> dict[str, Any]:
    """暂停清洁。可通过 resume_clean 恢复。

    命令已发送并确认，但设备执行需要几秒，建议稍后调用 get_status 查看最新状态。

    Args:
        device_name: 设备名称，支持模糊匹配。单设备时可省略。
    """
    conn = get_conn()
    device = await conn.get_device(device_name)
    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    await device.v1_properties.command.send(RoborockCommand.APP_PAUSE)
    return {"status": "ok", "action": "pause"}


@mcp.tool(
    annotations={
        "title": "Resume Cleaning",
        "readOnlyHint": False,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    }
)
@_handle_errors
async def resume_clean(device_name: str | None = None) -> dict[str, Any]:
    """恢复之前暂停的清洁任务。

    使用 APP_START 恢复，无论全屋还是分区清洁均可恢复。
    命令已发送并确认，但设备执行需要几秒，建议稍后调用 get_status 查看最新状态。

    Args:
        device_name: 设备名称，支持模糊匹配。单设备时可省略。
    """
    conn = get_conn()
    device = await conn.get_device(device_name)
    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    await device.v1_properties.command.send(RoborockCommand.APP_START)
    return {"status": "ok", "action": "resume"}


@mcp.tool(
    annotations={
        "title": "Go Home",
        "readOnlyHint": False,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    }
)
@_handle_errors
async def go_home(device_name: str | None = None) -> dict[str, Any]:
    """让机器人回充电座。

    命令已发送并确认，但设备执行需要几秒，建议稍后调用 get_status 查看最新状态。

    Args:
        device_name: 设备名称，支持模糊匹配。单设备时可省略。
    """
    conn = get_conn()
    device = await conn.get_device(device_name)
    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    await device.v1_properties.command.send(RoborockCommand.APP_CHARGE)
    return {"status": "ok", "action": "go_home"}


@mcp.tool(
    annotations={
        "title": "Find Robot",
        "readOnlyHint": False,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    }
)
@_handle_errors
async def find_robot(device_name: str | None = None) -> dict[str, Any]:
    """让机器人发出声音，帮助定位。

    命令已发送并确认，但设备执行需要几秒。

    Args:
        device_name: 设备名称，支持模糊匹配。单设备时可省略。
    """
    conn = get_conn()
    device = await conn.get_device(device_name)
    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    await device.v1_properties.command.send(RoborockCommand.FIND_ME)
    return {"status": "ok", "action": "find_robot"}


@mcp.tool(
    annotations={
        "title": "Dock Action",
        "readOnlyHint": False,
        "destructiveHint": False,
        "idempotentHint": False,
        "openWorldHint": True,
    }
)
@_handle_errors
async def dock_action(
    action: str,
    device_name: str | None = None,
) -> dict[str, Any]:
    """控制基座操作。

    命令已发送并确认，但设备执行需要几秒，建议稍后调用 get_status 查看最新状态。

    Args:
        action: 操作类型。可选值：
            - "start_collect_dust": 开始集尘
            - "stop_collect_dust": 停止集尘
            - "start_wash": 开始洗拖布
            - "stop_wash": 停止洗拖布
        device_name: 设备名称，支持模糊匹配。单设备时可省略。
    """
    ACTION_MAP = {
        "start_collect_dust": RoborockCommand.APP_START_COLLECT_DUST,
        "stop_collect_dust": RoborockCommand.APP_STOP_COLLECT_DUST,
        "start_wash": RoborockCommand.APP_START_WASH,
        "stop_wash": RoborockCommand.APP_STOP_WASH,
    }
    if action not in ACTION_MAP:
        return {"error": f"未知操作: {action}，可选: {', '.join(ACTION_MAP)}"}

    conn = get_conn()
    device = await conn.get_device(device_name)
    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    await device.v1_properties.command.send(ACTION_MAP[action])
    return {"status": "ok", "action": action}


@mcp.tool(
    annotations={
        "title": "Set Volume",
        "readOnlyHint": False,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    }
)
@_handle_errors
async def set_volume(
    device_name: str | None = None,
    volume: int = 50,
) -> dict[str, Any]:
    """设置机器人语音音量。

    命令已发送并确认，但设备执行需要几秒，建议稍后调用 get_status 查看最新状态。

    Args:
        device_name: 设备名称，支持模糊匹配。单设备时可省略。
        volume: 音量值，0-100。
    """
    if not 0 <= volume <= 100:
        return {"error": "音量值需在 0-100 之间"}

    conn = get_conn()
    device = await conn.get_device(device_name)
    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    await device.v1_properties.sound_volume.set_volume(volume)
    return {"status": "ok", "volume": volume}


@mcp.tool(
    annotations={
        "title": "List Routines",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    }
)
@_handle_errors
async def get_routines(device_name: str | None = None) -> dict[str, Any]:
    """列出设备的智能场景（通过云端 API 获取）。

    返回可执行的预设场景列表，每个场景有 id 和名称。
    使用 execute_routine 执行具体场景。

    Args:
        device_name: 设备名称，支持模糊匹配。单设备时可省略。
    """
    conn = get_conn()
    device = await conn.get_device(device_name)
    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    routines = await device.v1_properties.routines.get_routines()
    return {
        "device_name": device.name,
        "count": len(routines),
        "routines": [{"id": r.id, "name": r.name} for r in routines],
    }


@mcp.tool(
    annotations={
        "title": "Execute Routine",
        "readOnlyHint": False,
        "destructiveHint": False,
        "idempotentHint": False,
        "openWorldHint": True,
    }
)
@_handle_errors
async def execute_routine(
    routine_id: int,
    device_name: str | None = None,
) -> dict[str, Any]:
    """执行智能场景。

    通过 get_routines 获取可用场景及其 ID，然后传入 routine_id 执行。
    命令已发送并确认，但设备执行需要几秒，建议稍后调用 get_status 查看最新状态。

    Args:
        routine_id: 场景 ID，通过 get_routines 获取。必填参数。
        device_name: 设备名称，支持模糊匹配。单设备时可省略。
    """
    conn = get_conn()
    device = await conn.get_device(device_name)
    if not device.v1_properties:
        return {"error": "设备不支持 V1 协议"}

    await device.v1_properties.routines.execute_routine(routine_id)
    return {"status": "ok", "routine_id": routine_id}
