多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python代码控制小米智能插座:从局域网通信到自动化场景实战

Python代码控制小米智能插座:从局域网通信到自动化场景实战 1. 项目缘起为什么需要代码控制智能插座几年前我还在用手机App手动开关家里的加湿器和鱼缸灯直到有一次出差突然降温担心家里的热带鱼才意识到远程、自动控制的重要性。市面上大多数智能家居App的自动化功能都相对基础比如定时开关或者简单的“如果A就B”的联动。但当你想要实现更复杂的逻辑时比如“当室外温度低于10度且室内湿度低于40%时自动开启加湿器并在两小时后关闭”或者“监测到我的电脑进入休眠状态后自动关闭显示器插座电源”App就显得力不从心了。这时代码控制的价值就凸显出来了。通过编程你可以将小米智能插座无缝集成到你的自动化脚本、服务器管理、甚至是量化交易策略的硬件联动中。想象一下你的Python量化交易策略在触发某个条件时不仅能自动下单还能通过代码控制插座开启或关闭连接着多屏显示器的电源实现真正的全自动交易环境管理。或者你的家庭服务器在完成夜间备份任务后自动关闭辅助硬盘的电源以节能。这些场景都超越了普通用户界面所能提供的灵活性。小米智能插座以米家智能插座WiFi版为代表因其性价比高、生态成熟成为了极客和开发者们“折腾”的首选硬件之一。它本身通过米家App和Wi-Fi工作而我们要做的就是绕过App的图形界面直接与插座背后的服务“对话”。这听起来很技术但实际操作起来借助一些成熟的工具和协议门槛并没有想象中那么高。本文将带你从零开始实现用Python代码控制小米智能插座并探讨几种不同的实现路径及其背后的原理。2. 核心原理智能插座如何与代码“对话”在动手写代码之前我们必须先搞清楚小米智能插座以及其他众多小米Wi-Fi设备是如何工作的。这不仅仅是调用一个API那么简单理解其通信机制能帮助我们在遇到问题时快速定位。简单来说当你用米家App添加一个智能插座时会发生以下几件事配网插座进入配网模式手机App将家里的Wi-Fi名称和密码发送给插座。连接云端插座成功连接Wi-Fi后会主动连接到小米的IoT云服务器。App控制此后无论你在家同一局域网还是在外通过App发出的控制指令大多都会先经过小米云端再由云端转发给插座。这是一种典型的“云控”模式。那么我们的代码要如何介入这个过程呢主要有三种思路各有优劣2.1 思路一模拟官方App调用云端API这是最“正统”但也可能最复杂的方法。你需要逆向分析米家App与服务器通信的协议模拟登录、获取设备列表、发送控制指令。这种方法不依赖于本地网络可以真正实现全球任意地点的控制。但缺点也很明显协议可能加密且未公开小米随时可能更改API导致你的代码失效并且涉及到账户安全风险。对于个人项目除非有强烈的远程控制需求且其他方法不可行否则不推荐优先采用此方法。2.2 思路二利用本地局域网通信协议这是目前最流行、最稳定的方法。小米的很多Wi-Fi设备在连接云端的同时也开放了本地局域网LAN控制接口。这意味着只要你的代码运行在和智能插座同一个Wi-Fi网络下的设备上比如你的电脑、树莓派、NAS就可以不经过小米云端直接与插座通信。这带来了几个巨大优势速度快指令在局域网内传输延迟极低。更可靠即使家庭宽带断网局域网内的控制依然有效。隐私性好数据不出家门。协议相对稳定本地协议一旦设备固件确定变化较少。小米设备常用的本地通信协议是Mi Home Protocol一种基于UDP和TCP的自定义协议。幸运的是开源社区已经有人帮我们破解并封装好了这些协议我们不需要从零开始研究二进制数据包。2.3 思路三通过第三方IoT平台桥接如果你使用Home Assistant、OpenHAB等开源家庭自动化平台它们通常已经集成了对小米设备的支持通过插件或内置组件。你可以在这些平台上添加小米插座然后通过平台提供的REST API、MQTT等方式用代码进行二次控制。这种方法相当于增加了一个“中间层”优点是能统一管理不同品牌的设备功能强大缺点是部署和维护整个平台系统相对较重。对于本次“使用代码控制小米智能插座”的目标思路二本地局域网控制在易用性、稳定性和学习价值上取得了最佳平衡也是我们接下来重点讲解的方法。我们将使用一个非常强大的Python库python-miio。3. 实战准备环境搭建与设备发现在开始写控制代码前我们需要做好准备工作。整个过程就像你要和一个人打电话得先知道他的电话号码设备IP和令牌并且准备好能说同一种语言的电话Python环境和库。3.1 硬件与网络环境确认首先确保你的小米智能插座以Zimi Smart Power Strip为例已经按照常规流程通过米家App成功添加并可以正常使用。这意味着它已经连接到了你的家庭Wi-Fi网络并且和你的手机以及即将运行代码的电脑在同一个局域网段内。例如你的电脑IP是192.168.1.100插座的IP应该在192.168.1.x这个范围内。3.2 获取设备的IP地址和通信令牌Token这是最关键的一步。Token相当于设备在局域网内的“密码”任何本地通信都需要用它来认证。获取IP地址最简单的方法是在你的路由器管理后台查看已连接设备列表寻找设备名类似“chu”或“miio”的设备。或者使用米家App在插座的设置 - 设备信息中有时也能看到局域网通信的IP地址。获取Token这稍微麻烦一点因为小米出于安全考虑不会在App里直接显示。有几种方法推荐使用特定版本米家App或插件在安卓设备上安装较老版本的米家App如5.4.x或使用一些大神开发的获取Token的插件可以在设备信息页面直接显示Token。这是最方便的方法。从iOS备份或安卓数据中提取如果你对手机文件系统比较熟悉可以从iOS的iTunes备份或已Root的安卓手机数据目录中找到存储设备Token的数据库文件。使用抓包工具在配网过程中使用抓包工具如Wireshark分析手机与设备的通信数据包从中提取Token。这种方法技术门槛较高。注意Token是一串32位的十六进制字符串如5872656a647a77354d4a6c5954453946。请妥善保管不要泄露。拥有Token和IP就拥有了对该设备的完全控制权。为了方便后续步骤我们假设你已经成功获取了设备IP192.168.1.88设备Token5872656a647a77354d4a6c5954453946此为示例请替换为你自己的3.3 安装必要的Python库我们将使用python-miio库它封装了与小米IoT设备通信的底层细节。打开你的命令行终端CMD、PowerShell或终端使用pip进行安装pip install python-miio这个命令会安装miio库及其依赖。如果安装缓慢可以考虑使用国内镜像源例如pip install python-miio -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后可以在Python环境中导入miio来验证。3.4 使用miio命令行工具发现并验证设备python-miio库自带了一些命令行工具在正式写代码前我们可以先用它们来测试一下通信是否正常。首先使用mirobo工具尝试发现设备注意mirobo最初为机器人吸尘器设计但发现功能通用mirobo --discover等待几秒钟你应该能看到类似下面的输出其中包含了设备的IP地址和Token如果Token未知则显示???INFO:miio.discovery:Discovered 192.168.1.88 with token: 5872656a647a77354d4a6c5954453946这个步骤验证了你的电脑能在网络上发现该设备。如果发现不了请检查防火墙设置确保允许UDP端口54321的通信这是小米设备本地通信的默认端口。4. 核心代码实现从基础控制到状态监控环境准备好后我们就可以开始编写控制代码了。python-miio采用了面向对象的设计为不同类型的设备提供了相应的类。我们需要先确定你插座的具体型号。4.1 确定设备型号与初始化常见的Wi-Fi插座型号有chuangmi.plug.v3、chuangmi.plug.v1等。你可以通过之前发现命令的结果看到型号或者在米家App的设备信息里找到。我们以chuangmi.plug.v3为例。创建一个新的Python文件比如smart_plug_controller.py开始编写代码from miio import ChuangmiPlug # 替换为你的设备信息 PLUG_IP 192.168.1.88 PLUG_TOKEN 5872656a647a77354d4a6c5954453946 # 初始化插座对象 try: plug ChuangmiPlug(ipPLUG_IP, tokenPLUG_TOKEN) print(设备连接成功) except Exception as e: print(f连接设备失败: {e}) exit(1)这里我们导入了ChuangmiPlug类并用IP和Token创建了一个实例。如果初始化失败会抛出异常可能是IP、Token错误或者网络不通。4.2 实现基础开关功能控制插座开关是最基本的需求。ChuangmiPlug对象提供了相应的方法# 打开插座 plug.on() print(插座已开启) # 关闭插座 plug.off() print(插座已关闭) # 切换开关状态如果开着则关如果关着则开 plug.toggle() print(插座状态已切换)这些方法会同步执行并等待设备的响应。你可以将它们封装成函数方便调用。4.3 查询插座状态与功耗信息仅仅控制开关还不够我们常常需要知道插座的当前状态以及它正在为电器提供多少电力。这对于能耗监控、自动化逻辑判断非常重要。# 获取插座状态 status plug.status() print(f插座是否开启: {status.is_on}) print(f插座温度: {status.temperature}°C) # 部分型号支持 print(fWi-Fi信号强度: {status.wifi_led}) # 部分型号显示Wi-Fi LED状态 # 对于支持功率计的插座型号如米家智能插座2可以获取实时功耗 # 注意需要确认你的插座型号支持此功能否则可能报错 try: # 使用plug对象的power属性如果可用或调用特定方法 # 对于ChuangmiPlug我们可能需要使用更通用的miio.Device类来发送原始命令 # 这里演示一种通用查询方式 from miio import Device power_device Device(ipPLUG_IP, tokenPLUG_TOKEN) # 查询功耗的命令因型号而异需要查阅对应型号的协议文档 # 例如对于某些型号命令可能是“get_power” power_info power_device.send(get_power) print(f实时功耗信息: {power_info}) except AttributeError as e: print(f该插座可能不支持功率查询: {e}) except Exception as e: print(f查询功耗失败: {e})实操心得不同型号的小米插座功能差异很大。V1版本可能只有开关功能V3或带USB的版本可能有分控而“米家智能插座2”则内置了功率计量。在编写通用代码时一定要做好异常处理并查阅python-miio的官方文档或对应设备的协议定义。最可靠的方法是先用miio命令行工具miio inspect IP来查看设备支持的所有属性和方法。4.4 封装一个实用的控制类为了代码更清晰和复用我们可以将上述功能封装成一个类class XiaomiSmartPlugController: def __init__(self, ip, token, modelchuangmi.plug.v3): self.ip ip self.token token self.model model self._device None self._connect() def _connect(self): 内部连接方法 try: if plug in self.model: from miio import ChuangmiPlug self._device ChuangmiPlug(ipself.ip, tokenself.token) else: # 对于其他型号使用通用Device类 from miio import Device self._device Device(ipself.ip, tokenself.token) print(f[Success] Connected to {self.model} at {self.ip}) except Exception as e: print(f[Error] Failed to connect: {e}) raise def turn_on(self): if self._device: self._device.on() return True return False def turn_off(self): if self._device: self._device.off() return True return False def get_status(self): if self._device and hasattr(self._device, status): return self._device.status() else: # 尝试通用状态查询 try: # 这是一个示例命令实际命令需根据型号调整 return self._device.send(get_prop, [power]) except: return None def is_on(self): status self.get_status() if status and hasattr(status, is_on): return status.is_on # 尝试从原始响应中解析 # ... (此处可根据实际设备响应格式添加解析逻辑) return None # 使用示例 if __name__ __main__: controller XiaomiSmartPlugController(192.168.1.88, 5872656a647a77354d4a6c5954453946) controller.turn_on() import time time.sleep(2) print(f插座状态: {开启 if controller.is_on() else 关闭}) controller.turn_off()这个类提供了基础的连接、开关和状态查询功能并且根据型号做了简单的适配。在实际项目中你可以根据需求扩展更多方法比如定时任务、功率统计等。5. 进阶应用打造你的自动化场景掌握了基础控制后我们就可以将这些代码片段组合起来嵌入到更复杂的自动化场景中。这才是代码控制的精髓所在——将硬件变成你逻辑流程中的一环。5.1 场景一电脑状态联动插座节能与设备保护很多人习惯让显示器、音箱等外设长期待机。我们可以写一个脚本当电脑进入睡眠或关机时自动关闭连接这些外设的插座。思路在电脑上运行一个后台脚本定期检测系统状态如睡眠、活跃用户。当检测到系统进入睡眠或用户注销时调用turn_off()方法。对于Windows可以使用ctypes或pywin32库检测系统电源状态对于macOS/Linux可以监听特定的系统事件或DBus信号。示例Windows使用psutil检测无用户交互import psutil import time from xiaomi_plug_controller import XiaomiSmartPlugController # 假设上面的类保存在这个文件 def is_computer_inactive(timeout_minutes30): 简单检测电脑是否长时间无用户输入通过检测所有用户会话的闲置时间 for user in psutil.users(): # user.idle 是闲置时间秒 if user.idle timeout_minutes * 60: return False return True def main(): plug XiaomiSmartPlugController(192.168.1.88, your_token_here) check_interval 60 # 每秒检查一次 idle_threshold 10 * 60 # 10分钟无操作视为不活跃 last_active_time time.time() try: while True: current_idle 0 for user in psutil.users(): current_idle max(current_idle, user.idle) if current_idle idle_threshold: # 电脑已不活跃一段时间关闭插座 if plug.is_on(): print(f系统闲置超过{idle_threshold//60}分钟关闭插座。) plug.turn_off() else: # 电脑活跃确保插座开启 if not plug.is_on(): print(系统恢复活跃开启插座。) plug.turn_on() last_active_time time.time() time.sleep(check_interval) except KeyboardInterrupt: print(程序被用户中断) finally: # 程序退出前可以选择将插座置于安全状态 pass if __name__ __main__: main()这个脚本会持续运行监测用户闲置时间。当闲置超过阈值就关闭插座当用户恢复活动就打开插座。你可以把它设置为开机启动的后台服务。5.2 场景二集成到Home Assistant或Node-RED中如果你已经在使用Home AssistantHA这样的家庭自动化平台那么将小米插座接入后用代码控制会更加标准化。HA提供了强大的REST API和WebSocket API。步骤在HA中通过Xiaomi Miio集成或第三方插件添加你的小米插座。在HA中为插座创建一个开关实体例如switch.xiaomi_plug。在Python代码中使用HA的API来控制这个实体。import requests import json HA_URL http://你的HA内网IP:8123 HA_TOKEN 你的HA长期访问令牌 # 在HA用户配置文件中创建 ENTITY_ID switch.xiaomi_plug def call_ha_service(service, entity_id, dataNone): 调用HA服务 url f{HA_URL}/api/services/{service.replace(., /)} headers { Authorization: fBearer {HA_TOKEN}, Content-Type: application/json, } payload {entity_id: entity_id} if data: payload.update(data) response requests.post(url, headersheaders, jsonpayload) return response.json() # 打开插座 call_ha_service(switch.turn_on, ENTITY_ID) print(通过HA API打开插座) # 关闭插座 call_ha_service(switch.turn_off, ENTITY_ID) print(通过HA API关闭插座) # 获取插座状态 state_url f{HA_URL}/api/states/{ENTITY_ID} headers {Authorization: fBearer {HA_TOKEN}} state_info requests.get(state_url, headersheaders).json() print(f插座状态: {state_info.get(state)}) print(f属性: {state_info.get(attributes)})这种方式的好处是你的代码不再直接依赖python-miio库和小米设备的本地协议而是通过HA这个中间层控制逻辑更清晰也更容易与其他HA中的设备如传感器、灯光进行复杂联动。5.3 场景三基于环境传感器的自动化假设你有一个小米温湿度传感器你想实现“当湿度低于30%时自动打开加湿器插座高于50%时关闭”。虽然米家App可以设置简单的自动化但用代码实现可以更灵活地加入条件比如“仅在晚上8点到早上8点之间执行此自动化”或者“如果室内温度同时高于25度则忽略湿度规则”。你需要获取传感器数据同样可以通过python-miio或HA API。编写一个循环或事件驱动的脚本定期读取数据。根据自定义的逻辑规则调用插座的控制方法。# 伪代码逻辑 def humidity_based_control(): plug XiaomiSmartPlugController(...) # 假设有一个函数 get_humidity_from_sensor() 来获取湿度 humidity get_humidity_from_sensor() current_hour datetime.now().hour is_night_time 20 current_hour or current_hour 8 if is_night_time and humidity 30 and not plug.is_on(): plug.turn_on() print(f夜间湿度低({humidity}%)开启加湿器。) elif humidity 50 and plug.is_on(): plug.turn_off() print(f湿度过高({humidity}%)关闭加湿器。) # 可以添加更多条件如温度判断6. 避坑指南与故障排查在实际操作中你几乎一定会遇到一些问题。下面是我在多次实践中总结的常见坑点和解决方法。6.1 连接失败miio.exceptions.DeviceException或超时这是最常见的问题。可能原因1IP地址或Token错误。这是最可能的原因。请仔细核对。Token必须是从当前绑定该插座的米家账户下获取的32位十六进制字符串。可能原因2设备不在同一局域网。确保运行代码的电脑和插座连接的是同一个Wi-Fi网络同一个子网。有时路由器的“访客网络”或“AP隔离”功能会导致设备间无法通信需要关闭。可能原因3防火墙阻止了UDP 54321端口。在电脑的防火墙设置中为Python或你的脚本添加入站/出站规则允许UDP端口54321的通信。在路由器端一般不需要特殊设置。可能原因4设备固件更新导致协议变更。较新的固件可能修改了通信协议。python-miio库的维护者会尽力跟进但可能存在滞后。尝试在GitHub上查看python-miio的Issues看是否有相同型号设备的类似问题。一个临时解决方案是尝试使用miio的通用Device类并手动发送原始命令。排查步骤ping 设备IP确认网络可达。使用mirobo --discover --handshake true尝试握手发现看能否获取到设备信息。使用网络抓包工具如Wireshark过滤UDP端口54321观察代码发送请求时是否有数据包发出以及设备是否有回应。如果没有请求发出是代码或网络问题如果有请求但无回应可能是Token错误或设备问题。6.2 控制无反应但能获取状态有时你能ping通设备也能用status()方法获取状态但on()/off()方法无效。可能原因设备型号与类不匹配。你使用的设备类如ChuangmiPlug可能不完全兼容你的插座型号。尝试使用通用的miio.Device类。from miio import Device plug Device(ipPLUG_IP, tokenPLUG_TOKEN) # 尝试发送原始开关命令。命令格式需要查阅设备协议。 # 对于很多插座“power”属性控制开关。 plug.send(set_power, [on]) # 或 [off] plug.send(set_power, [off])如何查找正确的命令使用miio inspect IP --token TOKEN命令。这会列出设备支持的所有方法methods和属性properties。在输出中寻找与电源控制相关的方法如set_power,toggle_plug, 或者属性power然后用set_prop命令修改。6.3 代码在Docker容器或虚拟机中无法运行如果你的代码运行在Docker容器或虚拟机里。问题容器/虚拟机有独立的网络命名空间可能无法直接广播发现设备或者IP地址与主机不在同一网段。解决方案对于Docker使用--networkhost模式运行容器让容器共享宿主机的网络栈。这是最简单的方法。例如docker run --networkhost your-python-script-image。对于虚拟机将网络连接模式设置为“桥接模式”Bridged这样虚拟机会从主路由获取一个同网段的IP就像一台独立的物理机。6.4 设备偶尔掉线或无响应可能原因1Wi-Fi信号不稳定。智能插座通常放在墙角或插在排插深处信号可能较差。尝试调整插座位置或使用Wi-Fi中继器。可能原因2路由器连接设备过多。低端路由器处理大量并发连接的能力有限可能导致IoT设备被挤掉线。考虑优化网络或升级路由器。代码层面的容错在生产环境中你的控制代码必须加入重试机制和异常处理。import time from miio.exceptions import DeviceException def robust_turn_on(device, retries3, delay2): for i in range(retries): try: device.on() return True except DeviceException as e: print(f尝试 {i1}/{retries} 失败: {e}) if i retries - 1: time.sleep(delay) print(所有重试均失败) return False7. 安全考量与最佳实践将家用设备接入代码控制安全是重中之重。以下几点需要牢记Token就是密码你的设备Token一旦泄露他人就可以在同一个局域网内控制你的设备。绝对不要将Token硬编码在提交到公开Git仓库的代码中。应该使用环境变量、配置文件.gitignore排除或密钥管理服务来存储。# 使用环境变量 import os PLUG_TOKEN os.environ.get(MI_PLUG_TOKEN) if not PLUG_TOKEN: raise ValueError(请设置环境变量 MI_PLUG_TOKEN) # 或使用配置文件 config.json (加入.gitignore) import json with open(config.json, r) as f: config json.load(f) PLUG_TOKEN config[plug_token]控制权限最小化如果你的代码需要对外提供API比如做一个Web界面来控制插座务必做好身份验证和授权。不要开放无需认证的控制端口到公网。网络隔离可以考虑将IoT设备划分到一个独立的VLAN或子网中限制它们与主网络内其他设备如存有重要数据的电脑之间的通信。这需要路由器支持相关功能。固件更新谨慎非官方的破解或刷机固件可能带来安全风险或失去保修。除非你非常清楚自己在做什么否则建议保持官方固件。关注官方固件更新日志有时新固件会修复安全漏洞。代码健壮性如前所述添加重试、超时、异常处理和日志记录。确保自动化脚本不会因为网络波动而陷入死循环或产生不可预知的行为。例如在基于传感器数据的自动化中设置合理的轮询间隔避免过于频繁的请求对设备造成负担。通过以上七个部分的拆解我们从动机、原理、环境搭建、代码实现、场景应用到排错安全完成了一次对“代码控制小米智能插座”的深度探索。这不仅仅是一个简单的开关控制它打开了一扇门让你能够将物理世界的设备融入数字逻辑流中创造出真正个性化、智能化的生活和工作环境。
返回列表