准备
我自己有一台接入米家的夜灯,型号是米家床头灯二代,官方已经给出了产品规格与各项控制参数
感谢Do1e开源的的mijiaAPI,这个仓库向调用者提供了傻瓜式的米家电器控制调用接口,只需要调用相应方法、传入对应于设备控制意图的参数,即可通过云端控制米家设备
AstrBot部署于阿里云服务器,已经接入微信的ClawBot。为了插件开发方便,也在本地部署了AstrBot
环境配置
首先在AstrBot的根目录下安装依赖,官方提供了uv依赖管理:
cd AstrBot
uv sync
uv pip install mijiaAPI
AstrBot为插件开发者提供了模板仓库,只需使用此模板,然后克隆到本地的AstrBot插件文件夹即可开始开发
cd AstrBot/data/plugins
git clone <template-registry>
为了保证插件的正常运行,之后在插件开发调试中,Python环境都应使用外层目录的astrbot,而不应在插件目录新建环境
登录米家
mijiaAPI提供了便捷的登陆方式:
import mijiaAPI from mijiaAPI
api = mijiaAPI("auth.json")
api.login()
执行login()时,程序会在终端打印登录二维码,使用登陆了米家账号的设备扫描后即可完成登录,此后程序不再阻塞,继续执行后面的脚本
不过,如果要将登录逻辑引入AstrBot机器人聊天环境存在一些问题:
- 登陆时的阻塞会导致机器人无法回复
- 登陆二维码在终端打印,需要某种方法将登陆手段以机器人回复的形式向用户呈现
- 在满足以上条件的基础上,还要维持原mijiaAPI中的轮询用户登录逻辑,并在用户扫码登录成功后使机器人回复“登陆成功”提示用户
关于阻塞问题的解决方案,我参考了已有的在AstrBot中调用mijiaAPI的仓库,由674537331开发,核心思路是创建一个独立的进程沙盒,在沙盒中运行login()从而避免主进程被阻塞
在超时时间内每隔0.1秒不断读取进程的输出,如果读取到了二维码URL,则立即向用户回复该URL,回复后不结束登录流程,而是继续循环读取输出,直到超时或读取到登陆成功或失败
在子进程中执行登录的脚本_login_worker.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
登录工作脚本,在子进程中执行登录流程
"""
import sys
import os
import time
from pathlib import Path
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, encoding='utf-8')
logging.getLogger("mijiaAPI").setLevel(logging.INFO)
# 强制设置 stdout 编码为 UTF-8
if sys.stdout.encoding != 'utf-8':
sys.stdout.reconfigure(encoding='utf-8')
if sys.stderr.encoding != 'utf-8':
sys.stderr.reconfigure(encoding='utf-8')
from mijiaAPI import mijiaAPI
def main():
if len(sys.argv) != 2:
print("Usage: python _login_worker.py <auth_path>", flush=True)
sys.exit(1)
auth_path = Path(sys.argv[1])
print("[WORKER] 开始初始化认证环境。", flush=True)
try:
# 确保目录存在
auth_path.parent.mkdir(parents=True, exist_ok=True)
api = mijiaAPI(auth_path)
print("[WORKER] API 实例已创建,正在请求小米服务器...", flush=True)
# 执行登录
result = api.login()
print("[WORKER] 登录成功", flush=True)
print("[WORKER_SUCCESS] 授权完毕。", flush=True)
sys.exit(0)
except Exception as e:
print(f"[WORKER_ERROR] 登录流程失败: {type(e).__name__}: {e}", flush=True)
sys.exit(1)
if __name__ == "__main__":
main()
主文件:
@filter.command("登录")
async def login(self, event: AstrMessageEvent):
"""登录指令"""
try:
# 先发送一条消息,告知用户登录流程开始
yield event.plain_result("正在启动登录流程,请稍候...")
# 执行异步登录,不使用回调函数,而是直接获取二维码
# 先检查是否已经登录
if self.client.auth_path.exists():
try:
import json
with open(self.client.auth_path, 'r', encoding='utf-8') as f:
auth_data = json.load(f)
if auth_data.get('serviceToken'):
yield event.plain_result("Token有效,无需登录")
return
except Exception:
pass
# 启动登录沙盒进程
import subprocess
import sys
import time
import re
# 确保认证目录存在
self.client.auth_path.parent.mkdir(parents=True, exist_ok=True)
# 启动登录工作进程
proc = subprocess.Popen(
[sys.executable, "-u", "_login_worker.py", str(self.client.auth_path)],
cwd=os.path.dirname(__file__),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding='utf-8'
)
qr_found = False
qr_url = ""
start_time = time.time()
timeout = 60 # 60秒超时
# 读取进程输出,寻找二维码URL
while time.time() - start_time < timeout:
line = proc.stdout.readline()
if not line:
break
logger.debug(f"[Login Worker] {line.strip()}")
# 提取二维码URL
if not qr_found:
match = re.search(r'(https://account\.xiaomi\.com/pass/qr/login\?[^\s\'\"]+)', line)
if match:
qr_url = match.group(1).strip()
qr_found = True
yield event.plain_result(f"请使用米家APP扫描以下二维码登录:\n{qr_url}")
# 检查登录是否成功
if "[WORKER_SUCCESS]" in line:
yield event.plain_result("登录成功")
proc.terminate()
return
# 检查登录是否失败
if "[WORKER_ERROR]" in line:
yield event.plain_result(f"登录失败: {line}")
proc.terminate()
return
time.sleep(0.1)
# 超时处理
if not qr_found:
yield event.plain_result("无法获取二维码,请检查网络")
else:
yield event.plain_result("登录超时,请重试")
proc.terminate()
except Exception as e:
logger.error("登录失败: " + str(e))
yield event.plain_result("登录失败: " + str(e))
夜灯控制
夜灯的控制逻辑很简单,为LLM注册一个工具Tool,提供参数与参数说明之后调用api.set_devices_prop()或api.run_action()即可,这里我多封装了一层client,实现原理不变
@filter.llm_tool(name="light_power_control")
async def operate_light(self, event: AstrMessageEvent, on: bool):
'''控制夜灯
Args:
on(bool): 是否开灯,True为开灯,False为关灯
'''
result = self.client.set_device_property(
did="<my-did>",
siid=2,
piid=1,
value=on
)
# 直接返回结果,让模型接收到工具调用的结果
return f"夜灯控制结果: {result}"
注意这里要用return而不是yield,前者是将调用结果返回给LLM,后者是直接将调用结果响应给用户
测试
在微信ClawBot中发送/登录,即可得到二维码,进行认证后就可以通过自然语言下达指令了,LLM会自动决定是否调用控制夜灯的Tool
结合AstrBot的新特性【定时任务】,还可以实现定时开关灯的复杂操作