flask_api.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856
  1. from flask import Flask, jsonify, request, render_template, send_from_directory, session, redirect, url_for, flash
  2. from functools import wraps
  3. from flask_cors import CORS
  4. import json
  5. import yaml
  6. import os
  7. import time
  8. import uuid
  9. import socket
  10. import threading
  11. from werkzeug.utils import secure_filename
  12. from application.kodi_alive_thread import start_kodi_alive_check
  13. from application.kodi_free_time_thread import start_kodi_free_time_play
  14. from application.wled_thread import start_exhibit_led_effect, stop_led_effect, is_effect_running
  15. from application.kodi_thread import start_kodi_play, stop_kodi_play, is_kodi_thread_running, play_image, play_rtsp, revoke_individual_state, start_all_kodi_apps, get_kodi_clients, get_video_list, set_volume
  16. from hardware.mitvs_module import turn_on_display, turn_off_display, turn_on_all_displays, turn_off_all_displays
  17. from hardware.door_module import set_emergency_control, open_door_control
  18. from utils.logger_config import logger
  19. app = Flask(__name__)
  20. app.secret_key = 'your_secret_key_here_hnhz_0821' # 用于 session 加密,请更改为随机字符串
  21. CORS(app) # 允许跨域请求
  22. # 登录账号配置
  23. ADMIN_USERNAME = 'admin'
  24. ADMIN_PASSWORD = 'HNYZ0821'
  25. # 登录验证装饰器
  26. def login_required(f):
  27. @wraps(f)
  28. def decorated_function(*args, **kwargs):
  29. if 'logged_in' not in session:
  30. # 如果是 API 请求,返回 401
  31. if request.path.startswith('/api/'):
  32. return jsonify({'success': False, 'message': '未登录'}), 401
  33. # 如果是页面请求,跳转到登录页
  34. return redirect(url_for('login'))
  35. return f(*args, **kwargs)
  36. return decorated_function
  37. # 配置上传文件夹
  38. UPLOAD_FOLDER = 'uploads'
  39. ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'}
  40. app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
  41. app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 限制上传文件大小为16MB
  42. # 确保上传文件夹存在
  43. if not os.path.exists(UPLOAD_FOLDER):
  44. os.makedirs(UPLOAD_FOLDER)
  45. def allowed_file(filename):
  46. """检查文件扩展名是否允许"""
  47. return '.' in filename and \
  48. filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
  49. def get_server_ip():
  50. """获取服务器的本地IP地址"""
  51. try:
  52. # 连接到一个远程地址(不会实际发送数据)
  53. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  54. s.connect(("8.8.8.8", 80))
  55. ip = s.getsockname()[0]
  56. s.close()
  57. return ip
  58. except Exception:
  59. # 如果获取失败,尝试其他方法
  60. try:
  61. hostname = socket.gethostname()
  62. ip = socket.gethostbyname(hostname)
  63. return ip
  64. except Exception:
  65. return "127.0.0.1" # 回退到localhost
  66. def load_led_config():
  67. """读取LED配置文件"""
  68. try:
  69. config_path = 'led_config.yaml'
  70. if os.path.exists(config_path):
  71. with open(config_path, 'r', encoding='utf-8') as f:
  72. config = yaml.safe_load(f)
  73. return config.get('segments', [])
  74. return []
  75. except Exception as e:
  76. logger.error(f"读取LED配置文件失败: {e}")
  77. return []
  78. @app.route('/login', methods=['GET', 'POST'])
  79. def login():
  80. """登录页面"""
  81. if request.method == 'POST':
  82. username = request.form.get('username')
  83. password = request.form.get('password')
  84. if username == ADMIN_USERNAME and password == ADMIN_PASSWORD:
  85. session['logged_in'] = True
  86. # flash('登录成功', 'success') # 登录成功不需要提示,直接进
  87. return redirect(url_for('index'))
  88. else:
  89. flash('账号或密码错误', 'error')
  90. return render_template('login.html')
  91. return render_template('login.html')
  92. @app.route('/logout')
  93. def logout():
  94. """登出"""
  95. session.pop('logged_in', None)
  96. flash('已退出登录', 'success')
  97. return redirect(url_for('login'))
  98. @app.route('/')
  99. @login_required
  100. def index():
  101. """返回HTML页面"""
  102. led_segments = load_led_config()
  103. return render_template('index.html', led_segments=led_segments)
  104. @app.route('/api/led/status', methods=['GET'])
  105. @login_required
  106. def get_led_status():
  107. """获取LED状态"""
  108. try:
  109. is_running = is_effect_running()
  110. return jsonify({
  111. "success": True,
  112. "data": {
  113. "is_running": is_running,
  114. "message": "灯效正在运行" if is_running else "灯效已停止"
  115. }
  116. })
  117. except Exception as e:
  118. return jsonify({
  119. "success": False,
  120. "message": f"获取状态失败: {str(e)}"
  121. }), 500
  122. @app.route('/api/led/start', methods=['POST'])
  123. @login_required
  124. def start_led_effect():
  125. """启动展品LED灯效控制"""
  126. try:
  127. data = request.get_json()
  128. if not data or 'exhibit_id' not in data:
  129. return jsonify({
  130. "success": False,
  131. "message": "缺少展品ID参数"
  132. }), 400
  133. exhibit_id = data['exhibit_id']
  134. if not isinstance(exhibit_id, int) or exhibit_id < 0:
  135. return jsonify({
  136. "success": False,
  137. "message": "展品ID必须是大于等于0的整数"
  138. }), 400
  139. success = start_exhibit_led_effect(exhibit_id)
  140. if success:
  141. return jsonify({
  142. "success": True,
  143. "message": f"展品 {exhibit_id} 的灯效已启动",
  144. "data": {
  145. "exhibit_id": exhibit_id,
  146. "is_running": True
  147. }
  148. })
  149. else:
  150. return jsonify({
  151. "success": False,
  152. "message": f"启动展品 {exhibit_id} 的灯效失败"
  153. }), 500
  154. except Exception as e:
  155. return jsonify({
  156. "success": False,
  157. "message": f"启动灯效失败: {str(e)}"
  158. }), 500
  159. @app.route('/api/led/stop', methods=['POST'])
  160. @login_required
  161. def stop_led_effect_api():
  162. """停止当前LED灯效"""
  163. try:
  164. success = stop_led_effect()
  165. if success:
  166. return jsonify({
  167. "success": True,
  168. "message": "灯效已停止",
  169. "data": {
  170. "is_running": False
  171. }
  172. })
  173. else:
  174. return jsonify({
  175. "success": False,
  176. "message": "停止灯效失败"
  177. }), 500
  178. except Exception as e:
  179. return jsonify({
  180. "success": False,
  181. "message": f"停止灯效失败: {str(e)}"
  182. }), 500
  183. # ===== Kodi 播放控制接口 =====
  184. @app.route('/api/kodi/status', methods=['GET'])
  185. @login_required
  186. def get_kodi_status():
  187. try:
  188. running = is_kodi_thread_running()
  189. return jsonify({
  190. "success": True,
  191. "data": {
  192. "is_running": running,
  193. "message": "Kodi播放线程运行中" if running else "Kodi播放线程已停止"
  194. }
  195. })
  196. except Exception as e:
  197. return jsonify({
  198. "success": False,
  199. "message": f"获取Kodi状态失败: {str(e)}"
  200. }), 500
  201. @app.route('/api/kodi/clients', methods=['GET'])
  202. @login_required
  203. def get_kodi_clients_api():
  204. """获取所有Kodi客户端列表"""
  205. try:
  206. clients = get_kodi_clients()
  207. return jsonify({
  208. "success": True,
  209. "data": clients
  210. })
  211. except Exception as e:
  212. return jsonify({
  213. "success": False,
  214. "message": f"获取Kodi客户端列表失败: {str(e)}"
  215. }), 500
  216. @app.route('/api/kodi/videos', methods=['GET'])
  217. @login_required
  218. def get_kodi_videos_api():
  219. """获取所有视频列表"""
  220. try:
  221. videos = get_video_list()
  222. return jsonify({
  223. "success": True,
  224. "data": videos
  225. })
  226. except Exception as e:
  227. return jsonify({
  228. "success": False,
  229. "message": f"获取视频列表失败: {str(e)}"
  230. }), 500
  231. @app.route('/api/kodi/start', methods=['POST'])
  232. @login_required
  233. def start_kodi_play_api():
  234. try:
  235. data = request.get_json()
  236. if not data or 'video_id' not in data:
  237. return jsonify({
  238. "success": False,
  239. "message": "缺少视频ID参数"
  240. }), 400
  241. video_id = data['video_id']
  242. volume = data.get('volume', -1)
  243. if not isinstance(video_id, int) or video_id < 0:
  244. return jsonify({
  245. "success": False,
  246. "message": "视频ID必须是大于等于0的整数"
  247. }), 400
  248. # 验证 volume 参数
  249. if volume != -1:
  250. if not isinstance(volume, int) or not (0 <= volume <= 100):
  251. return jsonify({
  252. "success": False,
  253. "message": "音量必须是 0-100 之间的整数"
  254. }), 400
  255. ok = start_kodi_play(video_id, volume)
  256. if ok:
  257. return jsonify({
  258. "success": True,
  259. "message": f"Kodi开始/切换播放 视频ID={video_id} 音量={'默认' if volume == -1 else volume}",
  260. "data": {"video_id": video_id, "volume": volume}
  261. })
  262. return jsonify({
  263. "success": False,
  264. "message": f"Kodi播放启动失败(视频ID={video_id})"
  265. }), 500
  266. except Exception as e:
  267. return jsonify({
  268. "success": False,
  269. "message": f"Kodi播放启动异常: {str(e)}"
  270. }), 500
  271. # 指定某台kodi_client_index播放图片,这边要上传图片并且传递完整url给kodi播放
  272. @app.route('/api/kodi/play_image', methods=['POST'])
  273. @login_required
  274. def play_image_api():
  275. """播放图片接口,支持文件上传或直接传递图片URL"""
  276. try:
  277. # 检查是否有文件上传
  278. if 'file' in request.files:
  279. file = request.files['file']
  280. if file.filename == '':
  281. return jsonify({
  282. "success": False,
  283. "message": "未选择文件"
  284. }), 400
  285. if file and allowed_file(file.filename):
  286. # 获取原始文件扩展名
  287. original_filename = file.filename
  288. # 提取文件扩展名(不包含点号)
  289. if '.' in original_filename:
  290. file_ext = original_filename.rsplit('.', 1)[1].lower()
  291. else:
  292. # 如果没有扩展名,根据Content-Type推断或默认为jpg
  293. content_type = file.content_type or ''
  294. if 'png' in content_type:
  295. file_ext = 'png'
  296. elif 'jpeg' in content_type or 'jpg' in content_type:
  297. file_ext = 'jpg'
  298. elif 'gif' in content_type:
  299. file_ext = 'gif'
  300. else:
  301. file_ext = 'jpg' # 默认扩展名
  302. # 确保扩展名在允许列表中
  303. if file_ext not in ALLOWED_EXTENSIONS:
  304. file_ext = 'jpg'
  305. # 生成唯一文件名:使用时间戳和UUID,确保文件名唯一且安全
  306. timestamp = int(time.time() * 1000)
  307. unique_id = str(uuid.uuid4())[:8] # 使用UUID的前8位作为唯一标识
  308. filename = f"{timestamp}_{unique_id}.{file_ext}"
  309. filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
  310. file.save(filepath)
  311. # 生成可访问的URL(使用服务器的实际IP地址,而不是localhost)
  312. # 如果request.host包含localhost或127.0.0.1,使用实际IP
  313. host = request.host
  314. if 'localhost' in host or '127.0.0.1' in host:
  315. server_ip = get_server_ip()
  316. port = request.environ.get('SERVER_PORT', '5000')
  317. host = f"{server_ip}:{port}"
  318. image_url = f"http://{host}/uploads/{filename}"
  319. logger.info(f"图片已上传: {filepath}, URL: {image_url}, 原始文件名: {original_filename}")
  320. else:
  321. return jsonify({
  322. "success": False,
  323. "message": f"不支持的文件类型,允许的类型: {', '.join(ALLOWED_EXTENSIONS)}"
  324. }), 400
  325. elif request.is_json:
  326. # 检查是否有直接的图片URL
  327. data = request.get_json()
  328. if 'image_url' not in data:
  329. return jsonify({
  330. "success": False,
  331. "message": "缺少参数:需要 'file'(文件上传)或 'image_url'(图片URL)"
  332. }), 400
  333. image_url = data['image_url']
  334. if not image_url or not isinstance(image_url, str):
  335. return jsonify({
  336. "success": False,
  337. "message": "无效的图片URL"
  338. }), 400
  339. else:
  340. return jsonify({
  341. "success": False,
  342. "message": "缺少参数:需要 'file'(文件上传)或 'image_url'(图片URL)"
  343. }), 400
  344. # 获取客户端索引
  345. if 'kodi_client_index' in request.form:
  346. try:
  347. client_index = int(request.form['kodi_client_index'])
  348. except (ValueError, TypeError):
  349. return jsonify({
  350. "success": False,
  351. "message": "kodi_client_index 必须是整数"
  352. }), 400
  353. elif request.is_json and 'kodi_client_index' in request.get_json():
  354. client_index = request.get_json()['kodi_client_index']
  355. else:
  356. return jsonify({
  357. "success": False,
  358. "message": "缺少参数: kodi_client_index"
  359. }), 400
  360. if not isinstance(client_index, int) or client_index < 0:
  361. return jsonify({
  362. "success": False,
  363. "message": "kodi_client_index 必须是大于等于0的整数"
  364. }), 400
  365. # 调用播放函数
  366. success = play_image(image_url, client_index)
  367. if success:
  368. return jsonify({
  369. "success": True,
  370. "message": f"已在客户端 {client_index} 上启动图片播放",
  371. "data": {
  372. "image_url": image_url,
  373. "client_index": client_index
  374. }
  375. })
  376. else:
  377. return jsonify({
  378. "success": False,
  379. "message": f"在客户端 {client_index} 上启动图片播放失败"
  380. }), 500
  381. except Exception as e:
  382. logger.error(f"播放图片异常: {str(e)}")
  383. return jsonify({
  384. "success": False,
  385. "message": f"播放图片失败: {str(e)}"
  386. }), 500
  387. # 指定某台kodi_client_index播放rtsp视频
  388. @app.route('/api/kodi/play_rtsp', methods=['POST'])
  389. @login_required
  390. def play_rtsp_api():
  391. """播放RTSP视频流接口"""
  392. try:
  393. data = request.get_json()
  394. if not data:
  395. return jsonify({
  396. "success": False,
  397. "message": "请求体不能为空"
  398. }), 400
  399. # 检查必需的参数
  400. if 'rtsp_url' not in data:
  401. return jsonify({
  402. "success": False,
  403. "message": "缺少参数: rtsp_url"
  404. }), 400
  405. if 'kodi_client_index' not in data:
  406. return jsonify({
  407. "success": False,
  408. "message": "缺少参数: kodi_client_index"
  409. }), 400
  410. rtsp_url = data['rtsp_url']
  411. client_index = data['kodi_client_index']
  412. volume = data.get('volume', 0) # 可选参数,默认为0
  413. # 参数验证
  414. if not isinstance(rtsp_url, str) or not rtsp_url.strip():
  415. return jsonify({
  416. "success": False,
  417. "message": "rtsp_url 必须是有效的字符串"
  418. }), 400
  419. if not isinstance(client_index, int) or client_index < 0:
  420. return jsonify({
  421. "success": False,
  422. "message": "kodi_client_index 必须是大于等于0的整数"
  423. }), 400
  424. if not isinstance(volume, int) or volume < 0 or volume > 100:
  425. return jsonify({
  426. "success": False,
  427. "message": "volume 必须是 0-100 之间的整数"
  428. }), 400
  429. # 调用播放函数
  430. success = play_rtsp(rtsp_url, client_index, volume)
  431. if success:
  432. return jsonify({
  433. "success": True,
  434. "message": f"已在客户端 {client_index} 上启动RTSP播放",
  435. "data": {
  436. "rtsp_url": rtsp_url,
  437. "client_index": client_index,
  438. "volume": volume
  439. }
  440. })
  441. else:
  442. return jsonify({
  443. "success": False,
  444. "message": f"在客户端 {client_index} 上启动RTSP播放失败"
  445. }), 500
  446. except Exception as e:
  447. logger.error(f"播放RTSP异常: {str(e)}")
  448. return jsonify({
  449. "success": False,
  450. "message": f"播放RTSP失败: {str(e)}"
  451. }), 500
  452. @app.route('/api/kodi/revoke_individual_state', methods=['POST'])
  453. @login_required
  454. def revoke_individual_state_api():
  455. """撤销所有客户端的独立状态接口"""
  456. try:
  457. success = revoke_individual_state()
  458. if success:
  459. return jsonify({
  460. "success": True,
  461. "message": "已撤销所有客户端的独立状态"
  462. })
  463. else:
  464. return jsonify({
  465. "success": False,
  466. "message": "撤销所有客户端的独立状态失败"
  467. }), 500
  468. except Exception as e:
  469. logger.error(f"撤销独立状态异常: {str(e)}")
  470. return jsonify({
  471. "success": False,
  472. "message": f"撤销独立状态失败: {str(e)}"
  473. }), 500
  474. @app.route('/api/kodi/start_all_apps', methods=['POST'])
  475. @login_required
  476. def start_all_kodi_apps_api():
  477. """启动所有kodi应用程序接口"""
  478. try:
  479. success = start_all_kodi_apps()
  480. if success:
  481. return jsonify({
  482. "success": True,
  483. "message": "已启动所有kodi应用程序"
  484. })
  485. else:
  486. return jsonify({
  487. "success": False,
  488. "message": "启动所有kodi应用程序失败"
  489. }), 500
  490. except Exception as e:
  491. logger.error(f"启动所有kodi应用程序异常: {str(e)}")
  492. return jsonify({
  493. "success": False,
  494. "message": f"启动所有kodi应用程序失败: {str(e)}"
  495. }), 500
  496. @app.route('/api/kodi/set_volume', methods=['POST'])
  497. @login_required
  498. def set_kodi_volume_api():
  499. """设置Kodi播放音量接口"""
  500. try:
  501. data = request.get_json()
  502. if not data or 'volume' not in data:
  503. return jsonify({
  504. "success": False,
  505. "message": "缺少 volume 参数"
  506. }), 400
  507. volume = data['volume']
  508. if not isinstance(volume, int) or not (0 <= volume <= 100):
  509. return jsonify({
  510. "success": False,
  511. "message": "音量必须是 0-100 之间的整数"
  512. }), 400
  513. success = set_volume(volume)
  514. if success:
  515. return jsonify({
  516. "success": True,
  517. "message": f"已设置音量为 {volume}",
  518. "data": {"volume": volume}
  519. })
  520. else:
  521. return jsonify({
  522. "success": False,
  523. "message": "设置音量失败"
  524. }), 500
  525. except Exception as e:
  526. logger.error(f"设置音量异常: {str(e)}")
  527. return jsonify({
  528. "success": False,
  529. "message": f"设置音量失败: {str(e)}"
  530. }), 500
  531. # 提供上传文件的访问接口
  532. @app.route('/uploads/<filename>')
  533. def uploaded_file(filename):
  534. """提供上传文件的访问接口"""
  535. return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
  536. # ===== 电视开关控制接口 =====
  537. @app.route('/api/mitv/turn_on', methods=['POST'])
  538. @login_required
  539. def turn_on_display_api():
  540. """唤醒指定ID的电视"""
  541. try:
  542. data = request.get_json()
  543. if not data or 'kodi_id' not in data:
  544. return jsonify({
  545. "success": False,
  546. "message": "缺少 kodi_id 参数"
  547. }), 400
  548. kodi_id = data['kodi_id']
  549. if not isinstance(kodi_id, int) or kodi_id < 0:
  550. return jsonify({
  551. "success": False,
  552. "message": "kodi_id 必须是大于等于0的整数"
  553. }), 400
  554. def task():
  555. try:
  556. turn_on_display(kodi_id)
  557. except Exception as e:
  558. logger.error(f"异步唤醒电视异常 (ID: {kodi_id}): {str(e)}")
  559. threading.Thread(target=task).start()
  560. return jsonify({
  561. "success": True,
  562. "message": f"已发送唤醒指令给电视 (ID: {kodi_id}) - 异步执行"
  563. })
  564. except Exception as e:
  565. logger.error(f"唤醒电视异常: {str(e)}")
  566. return jsonify({
  567. "success": False,
  568. "message": f"唤醒电视失败: {str(e)}"
  569. }), 500
  570. @app.route('/api/mitv/turn_off', methods=['POST'])
  571. @login_required
  572. def turn_off_display_api():
  573. """息屏指定ID的电视"""
  574. try:
  575. data = request.get_json()
  576. if not data or 'kodi_id' not in data:
  577. return jsonify({
  578. "success": False,
  579. "message": "缺少 kodi_id 参数"
  580. }), 400
  581. kodi_id = data['kodi_id']
  582. if not isinstance(kodi_id, int) or kodi_id < 0:
  583. return jsonify({
  584. "success": False,
  585. "message": "kodi_id 必须是大于等于0的整数"
  586. }), 400
  587. def task():
  588. try:
  589. turn_off_display(kodi_id)
  590. except Exception as e:
  591. logger.error(f"异步息屏电视异常 (ID: {kodi_id}): {str(e)}")
  592. threading.Thread(target=task).start()
  593. return jsonify({
  594. "success": True,
  595. "message": f"已发送息屏指令给电视 (ID: {kodi_id}) - 异步执行"
  596. })
  597. except Exception as e:
  598. logger.error(f"息屏电视异常: {str(e)}")
  599. return jsonify({
  600. "success": False,
  601. "message": f"息屏电视失败: {str(e)}"
  602. }), 500
  603. @app.route('/api/mitv/turn_on_all', methods=['POST'])
  604. @login_required
  605. def turn_on_all_displays_api():
  606. """唤醒所有电视"""
  607. try:
  608. def task():
  609. try:
  610. turn_on_all_displays()
  611. except Exception as e:
  612. logger.error(f"异步唤醒所有电视异常: {str(e)}")
  613. threading.Thread(target=task).start()
  614. return jsonify({
  615. "success": True,
  616. "message": "已发送唤醒指令给所有电视 - 异步执行"
  617. })
  618. except Exception as e:
  619. logger.error(f"唤醒所有电视异常: {str(e)}")
  620. return jsonify({
  621. "success": False,
  622. "message": f"唤醒所有电视失败: {str(e)}"
  623. }), 500
  624. @app.route('/api/mitv/turn_off_all', methods=['POST'])
  625. @login_required
  626. def turn_off_all_displays_api():
  627. """息屏所有电视"""
  628. try:
  629. def task():
  630. try:
  631. turn_off_all_displays()
  632. except Exception as e:
  633. logger.error(f"异步息屏所有电视异常: {str(e)}")
  634. threading.Thread(target=task).start()
  635. return jsonify({
  636. "success": True,
  637. "message": "已发送息屏指令给所有电视 - 异步执行"
  638. })
  639. except Exception as e:
  640. logger.error(f"息屏所有电视异常: {str(e)}")
  641. return jsonify({
  642. "success": False,
  643. "message": f"息屏所有电视失败: {str(e)}"
  644. }), 500
  645. # ===== 门禁控制接口 =====
  646. @app.route('/api/door/control', methods=['POST'])
  647. @login_required
  648. def door_control_api():
  649. """门禁控制接口"""
  650. try:
  651. data = request.get_json()
  652. if not data or 'control_way' not in data:
  653. return jsonify({
  654. "success": False,
  655. "message": "缺少 control_way 参数"
  656. }), 400
  657. control_way = data['control_way']
  658. password = data.get('password') # 可选参数
  659. if not isinstance(control_way, int) or control_way not in [0, 1, 2]:
  660. return jsonify({
  661. "success": False,
  662. "message": "control_way 必须是 0(在线), 1(常开), 2(常闭)"
  663. }), 400
  664. def task():
  665. try:
  666. set_emergency_control(control_way, password)
  667. except Exception as e:
  668. logger.error(f"异步门禁控制异常 (Mode: {control_way}): {str(e)}")
  669. threading.Thread(target=task).start()
  670. return jsonify({
  671. "success": True,
  672. "message": "已发送门禁控制指令 - 异步执行",
  673. "data": {
  674. "control_way": control_way
  675. }
  676. })
  677. except Exception as e:
  678. logger.error(f"门禁控制异常: {str(e)}")
  679. return jsonify({
  680. "success": False,
  681. "message": f"门禁控制失败: {str(e)}"
  682. }), 500
  683. @app.route('/api/door/open', methods=['POST'])
  684. @login_required
  685. def door_open_api():
  686. """远程开门接口"""
  687. try:
  688. data = request.get_json()
  689. if not data or 'door_id' not in data:
  690. return jsonify({
  691. "success": False,
  692. "message": "缺少 door_id 参数"
  693. }), 400
  694. door_id = data['door_id']
  695. password = data.get('password') # 可选参数
  696. if not isinstance(door_id, int):
  697. return jsonify({
  698. "success": False,
  699. "message": "door_id 必须是整数"
  700. }), 400
  701. def task():
  702. try:
  703. open_door_control(door_id, password)
  704. except Exception as e:
  705. logger.error(f"异步远程开门异常 (ID: {door_id}): {str(e)}")
  706. threading.Thread(target=task).start()
  707. return jsonify({
  708. "success": True,
  709. "message": "已发送远程开门指令 - 异步执行",
  710. "data": {
  711. "door_id": door_id
  712. }
  713. })
  714. except Exception as e:
  715. logger.error(f"远程开门异常: {str(e)}")
  716. return jsonify({
  717. "success": False,
  718. "message": f"远程开门失败: {str(e)}"
  719. }), 500
  720. if __name__ == '__main__':
  721. # 创建templates目录
  722. import os
  723. if not os.path.exists('templates'):
  724. os.makedirs('templates')
  725. logger.info("启动Kodi心跳检测")
  726. start_kodi_alive_check()
  727. logger.info("启动Kodi空闲时间播放")
  728. start_kodi_free_time_play()
  729. logger.info("Flask API服务器启动中...")
  730. logger.info("访问 http://localhost:5050 查看HTML页面")
  731. logger.info("API端点:")
  732. logger.info(" GET /api/led/status - 获取LED状态")
  733. logger.info(" POST /api/led/start - 启动展品灯效")
  734. logger.info(" POST /api/led/stop - 停止灯效")
  735. logger.info(" GET /api/kodi/status - 获取Kodi状态")
  736. logger.info(" GET /api/kodi/clients - 获取Kodi客户端列表")
  737. logger.info(" GET /api/kodi/videos - 获取视频列表")
  738. logger.info(" POST /api/kodi/start - 启动/切换Kodi播放")
  739. logger.info(" POST /api/kodi/play_image - 播放图片(支持上传或URL)")
  740. logger.info(" POST /api/kodi/play_rtsp - 播放RTSP视频流")
  741. logger.info(" POST /api/kodi/revoke_individual_state - 撤销所有客户端的独立状态")
  742. logger.info(" POST /api/kodi/start_all_apps - 启动所有kodi应用程序")
  743. logger.info(" POST /api/mitv/turn_on - 唤醒电视 (参数: kodi_id)")
  744. logger.info(" POST /api/mitv/turn_off - 息屏电视 (参数: kodi_id)")
  745. logger.info(" POST /api/mitv/turn_on_all - 唤醒所有电视")
  746. logger.info(" POST /api/mitv/turn_off_all - 息屏所有电视")
  747. logger.info(" POST /api/door/control - 门禁控制 (参数: control_way [0:在线, 1:常开, 2:常闭])")
  748. logger.info(" POST /api/door/open - 远程开门 (参数: door_id)")
  749. app.run(debug=True, host='0.0.0.0', port=5050)