
1. 前言
杭州的天气真是越来越逆天了,日常半夜30+白天接近40度

尤其是下了班之后回来,屋子里还是30+。房东的垃圾空调当然不支持联网,所以打算折腾一下怎么远程开空调
2. 搞个方案
以前在学校宿舍搞过一个类似的,用arduino接个三极管到红外线发射管,做串口-IR透传。然后又整了个树莓派开了个web,web后端生成红外线编码发到arduino。当时还把空调的红外线编码规则和CRC逆向了一波。然而…找不到了
打工几年之后,现在已经是懒狗了,一行代码都不想写,板子也懒得画的那种(
于是无意间发现了这个好东西

8-9块的价格已经远远优于抛开人力开发成本不谈,购买esp32+红外线管的费用了,甚至连运费都不够。而且它的完成度还相对很高,还带个壳,还送usb线
买了一筐回来

3. CT30W改造与刷机
3.1 拆机
这个小东西长得还挺别致,看起来还有某红点设计奖加持,有点不想拆的太难看。然而事实是这个外壳卡扣非常阴间,不仔细操作或者弄坏几个很难留全尸,至少断一个卡扣
搞断了卡扣交学费之后找到一个相对靠谱的办法

如图,从底部usb接口两侧大概30度的位置翘

红色是usb接口位置,绿色是翘的地方。这样把渣渣清理掉之后最终扣回去也几乎看不出来
3.2 编译esphome固件
拆开以后可以看到里面是一个esp8266,四个红外线发射管,一个红外线接收管。底座还有个配重铁块。八块钱买这一坨确实挺划算

首先需要准备esphome配置文件。这里参考了https://github.com/web1n/ORVIBO-CT30W和其他几个开源项目,做了小改动(主要是方便后面录制红外信号等,还有一些我的其他hass接入设备的使用习惯,如portal)
如果你有多个设备,记得修改第二行的device_name
substitutions:
device_name: ct30w-ir-studio
esphome:
name: $device_name
web_server:
port: 80
version: 3
esp8266:
board: esp_wroom_02
# Enable logging
logger:
level: INFO
# Enable Home Assistant API
api:
encryption:
key: !secret api_encryption_key
services:
- service: send_raw_ir
variables:
raw_data: 'int[]'
freq: float
then:
- remote_transmitter.transmit_raw:
code: !lambda 'return raw_data;'
carrier_frequency: !lambda 'return freq;'
- service: send_nec_ir
variables:
addr: int
cmd: int
cmd_repeats: int
then:
- remote_transmitter.transmit_nec:
address: !lambda 'return addr;' #0x1234
command: !lambda 'return cmd;' #0x78AB
command_repeats: !lambda 'return cmd_repeats;' # 1
- service: send_pronto_ir
variables:
pronto_data_string: string
then:
- remote_transmitter.transmit_pronto:
data: !lambda 'return pronto_data_string;' #
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret hz_iot_wifi_name
password: !secret hz_iot_wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "${device_name} Fallback Hotspot"
password: !secret fallback_ap_password
captive_portal:
status_led:
pin: 15
remote_transmitter:
id: ir_sender
pin: GPIO14
carrier_duty_percent: 50%
remote_receiver:
pin: GPIO5
dump: all
保存为随便什么名字.yaml
然后还需要新建一个secrets.yaml,用于替换掉里面的一些保密信息(比如wifi密码),传github也记得加.gitignore
- 前两个填你的2.4G wifi ssid和密码
- ota_password是升级固件时需要的密码,可以通过web升级
- api_encryption_key是hass后台配置的,可以在这里随机生成一个https://esphome.io/components/api.html
- fallback_ap_password是当你前面配置的wifi连不上时,这个东西会自己开一个热点,名为
"${device_name} Fallback Hotspot"
,密码是这里设置的fallback_ap_password
hz_iot_wifi_name: "your_wifi_ssid"
hz_iot_wifi_password: "your_wifi_password"
ota_password: "your_ota_password"
api_encryption_key: "your_hass_api_key"
fallback_ap_password: "fallback_hotspot_password"
这个需要保存为secrets.yaml
此时目录有这两个文件

装个esphome,compile出来

产物在当前目录下的./.esphome/build/ct30w-ir-studio/.pioenvs/ct30w-ir-studio/firmware.bin
这个位置
ct30w-ir-studio
是yaml第二行配置的device_name
3.3 刷入esphome固件
这个东西自带的固件据说也能用,不过为了接入hass+我不想把iot相关的东西暴露在任何公网可访问的地方,还是需要刷成刚刚编译好的固件
esp8266通过串口isp下载,所以需要先焊串口。如图,只焊tx rx gnd

然后这个esp8266如果要进串口isp,需要先短接gpio0到gnd。那么我们按这个步骤操作
- 镊子短接gpio0和gnd
- 插入usb供电
- 串口插入PC
- 下载固件
ISP短接点(gpio0)是模块左边第三个点,用镊子短接到金属壳或者随便哪个GND

esphome upload xxx.yaml选1,把固件刷进去

刷完固件后重启,等它连上wifi。可以去路由器后台看看它的IP。以及它的web ui

这个web ui还会额外依赖https://oi.esphome.io/v3/www.js
这个资源。看不惯可以yaml里替换成本地的
进来之后长这个样

4. hass接入与配置
4.1 接入hass
添加集成,选esphome

填上一步看到的IP即可,端口默认
API key填3.2步骤里secrets.yaml的api_encryption_key字段
4.2 录制无线信号
到步骤3.3的设备web ui,确保右上角是绿灯,正常串流log
然后拿着空调遥控器对着它按一个键,就会显示收到的信号

搞个脚本转成hex
import sys
import re
print("input esphome web portal output of pronto data, end with Ctrl-D")
print("---------------------------")
input_string = sys.stdin.read()
print("")
print("result")
print("-"*30)
match_pronto_char = re.findall(r"[A-F0-9]{4}", input_string)
print(" ".join(match_pronto_char))
把web ui显示的一坨复制,粘贴到脚本输入,然后ctrl-d
把输出的这坨hex记着

4.3 在hass内调用ct30w发射红外信号
在你的dashboard创建按钮即可

按钮名称随便填,例如空调关闭,空调制冷17度之类的。点击动作设置成对应设备的发送方法。方法参数填4.2章节录制的一长串hex红外信号

最终效果是这样

在外面的话,wireguard回家,hass点个按钮就能开空调了。成本8块,几乎不用写代码,爽到
5. 自动化
可以hass里搞个定时任务,具体看hass文档吧
6. one more thing
能不能不拆?可能可以,但是暂时没空研究

我把固件读了出来,逆向发现是有OTA能力的

大概是开机有一个上报和获取信息逻辑,通过url下载新的固件并自动升级
所以正常走他的app配置,然后http挟持到本地的esphome固件即可
需要看下它是不是https,大概率是不验证证书的因为这货没有RTC电池,没法判断证书过不过期。而且代码里也是ip地址,没找到CA列表
