我们拆卸Redmond G200S水壶协议并将其连接到HomeAssistant

参赛作品


在Gicktime上已经有一篇文章专门解析Redmond SkyKettle水壶的协议。 但是,他们在那里谈论RK-M171S型号,在这里我们将谈论功能更强​​大的G200S。 在此模型中,交互协议已更改,因此,前一篇文章的作者的方法不再起作用,并且出现了夜灯的其他功能以及当前色温的指示。

在本文中,我将通过python代码示例展示协议分析的结果(如果有人想开发用于控制茶壶的模块/应用程序)。 同样在本文的结尾处是指向将茶壶连接到HomeAssistant的现成模块的链接(这是我上完在线课程后第一次用python编写的经验,因此可以甚至需要改进该模块)。

每个有兴趣的人,欢迎猫。

问题与任务


该茶壶有一个很大的缺点(第一篇文章作者所指出的除外):一旦将茶壶从架子上移开,便会重置当前时间,因此不能使用时间表来煮茶壶。 根据该创作者的想法,每次将水壶返回到架子后,您都必须启动其专有的应用程序并将水壶与智能手机同步。 因此,“智能”技术不是在执行常规任务,而是训练我们执行其他操作。 但是,当HomeAssistant出现在房子里时,一切都改变了。 然后,我决定了解协议。

工具


老实说,我尝试反编译并解析原始应用程序,但失败了。 我使用的工具无法让我理解水壶的逻辑。 所有过程和功能都是通过“曲线”(无名(按类型a,b,c等)获得的)获得的。 也许我没有足够的经验和技能。 最后,我以与上一篇文章的作者相同的方式进行操作。 唯一的显着区别是,我使用了gatttool实用程序交互模式。 优点是这种模式消除了第一篇文章的作者所写的各种“竞赛”。

由于HomeAssistant是用python编写的,因此我们将在其上编写所有其他命令。 要在python中使用gatttool交互式操作模式,pexpect库将为我们提供帮助,使您可以生成第三方应用程序的本质并监视其输出(众所周知)。

练习


