calculator.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. # server.py
  2. from mcp.server.fastmcp import FastMCP
  3. import sys
  4. import logging
  5. import os
  6. import threading
  7. import time
  8. from typing import List, Dict, Any, Optional
  9. import requests
  10. from utils.logger_config import logger
  11. try:
  12. import yaml
  13. except ImportError: # 允许在未安装 PyYAML 的环境下给出清晰日志
  14. yaml = None
  15. # Fix UTF-8 encoding for Windows console
  16. if sys.platform == 'win32':
  17. sys.stderr.reconfigure(encoding='utf-8')
  18. sys.stdout.reconfigure(encoding='utf-8')
  19. # Create an MCP server
  20. mcp = FastMCP("展厅交互模块")
  21. # YAML 配置路径
  22. VIDEO_CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'video_config_prod.yaml')
  23. CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'config.yaml')
  24. # 内存状态
  25. _is_running: bool = False
  26. _current_video_id: Optional[int] = None
  27. def _load_config(config_path: str) -> Dict[str, Any]:
  28. if yaml is None:
  29. return {}
  30. if not os.path.exists(config_path):
  31. return {}
  32. try:
  33. with open(config_path, 'r', encoding='utf-8') as f:
  34. return yaml.safe_load(f) or {}
  35. except Exception as e:
  36. logger.error(f"读取配置文件失败 {config_path}: {e}")
  37. return {}
  38. def _load_videos_from_yaml(config_path: str) -> List[Dict[str, Any]]:
  39. if yaml is None:
  40. logger.error("未安装 PyYAML,请在环境中安装 pyyaml 以读取配置文件")
  41. return []
  42. if not os.path.exists(config_path):
  43. logger.error(f"未找到配置文件: {config_path}")
  44. return []
  45. try:
  46. with open(config_path, 'r', encoding='utf-8') as f:
  47. data = yaml.safe_load(f) or {}
  48. videos = data.get('video_infos', [])
  49. if not isinstance(videos, list):
  50. logger.error("配置文件格式错误: 'video_infos' 应为列表")
  51. return []
  52. # 规范化字段
  53. normalized = []
  54. for v in videos:
  55. if not isinstance(v, dict):
  56. continue
  57. if 'id' not in v or 'name' not in v:
  58. continue
  59. normalized.append({
  60. 'id': v.get('id'),
  61. 'name': v.get('name'),
  62. 'formula': v.get('formula'),
  63. 'description': v.get('description'),
  64. 'video_duration': v.get('video_duration'),
  65. 'video_path': v.get('video_path'),
  66. 'code': v.get('code'),
  67. })
  68. return normalized
  69. except Exception as e:
  70. logger.exception(f"读取配置失败: {e}")
  71. return []
  72. def _index_videos(videos: List[Dict[str, Any]]) -> Dict[int, Dict[str, Any]]:
  73. return {int(v['id']): v for v in videos if isinstance(v.get('id'), int)}
  74. # 启动时加载
  75. _videos: List[Dict[str, Any]] = _load_videos_from_yaml(VIDEO_CONFIG_PATH)
  76. _videos_by_id: Dict[int, Dict[str, Any]] = _index_videos(_videos)
  77. # Flask API 基础地址(与 README_API.md 一致)
  78. _app_config = _load_config(CONFIG_PATH)
  79. FLASK_BASE = _app_config.get('flask_api_base', os.environ.get('FLASK_API_BASE', 'http://192.168.254.242:5050'))
  80. # 登录配置
  81. ADMIN_USERNAME = 'admin'
  82. ADMIN_PASSWORD = 'HNYZ0821'
  83. _session = requests.Session()
  84. _has_logged_in = False
  85. def _ensure_login():
  86. global _has_logged_in
  87. if _has_logged_in:
  88. return
  89. # 尝试常用登录路径 (先尝试 /auth/login, 再尝试 /login)
  90. for login_path in ['/auth/login', '/login']:
  91. url = f"{FLASK_BASE}{login_path}"
  92. try:
  93. # 只有当响应不是 404 时才认为是有效的登录端点
  94. resp = _session.post(url, data={
  95. 'username': ADMIN_USERNAME,
  96. 'password': ADMIN_PASSWORD
  97. }, timeout=5)
  98. if resp.status_code != 404:
  99. resp.raise_for_status()
  100. logger.info(f"登录成功: {url}")
  101. _has_logged_in = True
  102. return
  103. except Exception as e:
  104. logger.warning(f"尝试登录 {url} 失败: {e}")
  105. logger.error("无法登录 Flask API,后续请求可能会失败")
  106. def _flask_get(path: str) -> Dict[str, Any]:
  107. _ensure_login()
  108. url = f"{FLASK_BASE}{path}"
  109. try:
  110. resp = _session.get(url, timeout=3)
  111. resp.raise_for_status()
  112. return resp.json()
  113. except Exception as e:
  114. logger.error(f"GET {url} 失败: {e}")
  115. return {"success": False, "message": str(e)}
  116. def _flask_post(path: str, json_body: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
  117. _ensure_login()
  118. url = f"{FLASK_BASE}{path}"
  119. try:
  120. resp = _session.post(url, json=json_body or {}, timeout=3)
  121. resp.raise_for_status()
  122. return resp.json()
  123. except Exception as e:
  124. logger.error(f"POST {url} 失败: {e}")
  125. return {"success": False, "message": str(e)}
  126. @mcp.tool(name="获取所有可用的视频列表")
  127. def list_videos() -> dict:
  128. """获取所有可用的视频列表(来自 video_config.yaml)。
  129. 使用建议:
  130. - 当用户只说出展品名称且可能存在谐音/近音时,先调用本工具获取候选列表,
  131. 在客户端侧做相似度/拼音/编辑距离匹配,选取"最接近"的名称对应的 video_id,
  132. 再调用 kodi_start(video_id)。
  133. 返回的每个视频包含以下字段,可用于匹配:
  134. - id: 视频ID(整数)
  135. - name: 展品名称(如"氯化铷"、"硫酸钙新材料"等)
  136. - code: 展品编号(如"1号展品"、"2号展品"等)
  137. - formula: 化学式或公式
  138. - description: 描述信息
  139. """
  140. logger.info(f"获取视频列表,共 {_videos.__len__()} 个")
  141. return {"success": True, "videos": _videos, "count": len(_videos)}
  142. @mcp.tool(name="播放到指定的展品视频")
  143. def kodi_start(video_id: int) -> dict:
  144. """启动或切换 Kodi 播放到指定 video_id(转发到 Flask: POST /api/kodi/start)。
  145. 参数:
  146. - video_id: 整数类型的视频ID(从 list_videos() 获取)
  147. 使用场景和匹配策略:
  148. 1. 用户说编号类表达(如"一号展品"、"1号产品"、"展品1"、"产品1"等):
  149. - 提取数字部分(如"一号"→1、"1号"→1、"展品1"→1)
  150. - 直接使用该数字作为 video_id
  151. 2. 用户直接说物质名称(如"播放氯化铷"、"播放硫酸钙"、"氯化铯介绍视频"等):
  152. - 先调用 list_videos() 获取所有视频列表
  153. - 从返回的列表中匹配:
  154. a) 优先匹配 name 字段(精确或模糊匹配)
  155. b) 其次匹配 formula 字段
  156. c) 再次匹配 code 字段中的名称部分
  157. - 匹配时考虑:
  158. * 精确匹配(如"氯化铷"完全匹配)
  159. * 包含匹配(如输入"氯化铷"匹配到 name="氯化铷")
  160. * 拼音/谐音匹配(如"氯化铯"与"氯化铷"的相似度)
  161. * 编辑距离(允许轻微口误)
  162. - 选择"最接近"的条目,使用其 id 字段作为 video_id 调用本接口
  163. 3. 匹配示例:
  164. - "播放1号展品" → video_id=1
  165. - "播放一号产品" → video_id=1
  166. - "播放氯化铷" → 匹配 name="氯化铷" → video_id=1
  167. - "播放氯化铯介绍视频" → 匹配 name="氯化铯" → video_id=6
  168. - "硫酸钙" → 匹配 name="硫酸钙新材料"(包含匹配) → video_id=2
  169. 提示:如果用户给的是展品"名称"而非 id,且存在谐音/口误,
  170. - 先调用 list_videos() 获取所有视频元数据
  171. - 在调用侧进行名称相似度匹配(可用拼音/编辑距离/分词等方法)
  172. - 选择"最接近"的条目并传入其 video_id 调用本接口
  173. """
  174. if not isinstance(video_id, int) or video_id < 0:
  175. return {"success": False, "message": "video_id 必须为大于等于 0 的整数"}
  176. if video_id not in _videos_by_id:
  177. return {"success": False, "message": f"未找到视频ID={video_id}"}
  178. video = _videos_by_id[video_id]
  179. logger.info(f"请求 Flask 启动/切换播放: 视频ID={video_id},名称={video.get('name')}")
  180. api = _flask_post('/api/kodi/start', {"video_id": video_id})
  181. # 同步内存状态
  182. if api.get('success') is True:
  183. global _is_running, _current_video_id
  184. _is_running = True
  185. _current_video_id = video_id
  186. return api
  187. @mcp.tool(name="设置电视播放视频音量全局音量")
  188. def set_global_volume(volume: int) -> dict:
  189. """设置电视播放视频的全局音量 (转发到 Flask: POST /api/kodi/set_volume)。
  190. 使用场景:
  191. - 在播放产品视频或公司宣传视频时候,播放声音太小或太大就可以通过这个来控制。
  192. 参数:
  193. - volume: 音量值,整数 0-100
  194. """
  195. # 参数校验
  196. if not isinstance(volume, int):
  197. try:
  198. volume = int(volume)
  199. except (ValueError, TypeError):
  200. return {"success": False, "message": "音量值必须为整数"}
  201. if volume < 0 or volume > 100:
  202. return {"success": False, "message": "音量值必须在 0-100 之间"}
  203. logger.info(f"请求设置全局音量: {volume}")
  204. return _flask_post('/api/kodi/set_volume', {"volume": volume})
  205. @mcp.tool(name="打开办公楼大门")
  206. def open_door(door_id: int = 1) -> dict:
  207. """打开办公楼大门 (转发到 Flask: POST /api/door/open)。
  208. 使用场景:
  209. - 当用户说"打开大门"、"帮我开门"时调用此工具。
  210. - 目前只控制一个门,默认 door_id 为 1。
  211. 参数:
  212. - door_id: 门ID,整数,默认为 1
  213. """
  214. if not isinstance(door_id, int):
  215. return {"success": False, "message": "door_id 必须为整数"}
  216. logger.info(f"请求打开大门: door_id={door_id}")
  217. return _flask_post('/api/door/open', {"door_id": door_id})
  218. @mcp.tool(name="设置办公楼大门模式")
  219. def set_door_mode(control_way: int) -> dict:
  220. """设置办公楼大门模式 (转发到 Flask: POST /api/door/control)。
  221. 参数:
  222. - control_way: 模式,整数 (0:正常模式, 1:常开模式, 2:常闭模式)
  223. """
  224. if not isinstance(control_way, int):
  225. return {"success": False, "message": "control_way 必须为整数"}
  226. if control_way not in [0, 1, 2]:
  227. return {"success": False, "message": "control_way 必须为 0, 1 或 2"}
  228. logger.info(f"请求设置大门模式: control_way={control_way}")
  229. return _flask_post('/api/door/control', {"control_way": control_way})
  230. @mcp.tool(name="打开指定电视")
  231. def turn_on_tv(kodi_id: int) -> dict:
  232. """打开指定 ID 的电视 (转发到 Flask: POST /api/mitv/turn_on)。
  233. 参数:
  234. - kodi_id: 整数 (必填) 对应 Kodi 客户端的 ID/索引
  235. 使用说明:
  236. - 展厅共有 6 台电视,顺序从左到右。
  237. - kodi_id 从 0 开始计数:
  238. * "第一台"、"1号电视"、"左边第一台" -> kodi_id=0
  239. * "第二台"、"2号电视" -> kodi_id=1
  240. * ...
  241. * "第六台"、"6号电视"、"最右边那台" -> kodi_id=5
  242. """
  243. if not isinstance(kodi_id, int):
  244. return {"success": False, "message": "kodi_id 必须为整数"}
  245. logger.info(f"请求打开电视: kodi_id={kodi_id}")
  246. return _flask_post('/api/mitv/turn_on', {"kodi_id": kodi_id})
  247. @mcp.tool(name="关闭指定电视")
  248. def turn_off_tv(kodi_id: int) -> dict:
  249. """关闭指定 ID 的电视 (转发到 Flask: POST /api/mitv/turn_off)。
  250. 参数:
  251. - kodi_id: 整数 (必填) 对应 Kodi 客户端的 ID/索引
  252. 使用说明:
  253. - 展厅共有 6 台电视,顺序从左到右。
  254. - kodi_id 从 0 开始计数:
  255. * "第一台"、"1号电视"、"左边第一台" -> kodi_id=0
  256. * "第二台"、"2号电视" -> kodi_id=1
  257. * ...
  258. * "第六台"、"6号电视"、"最右边那台" -> kodi_id=5
  259. """
  260. if not isinstance(kodi_id, int):
  261. return {"success": False, "message": "kodi_id 必须为整数"}
  262. logger.info(f"请求关闭电视: kodi_id={kodi_id}")
  263. return _flask_post('/api/mitv/turn_off', {"kodi_id": kodi_id})
  264. @mcp.tool(name="打开所有电视")
  265. def turn_on_all_tvs() -> dict:
  266. """打开所有电视 (转发到 Flask: POST /api/mitv/turn_on_all)。
  267. 使用场景:
  268. - 当用户明确提到"所有"、"全部"时使用,如"打开所有电视"。
  269. - 如果用户仅说"开电视"、"帮我把电视打开",未明确指明是"所有"还是"某一台",请不要直接调用本工具,而是先向用户确认:"请问是打开所有电视,还是指定哪一台?"
  270. """
  271. logger.info("请求打开所有电视")
  272. return _flask_post('/api/mitv/turn_on_all')
  273. @mcp.tool(name="关闭所有电视")
  274. def turn_off_all_tvs() -> dict:
  275. """关闭所有电视 (转发到 Flask: POST /api/mitv/turn_off_all)。
  276. 使用场景:
  277. - 当用户明确提到"所有"、"全部"时使用,如"关闭所有电视"。
  278. - 用户说"关闭电视电源"通常也理解为关闭所有电视。
  279. - 如果用户仅说"关电视"、"把电视关了",未明确指明是"所有"还是"某一台",请不要直接调用本工具,而是先向用户确认:"请问是关闭所有电视,还是指定哪一台?"
  280. """
  281. logger.info("请求关闭所有电视")
  282. return _flask_post('/api/mitv/turn_off_all')
  283. # 灯光控制相关工具
  284. @mcp.tool(name="控制一楼大门玄关顶灯")
  285. def control_entrance_lights(action: str) -> dict:
  286. """控制一楼大门玄关顶灯 (转发到 Flask: POST /api/ha/entrance_lights/turn_on|off)。
  287. 参数:
  288. - action: "turn_on" (打开) 或 "turn_off" (关闭)
  289. """
  290. if action not in ["turn_on", "turn_off"]:
  291. return {"success": False, "message": "action 必须为 'turn_on' 或 'turn_off'"}
  292. logger.info(f"请求控制玄关顶灯: {action}")
  293. return _flask_post(f'/api/ha/entrance_lights/{action}')
  294. @mcp.tool(name="控制一楼大门玄关射灯")
  295. def control_exhibition_spotlight(action: str) -> dict:
  296. """控制一楼大门玄关射灯 (转发到 Flask: POST /api/ha/exhibition_spotlight/turn_on|off)。
  297. 参数:
  298. - action: "turn_on" (打开) 或 "turn_off" (关闭)
  299. """
  300. if action not in ["turn_on", "turn_off"]:
  301. return {"success": False, "message": "action 必须为 'turn_on' 或 'turn_off'"}
  302. logger.info(f"请求控制玄关射灯: {action}")
  303. return _flask_post(f'/api/ha/exhibition_spotlight/{action}')
  304. @mcp.tool(name="控制一楼展厅顶灯")
  305. def control_exhibition_ceiling_light(action: str) -> dict:
  306. """控制一楼展厅顶灯 (转发到 Flask: POST /api/ha/exhibition_ceiling_light/turn_on|off)。
  307. 参数:
  308. - action: "turn_on" (打开) 或 "turn_off" (关闭)
  309. """
  310. if action not in ["turn_on", "turn_off"]:
  311. return {"success": False, "message": "action 必须为 'turn_on' 或 'turn_off'"}
  312. logger.info(f"请求控制展厅顶灯: {action}")
  313. return _flask_post(f'/api/ha/exhibition_ceiling_light/{action}')
  314. @mcp.tool(name="控制展厅桌面灯座总开关")
  315. def control_exhibition_desktop_switch(action: str) -> dict:
  316. """控制展厅桌面灯座总开关 (转发到 Flask: POST /api/ha/exhibition_desktop_switch/turn_on|off)。
  317. 参数:
  318. - action: "turn_on" (打开) 或 "turn_off" (关闭)
  319. """
  320. if action not in ["turn_on", "turn_off"]:
  321. return {"success": False, "message": "action 必须为 'turn_on' 或 'turn_off'"}
  322. logger.info(f"请求控制桌面灯座总开关: {action}")
  323. return _flask_post(f'/api/ha/exhibition_desktop_switch/{action}')
  324. @mcp.tool(name="控制展厅桌面3D风扇投影")
  325. def control_exhibition_3d_fan(action: str) -> dict:
  326. """控制展厅桌面3D风扇投影 (转发到 Flask: POST /api/ha/exhibition_3d_fan/turn_on|off)。
  327. 参数:
  328. - action: "turn_on" (打开) 或 "turn_off" (关闭)
  329. """
  330. if action not in ["turn_on", "turn_off"]:
  331. return {"success": False, "message": "action 必须为 'turn_on' 或 'turn_off'"}
  332. logger.info(f"请求控制3D风扇投影: {action}")
  333. return _flask_post(f'/api/ha/exhibition_3d_fan/{action}')
  334. @mcp.tool(name="控制展台桌子灯带")
  335. def control_exhibition_stand_light_strip(action: str) -> dict:
  336. """控制展台桌子灯带 (转发到 Flask: POST /api/ha/exhibition_stand_light_strip/turn_on|off)。
  337. 参数:
  338. - action: "turn_on" (打开) 或 "turn_off" (关闭)
  339. """
  340. if action not in ["turn_on", "turn_off"]:
  341. return {"success": False, "message": "action 必须为 'turn_on' 或 'turn_off'"}
  342. logger.info(f"请求控制展台灯带: {action}")
  343. return _flask_post(f'/api/ha/exhibition_stand_light_strip/{action}')
  344. @mcp.tool(name="重启展厅所有设备")
  345. def restart_all_devices() -> dict:
  346. """重启展厅所有设备。
  347. 使用场景:
  348. - 当用户反馈设备故障、卡顿、无反应时调用。
  349. - 例如用户说:"电视播放卡住了"、"灯光控制没反应"、"设备出问题了"、"系统好像死机了"。
  350. - 此时建议主动询问用户:"检测到设备可能出现异常,建议重启展厅所有设备进行恢复,是否现在执行重启?"
  351. 功能:
  352. - 依次关闭所有电视、桌面3D风扇投影、展品灯座。
  353. - 等待 1 分钟。
  354. - 依次重新启动上述设备。
  355. - 此操作异步执行。
  356. """
  357. def _restart_process():
  358. try:
  359. logger.info("【重启任务】开始执行...")
  360. # 1. 关闭设备
  361. logger.info("【重启任务】正在关闭设备...")
  362. # 关闭所有电视
  363. _flask_post('/api/mitv/turn_off_all')
  364. # 关闭桌面3D风扇投影
  365. _flask_post('/api/ha/exhibition_3d_fan/turn_off')
  366. # 关闭展品灯座
  367. _flask_post('/api/ha/exhibition_desktop_switch/turn_off')
  368. # 2. 等待
  369. logger.info("【重启任务】设备已关闭,等待 60 秒...")
  370. time.sleep(60)
  371. # 3. 启动设备
  372. logger.info("【重启任务】正在启动设备...")
  373. # 启动所有电视
  374. _flask_post('/api/mitv/turn_on_all')
  375. # 启动桌面3D风扇投影
  376. _flask_post('/api/ha/exhibition_3d_fan/turn_on')
  377. # 启动展品灯座
  378. _flask_post('/api/ha/exhibition_desktop_switch/turn_on')
  379. # 启动视频播放
  380. _flask_post('/api/kodi/free_time/control', {"action": "start"})
  381. logger.info("【重启任务】执行完成")
  382. except Exception as e:
  383. logger.error(f"【重启任务】发生异常: {e}")
  384. # 启动后台线程
  385. t = threading.Thread(target=_restart_process)
  386. t.daemon = True
  387. t.start()
  388. return {"success": True, "message": "已开始重启展厅所有设备,全程约需 1 分钟,请耐心等待。"}
  389. @mcp.tool(name="启动迎宾模式")
  390. def start_welcome_mode() -> dict:
  391. """启动迎宾模式。
  392. 使用场景:
  393. - 当用户明确要求"启动迎宾模式"时调用。
  394. - 如果用户提到"有客人要来"、"准备接待客人"、"开启接待模式"等含义,请先主动询问用户:"是否需要为您开启迎宾模式?"
  395. 功能(异步执行):
  396. 1. 打开一楼大门玄关顶灯
  397. 2. 打开一楼大门玄关射灯
  398. 3. 打开一楼展厅顶灯
  399. 4. 打开展台桌子灯带
  400. 5. 把大门模式设置为常开模式
  401. """
  402. def _process():
  403. try:
  404. logger.info("【迎宾模式】正在启动...")
  405. # 1.打开一楼大门玄关顶灯
  406. _flask_post('/api/ha/entrance_lights/turn_on')
  407. # 2.打开一楼大门玄关射灯
  408. _flask_post('/api/ha/exhibition_spotlight/turn_on')
  409. # 3.打开一楼展厅顶灯
  410. _flask_post('/api/ha/exhibition_ceiling_light/turn_on')
  411. # 6.打开展台桌子灯带
  412. _flask_post('/api/ha/exhibition_stand_light_strip/turn_on')
  413. # 7.把大门模式设置为常开模式 (1:常开)
  414. _flask_post('/api/door/control', {"control_way": 1})
  415. logger.info("【迎宾模式】启动完成")
  416. except Exception as e:
  417. logger.error(f"【迎宾模式】启动失败: {e}")
  418. t = threading.Thread(target=_process)
  419. t.daemon = True
  420. t.start()
  421. return {"success": True, "message": "正在启动迎宾模式..."}
  422. @mcp.tool(name="关闭迎宾模式")
  423. def stop_welcome_mode() -> dict:
  424. """关闭迎宾模式。
  425. 功能(异步执行):
  426. 1. 关闭一楼大门玄关顶灯
  427. 2. 关闭一楼大门玄关射灯
  428. 3. 关闭一楼展厅顶灯
  429. 4. 关闭展台桌子灯带
  430. 5. 把大门模式设置为正常模式
  431. """
  432. def _process():
  433. try:
  434. logger.info("【迎宾模式】正在关闭...")
  435. # 1.关闭一楼大门玄关顶灯
  436. _flask_post('/api/ha/entrance_lights/turn_off')
  437. # 2.关闭一楼大门玄关射灯
  438. _flask_post('/api/ha/exhibition_spotlight/turn_off')
  439. # 3.关闭一楼展厅顶灯
  440. _flask_post('/api/ha/exhibition_ceiling_light/turn_off')
  441. # 6.关闭展台桌子灯带
  442. _flask_post('/api/ha/exhibition_stand_light_strip/turn_off')
  443. # 7.把大门模式设置为正常模式 (0:正常)
  444. _flask_post('/api/door/control', {"control_way": 0})
  445. logger.info("【迎宾模式】关闭完成")
  446. except Exception as e:
  447. logger.error(f"【迎宾模式】关闭失败: {e}")
  448. t = threading.Thread(target=_process)
  449. t.daemon = True
  450. t.start()
  451. return {"success": True, "message": "正在关闭迎宾模式..."}
  452. @mcp.tool(name="控制展品以及宣传视频自动播放")
  453. def control_free_time_playback(action: str) -> dict:
  454. """控制展品以及宣传视频自动播放功能 (转发到 Flask: POST /api/kodi/free_time/control)。
  455. 功能别名:继续电视播放、开启/关闭公司宣传视频播放。
  456. 使用场景:
  457. - 当用户说"继续播放电视"、"电视继续播放"时,通常意味着恢复默认的宣传视频循环播放,请调用 action="start"。
  458. - 当用户说"播放宣传视频"、"开启公司宣传视频"、"开启自动播放"时,调用 action="start"。
  459. - 当用户说"停止自动播放"、"关闭宣传视频"、"不要播放宣传片了"时,调用 action="stop"。
  460. 参数:
  461. - action: "start" (开启/强制开启) 或 "stop" (停止)
  462. 功能说明:
  463. - 开启后,如果在 07:30-18:00 时间段内,会自动循环播放视频 ID 0 (通常为公司宣传片)。
  464. - 手动发送 "start" 会立即强制开启播放,无论是否在时间段内。
  465. - 发送 "stop" 会立即强制停止播放。
  466. """
  467. if action not in ["start", "stop"]:
  468. return {"success": False, "message": "action 必须为 'start' 或 'stop'"}
  469. logger.info(f"请求控制展品以及宣传视频自动播放: {action}")
  470. return _flask_post('/api/kodi/free_time/control', {"action": action})
  471. @mcp.tool(name="获取展品以及宣传视频自动播放状态")
  472. def get_free_time_playback_status() -> dict:
  473. """获取展品以及宣传视频自动播放功能的状态 (转发到 Flask: GET /api/kodi/free_time/status)。
  474. 返回包含:
  475. - is_running: 功能开关状态
  476. - enabled: 同 is_running
  477. - is_thread_alive: 底层线程存活状态
  478. """
  479. logger.info("请求获取展品以及宣传视频自动播放状态")
  480. return _flask_get('/api/kodi/free_time/status')
  481. # Start the server
  482. if __name__ == "__main__":
  483. mcp.run(transport="stdio")