我给米家写了个PC控制面板
一个媒体人的智能家居开发手记
写在前面
这不是一篇技术教程,更像是一段折腾经历的记录。影图空间-https://www.yingtux.cn/hobbies/xiezuo/8059.html
作为一个媒体人,视频制作是我的本职工作——策划、编导、拍摄、剪辑、航拍、直播导播,这些都能拿得出手。但骨子里,我对技术有种执念。年轻时候没赶上程序员的好时代,如今有了闲暇,便想着把当年欠下的"技术债"补回来。
学编程这事儿,说难不难,说简单也不简单。难的是坚持,简单的是资源丰富。网上有无数教程、开源项目、技术社区,只要你愿意动手,总能找到答案。我这个人做事严谨不含糊,既然决定学,就得认真对待。
家里的小米智能设备越来越多,灯泡、空调、传感器、音箱……米家APP用得挺顺手,界面简洁,操作直观。但每次想控制设备都得掏手机,扫码、解锁、打开APP、找到设备、调整参数……一整套流程下来,有时候电脑前正在写东西,思路就被打断了。我在想,能不能用电脑直接控制?桌面端有没有现成的方案?
一搜,还真有开源项目——mijiaAPI,Python写的,能调用米家的各种接口。虽然官方没有提供PC端控制方案,但社区的力量是无穷的。有人逆向分析了米家的通信协议,封装成了这个库,支持获取设备列表、控制设备属性、执行智能场景等功能。
那就干吧。
第一关:扫码登录
mijiaAPI支持两种登录方式:账号密码和扫码。账号密码登录?安全性存疑,而且小米的登录机制复杂,稍有不慎就会被风控,账号安全也是问题。扫码登录呢?直观、安全,米家APP一扫就行,和网页登录体验一致。
但问题来了:如何在Python程序里显示二维码?
最初我想自己生成二维码图片,用qrcode库把登录URL编码成二维码,显示在tkinter窗口里。代码写得挺顺,二维码也生成了,但米家APP扫描后毫无反应。我以为图片尺寸不对,调大了调小了,都不行。又怀疑是二维码版本问题,各种参数都试了一遍,依然无效。
查了半天资料才明白——米家的二维码必须从服务器获取,里面有动态生成的ticket和timestamp,还有签名信息。自己造的二维码,没有这些关键参数,小米服务器不认。这就像你拿着自己画的门禁卡去刷闸机,格式再标准也进不去。
米家PC端智能家居控制面板
解决方案是用requests直接从小米服务器下载二维码图片,再用PIL(Python Imaging Library)显示在tkinter窗口里。二维码URL是mijiaAPI在登录过程中输出的,格式类似:
https://account.xiaomi.com/pass/qr/login?ticket=xxx&dc=xxx&sid=mijia&_qrsize=240&_hasLogo=false&ts=xxx
这个URL是小米官方生成的,包含了所有必要的安全参数。用requests下载图片,转成PIL格式,再转成tkinter能识别的PhotoImage,就能在窗口里显示了。核心代码如下:
# 下载二维码图片 resp = requests.get(qr_url, timeout=10) img_data = io.BytesIO(resp.content) img = Image.open(img_data) img = img.resize((260, 260)) photo = ImageTk.PhotoImage(img) qr_label.config(image=photo, text="") qr_label.image = photo # 保持引用
登录是解决了,但每次启动都要扫码也很烦。有时候只是想调一下灯光亮度,结果要先掏手机扫码,等登录完成才能操作,体验很割裂。我又翻了翻mijiaAPI的源码,发现它有个缓存机制:登录成功后,认证token会保存在本地文件里,路径是`~/.config/mijia-api/auth.json`。
于是我优化了登录流程:启动时先检测这个缓存文件,如果存在就尝试自动登录。缓存有效,秒进主界面;缓存失效或不存在,再弹扫码窗口。这样用户只需要第一次扫码登录,后续启动都是自动的,体验顺滑多了。
# 检查登录缓存
auth_cache_path = os.path.expanduser('~/.config/mijia-api/auth.json')
if os.path.exists(auth_cache_path):
try:
api = mijiaAPI()
api.login() # 自动使用缓存
homes = api.get_homes_list() # 验证是否有效
if homes:
return # 自动登录成功
except:
pass # 缓存失效,弹出扫码窗口
第二关:设备控制
登录成功了,接下来是设备控制。我家里设备不少:蓝牙Mesh灯泡、WiFi床头灯、空调、温湿度传感器、人体传感器、智能音箱、网关……型号各异,控制方式也大不相同。
首先是蓝牙Mesh灯泡,这是家里最多的设备类型,客厅、卧室、餐厅、厨房都有。它的亮度范围是1-65535,这是协议层面的原始值,但用户习惯的是1-100百分比。想象一下,用户想设置50%亮度,你让他输入32767,这体验太反人类了。所以要做一个转换:
# 用户设置50%,实际发送 (50/100) * 65535 ≈ 32767 brightness_value = int(brightness_percent / 100 * 65535)
色温控制也类似,蓝牙Mesh灯泡的色温范围是2700K-6500K,但用户更习惯用"暖-冷"的直观感受来调节。我在界面上做了一个渐变滑块,从橙色(暖光)到蓝色(冷光),用户拖动滑块就能调节,后台自动换算成具体的色温值。
WiFi床头灯更厉害,yeelink.light.bslamp2型号,支持1677万色。这个灯泡在米家APP里有一个彩色圆盘,用户可以选择任意颜色。在PC端实现类似功能,我用了HSV颜色模型:H是色相(0-360度),对应彩虹光谱上的颜色;S是饱和度;V是明度。用户只需要拖动色相滑块,就能选择颜色。
但设备通信用的是RGB值,而且是一个奇怪的十进制数值。研究了一下协议,发现转换公式是:
# HSV转RGB R, G, B = colorsys.hsv_to_rgb(h/360, 1.0, 1.0) R, G, B = int(R*255), int(G*255), int(B*255) # RGB转设备识别的颜色值 color_value = R * 65536 + G * 256 + B
空调控制相对简单,电源开关、运行模式(制冷/制热/自动/除湿/送风)、目标温度,几个参数搞定。但不同设备的属性ID(siid、piid)都不一样,得一个一个查设备规范文档,试参数。米家的设备规范是公开的,但文档分散,有时候需要看源码里的常量定义。
最坑的是tkinter的Canvas渐变滑块。tkinter原生的Scale控件不支持渐变背景,只能设置单一颜色。我想要一个从青色到白色的亮度滑块,让用户直观感受"从暗到亮"的变化,但原生控件做不到。
解决方案是用Canvas逐行画矩形。比如滑块高度200像素,就画200条线,每条线的颜色从青色渐变到白色。颜色插值用RGB分量线性计算:
def draw_gradient(canvas, height, width):
for i in range(height):
ratio = i / height # 计算渐变比例
# 从青色(0, 180, 155)渐变到白色(255, 255, 255)
r = int(0 + (255 - 0) * ratio)
g = int(180 + (255 - 180) * ratio)
b = int(155 + (255 - 155) * ratio)
color = f'#{r:02x}{g:02x}{b:02x}'
canvas.create_line(0, i, width, i, fill=color)
这还没完,tkinter有个渲染时序问题:控件还没完全显示,Canvas的尺寸是错的(可能只有1像素高),画出来的渐变就变形。解决方法是用`root.after(50, draw_gradient)`延迟执行,等控件渲染完成再画渐变。这个坑坑了我半天,各种调试才发现是时序问题。
第三关:智能场景
某天突发奇想:既然能控制单个设备,能不能一键执行预设场景?比如"回到家"自动开灯开空调,"离开家"一键关全屋设备。这样就不需要逐个设备操作,一个按钮解决所有问题。
查了mijiaAPI的文档,它支持获取场景列表和执行场景,但不支持创建场景。场景得在米家APP里预先配置好。这倒也合理——场景涉及设备联动、触发条件(比如人体传感器检测到移动)、延时操作,在手机上拖拽配置更直观,PC端做配置界面反而复杂。
我只需要把场景执行功能搬到PC端就行。但第一次实现时,执行场景报错了:`invalid character '\\' looking for beginning of value`。这是一个JSON解析错误,说明API返回的数据格式有问题。
调试半天,发现mijiaAPI的`run_scene`方法直接传场景名称时,内部处理有问题。正确做法是先获取场景列表,找到对应的场景ID,再用ID执行:
# 获取场景列表
scenes = api.get_scenes_list(home_id=home_id)
# 找到目标场景的ID
for scene in scenes:
if scene.get('name') == scene_name:
scene_id = scene.get('scene_id') # 注意是scene_id不是id
break
# 用ID执行场景
api.run_scene(scene_id=scene_id, home_id=home_id)
这里还有个小坑:场景数据结构里,ID字段叫`scene_id`不是`id`。我一开始用`scene.get('id')`,取到的是None。打印完整数据才发现字段名不对。这个小坑爬出来后,三个场景按钮——回到家、离开家、全屋晚安——终于能正常工作了。
现在用户点击"回到家"按钮,程序会自动找到这个场景的ID,调用API执行,米家APP里配置的联动动作就会触发:客厅灯打开、空调调到26度、音箱播放欢迎语。一键搞定,不用逐个设备操作。
UI的打磨
功能做完了,但界面不能太丑。作为一个有审美追求的人,界面设计不能马虎。
我选了米家LOGO的标准色:#00b49b作为主色调。这个青绿色很舒服,有点"科技感"又不失温度,和米家的品牌调性一致。白色背景配青绿色点缀,整体简洁明快。
顶部标题栏用米家绿背景,白色文字。加了个米家logo图标,标题改成"米家PC端智能家居控制面板",字号24pt加粗,醒目但不张扬。右上角的状态栏用图标区分连接状态:
❌ 小米云:未连接(红色文字)
🌐 小米云:已连接(亮黄色文字)
红黄对比,状态一目了然。红色表示警告,黄色表示正常,符合用户的视觉直觉。图标的使用也让状态更加直观,不用看文字就能知道当前连接情况。
智能场景的三个按钮,我用米家绿背景白字,无边框设计,扁平风格。放在欢迎图片下方,居中排列。按钮之间留有适当间距,点击时有颜色反馈(按下变深绿)。整体风格和米家APP保持一致,用户上手没有学习成本。
左侧设备列表采用树形结构,按房间分组显示。每个房间有自己的图标:卧室🛏、餐厅🍽、客厅🛋、厨房🍳、卫生间🚿、阳台🌅。设备也有对应图标:灯泡💡、空调🧊、传感器📡、音箱🔊。在线状态用绿色对勾和红色叉号区分,一目了然。
代码打包
功能完善后,我用PyInstaller打包成exe,方便分享给朋友。打包这事儿看似简单,实际有不少坑。
打包命令一长串:
pyinstaller -D -w \ --add-data "mijia-api-main;mijia-api-main" \ --icon=icon.ico \ --add-data "icon.ico;." \ --add-data "images;images" \ --hidden-import=tzlocal \ --hidden-import=tzdata \ --hidden-import=pytz \ --hidden-import=Crypto \ --hidden-import=Crypto.Cipher \ --hidden-import=Crypto.Cipher.ARC4 \ --hidden-import=charset_normalizer \ --hidden-import=chardet \ mijia_gui_v4.py
为什么要这么多`--hidden-import`?因为PyInstaller检测不到动态导入的模块。mijiaAPI依赖的这些库,在代码里没有显式import,是运行时按需加载的。不显式声明,打包后的exe运行时会报"ModuleNotFoundError"。
`-w`参数是隐藏控制台窗口,让程序看起来更像一个正经的桌面应用。但加了这个参数后,`sys.stdout`和`sys.stderr`会变成None。mijiaAPI内部有个logger模块,调用了`isatty()`方法判断输出是否是终端,结果None对象没有这个方法,直接报错。
解决方案是在导入mijiaAPI之前,创建假的stdout/stderr对象:
if sys.stdout is None:
class DummyStdout:
def write(self, *args, **kwargs): pass
def flush(self, *args, **kwargs): pass
def isatty(self): return False
sys.stdout = DummyStdout()
这个小技巧解决了打包后的兼容性问题。程序运行时不会输出任何日志到控制台(因为根本没有控制台),但也不会因为缺少stdout而崩溃。
写在最后
这个控制面板,断断续续开发了一周多。
期间遇到无数问题:二维码识别不了、设备属性不对、渐变滑块变形、打包后缺少依赖、线程安全问题、窗口关闭后回调报错……每一个都得查资料、看源码、反复调试。有时候一个问题卡大半天,最后发现是个小细节,比如字段名写错了、时序不对、编码格式问题。
但解决问题后的那种成就感,是其他事情给不了的。当你看到自己写的代码跑起来,灯光按照预期变化,场景一键执行成功,那种满足感无法言喻。
这个控制面板现在还在用,偶尔有朋友来家里,看到电脑能直接控制全屋设备,都觉得新鲜。我说"开源的,你想要我打包一个给你",他们更惊讶了——原来自己做软件没那么难。
难的是什么?是迈出第一步的勇气,是遇到问题不退缩的耐心,是不断学习不断折腾的劲头。
我喜欢蓝色、青靛、墨绿色,爱好摄影、文学、钻研IT技术。对朗读、朗诵有一定水平,向往去祖国的大西北、西南、新疆、西藏等地体验不一样的人文与风情。座右铭是:人的一生是学习的一生。这些看似分散的兴趣,其实都指向同一个方向——对未知世界的好奇,对美好生活的追求。
技术更新换代这么快,保持学习能力和好奇心,才不会和时代脱节。代码还在GitHub上,有兴趣的可以自己搜mijiaAPI。我的这个小工具,不过是站在巨人肩膀上的一次小小尝试。
但正是这些小小的尝试,让生活变得更有趣。
📌 技术栈
Python + tkinter + mijiaAPI + PyInstall