我将再次将有关交换协议的一般描述的第一篇文章发送给该文章的作者,因此,不费吹灰之力,我们将继续进行控制命令。

  1. 安装与断开

    建立连接:

    child = pexpect.spawn("gatttool -I -t random -b " + mac, ignore_sighup=False) child.expect(r'\[LE\]>', timeout=3) child.sendline("connect") child.expect(r'Connection successful.*\[LE\]>', timeout=3) 

    mac是水壶的罂粟地址。

    我们断开连接:

     child.sendline("exit") 
  2. 订阅通知

    建立连接后,首先,我们需要订阅以接收来自水壶的通知。 没有这个,茶壶将感知命令,但是除了文本“ Successfully”之外,它将无法回答其他任何问题。

     child.sendline("char-write-cmd 0x000c 0100") child.expect(r'\[LE\]>') 
  3. 登入

     child.sendline("char-write-req 0x000e 55" + iter + "ff" + key + "aa") child.expect("value: ") child.expect("\r\n") connectedStr = child.before[0:].decode("utf-8") answer = connectedStr.split()[3] # parse: 00 - no 01 - yes child.expect(r'\[LE\]>') 

    在下文中,iter是从0到64(在十进制中从0到100)的整数迭代十六进制变量。 在每个命令(成功和不成功)之后,应将此变量增加1;当它达到64时,再次将其重置为0;否则将其重置为0。 密钥-十六进制8字节授权密钥(例如:ffffffffffffffffff)。

    答案示例:
    值:55 00 ff 01 aa
    第四个字节(01)表示水壶已授权您,否则答案为00。
  4. 一些街头魔术
    授权后,始终会发送一个“魔术”请求,其本质对我来说并不明确。 有一种理论认为需要“保持”连接状态。 据称,如果不发送,则断开连接会在一秒钟内发生,您需要重新开始。 如果您发送它,则超时将显着增加,最长可达十几秒。 可靠地确认这一点,我不能。

     child.sendline("char-write-req 0x000e 55" + iter + "01aa") child.expect("value: ") child.expect("\r\n") child.expect(r'\[LE\]>') 

    答案示例:
    值:55 01 01 02 1d aa

    在我所有的实验中,答案始终是这样。

    UPD:他们在评论中建议这完全不是魔术,而只是索要软件版本;因此,响应中包含此版本。 因此,通常可以不必要地删除该请求。
  5. 同步处理
    用于将茶壶中的时间与服务器时钟同步的命令。 她还有一种作用。 在水壶中,可以通过使某种颜色的LED闪烁来显示空闲模式下的当前温度。 此功能仅在同步后有效。 有关功能本身的说明,请参见第11段。

     child.sendline("char-write-req 0x000e 55" + iter + "6e" + timeNow + tmz + "0000aa") child.expect("value: ") child.expect("\r\n") child.expect(r'\[LE\]>') 

    这里的tmz是反十六进制格式的时区(例如,将时区+3转换为秒,然后转换为十六进制格式并得到十六进制(3 * 60 * 60)= 2a30,成对拆分并以相反顺序输出302a)。 我不知道如何处理负时区,我还没有测试过,但是有人怀疑下一个tmz字节是造成这种情况的原因。 这里的timeNow是当前的unixtime时间,采用反十六进制格式。 算法是相同的:我们以秒为单位获取当前时间,将其转换为十六进制,将其拆分成对,然后以相反的顺序输出。

    答案示例:
    价值:55 02 6e 00 aa
    在我所有的实验中,答案始终是这样。
  6. 统计资料
    电水壶有一个消耗的电表,总的工作时间和启动次数。 如果某人不需要此数据,则可以安全地跳过此项目。

     child.sendline("char-write-req 0x000e 55" + iter + "4700aa") child.expect("value: ") child.expect("\r\n") statusStr = child.before[0:].decode("utf-8") Watts = hexToDec(str(statusStr.split()[11] + statusStr.split()[10] + statusStr.split()[9])) alltime = round(self._Watts/2200, 1) child.expect(r'\[LE\]>') child.sendline("char-write-req 0x000e 55" + iter + "5000aa") child.expect("value: ") child.expect("\r\n") statusStr = child.before[0:].decode("utf-8") times = hexToDec(str(statusStr.split()[7] + statusStr.split()[6])) child.expect(r'\[LE\]>') 

    瓦数-始终以Wh * h的形式返回消耗的能量,始终-水壶的工作时间,次数-水壶的启动次数。 hexToDec-转换为十进制格式的函数。
  7. 阅读当前的操作模式

     child.sendline("char-write-req 0x000e 55" + iter + "06aa") child.expect("value: ") child.expect("\r\n") statusStr = child.before[0:].decode("utf-8") answer = statusStr.split() status = str(answer[11]) temp = hexToDec(str(answer[8])) mode = str(answer[3]) 

    答案示例:
    值:55 04 06 00 00 00 00 00 01 2a 1e 00 00 00 00 00 00 00 80 00 00 aa
    第四个字节是操作模式(模式):00-沸腾,01-加热至温度,03-小夜灯。 第六字节是在加热模式下必须加热到的十六进制温度,在沸腾模式下为00。第九字节是水的十六进制当前温度(2a = 42摄氏度)。 第十二个字节是茶壶的状态:00-关闭,02-打开。 第十七个字节是水壶达到所需温度后的持续时间,默认为十六进制的80(显然,这些是某种相对单位,肯定不是秒)。
  8. 记录当前的操作模式

     child.sendline("char-write-req 0x000e 55" + iter + "05" + mode + "00" + temp + "00000000000000000000" + howMuchBoil + "0000aa") child.expect("value: ") child.expect("\r\n") statusStr = child.before[0:].decode("utf-8") answer = statusStr.split()[3] child.expect(r'\[LE\]>') 

    参数模式:00-沸腾,01-加热至温度,03-夜灯。 temp参数是在“加热”模式下必须加热到的十六进制温度,在沸腾模式下为00。howMuchBoil参数是达到所需温度后的电水壶持续时间,默认值为80十六进制(显然,这是一些相对单位,当然不是秒)。

    答案示例:
    价值:55 05 05 01 aa
    响应的第四个字节表示设置成功:01-成功,00-不成功。
  9. 运行当前操作模式

     child.sendline("char-write-req 0x000e 55" + iter + "03aa") child.expect("value: ") child.expect("\r\n") statusStr = self.child.before[0:].decode("utf-8") answer = statusStr.split()[3] child.expect(r'\[LE\]>') 

    答案示例:
    价值:55 06 03 01 aa
    响应的第四个字节表示包含成功:01-成功,00-不成功。
  10. 停止当前操作模式

     child.sendline("char-write-req 0x000e 55" + iter + "04aa") child.expect("value: ") child.expect("\r\n") statusStr = self.child.before[0:].decode("utf-8") answer = statusStr.split()[3] child.expect(r'\[LE\]>') 

    答案示例:
    价值:55 07 04 01 aa
    响应的第四个字节表示关闭成功:01-成功,00-不成功。
  11. 闲置时以彩色显示当前温度

     child.sendline("char-write-req 0x000e 55" + iter + "37c8c8" + onoff + "aa") # 00 - off, 01 - on child.expect("value: ") child.expect("\r\n") child.expect(r'\[LE\]>') 

    onoff参数为01启用该功能,或00禁用该功能。

    答案示例:
    价值:55 08 37 00 aa
    在我所有的实验中,答案始终是这样。
  12. 记录各种操作模式的调色板
    在当前温度和加热和煮沸模式的显示模式中设置LED的颜色与温度之间的对应调色板,并且在夜灯模式中设置调色板。

     child.sendline("char-write-req 0x000e 55" + iter + "32" + boilOrLight + scale_from + rand + rgb1 + scale_mid + rand + rgb_mid + scale_to + rand + rgb2 + "aa") child.expect("value: ") child.expect("\r\n") child.expect(r'\[LE\]>') 

    如果设置当前温度的显示模式,则boilOrLight参数为00;如果设置夜间模式,则参数为01。 scale_from参数指示颜色变化范围的开始,在夜光模式下等于00,在当前温度显示模式下等于28(28为十进制格式的40,从该温度开始平滑的颜色变化)。 scale_mid参数位于范围的中间,在夜灯模式下为32,在当前温度显示模式下为46。 scale_to参数指示颜色范围的结束,在两种模式下均为64。 rgb1参数是调色板开头的十六进制颜色。 rgb_mid参数是调色板中间部分的十六进制颜色(我将其计算为左右两端之间的中间部分,但是理论上您可以指定任何颜色,这只会影响颜色变化的美感和平滑度)。 rgb2参数是调色板末端的十六进制颜色。 rand参数是一个特定的参数,我无法完全理解其值,也许与颜色的亮度有关(值示例:e5,cc)。

    答案示例:
    价值:55 09 32 00 aa
    在我所有的实验中,答案始终是这样。
  13. 阅读各种操作模式的调色板

     child.sendline("char-write-req 0x000e 55" + iter + "33" + boilOrLight + "aa") child.expect("value: ") child.expect("\r\n") statusStr = self.child.before[0:].decode("utf-8") child.expect(r'\[LE\]>') 

    如果设置当前温度的显示模式,则boilOrLight参数可以为00;如果设置夜间模式,则参数可以为01。

    答案示例:
    值:55 10 33 01 00 7f 00 00 ff 32 7f 00 ff 00 64 7f ff 00 00 aa
    在这里,第六,第十一和第十六个字节(7f)是项目12中的rand参数。第五个字节是scale_from,第十个字节是scale_mid,第十五个字节是scale_to。 第七+第八+第九字节是rgb_from。 第十二+第十三+第十四字节是rgb_mid。 第十七+第十八+第十九个字节-rgb_to。

结论


如果gatttool不想连接到茶壶(这是您第一次连接到未知设备时可能的情况),请在连接模块之前尝试使用os搜索茶壶:

 sudo hciconfig device reset sudo timeout 1 hcitool lescan 


设备-蓝牙设备的ID(例如hci0)。 确保水壶的罂粟地址在找到的设备列表中。 之后:

 sudo hcitool lewladd mac sudo hcitool lerladd mac 


mac-茶壶的罂粟地址

UPD6 :大大改进了水壶模块:
1.将模块从平台转移到集成模式
2.添加后,您将自动具有3个元素:热水器(当前温度,目标温度,沸腾和加热),传感器(同步时间,耗能,运行小时数,启动次数)和光(可用作夜灯并选择任何颜色)背光)
3.该模块现已在GitHub可用
4.该模块支持通过HACS进行安装
5.配置示例:
 r4s_kettler: device: 'hci0' mac: 'FF:FF:FF:FF:FF:FF' password: 'ffffffffffffffff' 


新版本的屏幕截图
图片
图片
图片
图片


UPD7已删除的不相关信​​息

Source: https://habr.com/ru/post/zh-CN412583/


All Articles