用HAP-NodeJS编写一个能原生接入苹果生态的配件
最近又冒出折腾智能家居的想法了,虽然前段时间基于ESP32-C3和MQTT协议搭建了一个可以通过webui控制的灯,让我过上了躺在被窝里就能关掉灯的幸福生活,但总觉得webui往后去是个麻烦事,数据要流经公网、维护也成本高,最重要的是,现有的所有家具配件都得自己去实现,这太不优雅了。
经过一段时间的探索,发现了HAP-NodeJS这个库:
Github地址:HAP-NodeJS
HomeKit Accessory Protocol (HAP) 的 Node.js 实现
非常好啊!用的也是我最爱的Node.js,不用学习新语言,就能实现编写苹果原生配件了!
接下来让我们看看这个库是怎么用的,并尝试编写一个能够接入苹果家庭的支持调节冷暖光和亮度的配件吧!
首先了解一些基础概念:
- Accessory(配件) 配件是实际物理设备的表示。配件由多个服务组成。
- Service(服务) 服务是将特定设备类型的功能进行分组的方式。常见的服务有
开关。灯泡或插座。服务由多个必需和可选特性组成。HAP 规范中提供了多个 Apple 预定义服务。Apple 预定义服务是唯一可通过 Home 应用程序或使用 Siri 来控制的服务。 - Characteristic(特性) 特性是实际的控制点,用于与服务的功能交互。例如,
Switch服务需要的特性是On。特性可以读取或写入(此外,它们还可以发送更新,称为事件)。特性定义了其值的特定格式。On特性定义了bool的格式,这意味着它可以接受true或false的值,表示打开或关闭。On特征不仅用于Switch服务,还用于例如Outlet服务。它基本上用于任何可以打开或关闭的服务。
现在开始编写代码:
-
在Node.js中安装
hap-nodejs包。 -
创建一个index.ts文件,并一次性引入所有需要的内容
typescriptimport { CharacteristicEventTypes, Service, Characteristic, Accessory, uuid, Categories } from 'hap-nodejs'; -
创建一个uuid,再根据uuid创建一个配件
typescriptconst accessoryUuid = uuid.generate("lumentune.hap.tonesc.cn"); const accessory = new Accessory("LumenTune", accessoryUuid);其中,
uuid.generate表示根据传入的字符串创建一个唯一的uuid,然后通过new Accessory来创建一个配件,其中LumenTune是我给该配件取得名字,可以调整成自己喜欢的。 -
创建一个Lightbulb(灯泡)服务
typescriptconst lightService = new Service.Lightbulb("LumenTune"); -
接下来为创建的服务,依次添加特性
typescript// 'On' 特征是灯光服务所必需的 const onCharacteristic = lightService.getCharacteristic(Characteristic.On); // 'Brightness' 特征是灯光服务的可选项;'getCharacteristic' 会自动将其添加到服务中 const brightnessCharacteristic = lightService.getCharacteristic(Characteristic.Brightness); // 'ColorTemperature' 特征用于调整色温 const colorTemperatureCharacteristic = lightService.getCharacteristic(Characteristic.ColorTemperature);接下来,依次为这三个服务注册事件
-
onCharacteristic
typescriptonCharacteristic.on(CharacteristicEventTypes.GET, (callback) => { // ... 添加自定义逻辑 callback(HAPStatus.SUCCESS, /* 替换成对应的逻辑 */); }); onCharacteristic.on(CharacteristicEventTypes.SET, (value, callback) => { // ... 添加自定义逻辑 callback(); });-
CharacteristicEventTypes.GET
表示使用者在请求获取该配件的开启还是关闭状态。callback调用,传递两个参数,第一个建议传入HAPStatus,如果成功,可以返回
HAPStatus.SUCCESS;第二个传入响应值,true表示灯处于打开、false表示关闭。 -
CharacteristicEventTypes.SET
使用者请求设置该配件的开关状态。value为请求设置的新状态,可以转换成boolean再进一步处理。如果设置成功,则直接调用callback即可,也可以传入第一个参数为
HAPStatus.SUCCESS。
-
-
brightnessCharacteristic
typescriptbrightnessCharacteristic.on(CharacteristicEventTypes.GET, (callback) => { // ... 添加自定义逻辑 callback(HAPStatus.SUCCESS, /* 替换成对应的逻辑 */); }); brightnessCharacteristic.on(CharacteristicEventTypes.SET, (value, callback) => { // ... 添加自定义逻辑 callback(); });-
CharacteristicEventTypes.GET
同上,callback第二个值,表示当前的亮度,值在0~100。
-
CharacteristicEventTypes.SET
同样的,value表示应该设置的新亮度,值在0~100。
-
-
colorTemperatureCharacteristic
typescriptcolorTemperatureCharacteristic.setProps({ minValue: 118, // 对应 8500K maxValue: 400, // 对应 2500K minStep: 1 }); colorTemperatureCharacteristic.on(CharacteristicEventTypes.GET, (callback) => { // ... 添加自定义逻辑 callback(HAPStatus.SUCCESS, /* 替换成对应的逻辑 */); }); colorTemperatureCharacteristic.on(CharacteristicEventTypes.SET, (value, callback) => { // ... 添加自定义逻辑 callback(); });色温值为 Mireds(微倒度),不是 Kelvin(开尔文)。
色温属性相比其他的多个步骤,需要先调用
setProps设置色温特征的属性,一般标准范围在140 ~ 500,140 约 7143K,偏蓝;500 约 2000K,偏暖;剩下的就和上面类似了。
假如你说,TONE TONE,怎么上面的callback全是成功,我如果要返回一个失败怎么办?
好办,太好办了,看看HAPStatus,为我们提供了丰富的状态码,我们可以打开源码:
typescriptexport declare const enum HAPStatus { // 请求成功。 SUCCESS = 0, // 由于权限不足,请求被拒绝。 INSUFFICIENT_PRIVILEGES = -70401, // 由于与特征值的通信失败,操作失败。 SERVICE_COMMUNICATION_FAILURE = -70402, // 资源繁忙。请稍后重试。 RESOURCE_BUSY = -70403, // 无法写入只读特征(未定义 {@link Perms.PAIRED_WRITE})。 READ_ONLY_CHARACTERISTIC = -70404, // 无法读取只写特征(未定义 {@link Perms.PAIRED_READ})。 WRITE_ONLY_CHARACTERISTIC = -70405, // 请求的特征不支持事件通知(未定义 {@link Perms.NOTIFY})。 NOTIFICATION_NOT_SUPPORTED = -70406, // 设备资源不足,无法处理请求。 OUT_OF_RESOURCE = -70407, // 操作超时。 OPERATION_TIMED_OUT = -70408, // 指定的资源不存在。 RESOURCE_DOES_NOT_EXIST = -70409, // 针对该特征的请求中收到了无效的值。 INVALID_VALUE_IN_REQUEST = -70410, // 授权不足。 INSUFFICIENT_AUTHORIZATION = -70411, // 当前状态下不允许执行此操作。 NOT_ALLOWED_IN_CURRENT_STATE = -70412 }根据需求选择对应的状态返回就可以了
-
-
将服务添加到配件
typescriptaccessory.addService(lightService); -
发布配件,
publish方法应该放到最后一步实现,该方法调用后返回一个Promise,表示是否发布成功。typescriptaccessory.publish({ username: "17:51:07:F4:BC:8A",// MAC 地址格式,配件和该名称绑定 pincode: "500-60-700", // 配对密码,按需修改 port: 47129,// 监听端口 category: Categories.LIGHTBULB, // 配对屏幕中显示的符号 // ... 还有一些其他可选的配置,可根据文档进行配置 }).then(() => { console.log("Accessory setup finished!"); });如果需要销毁,可以调用
accessory.destroy()方法
编写完成后,就可以运行代码进行测试了。配件成功发布后,就可以打开苹果家庭添加配件了,添加配件的设备需要和发布配件的设备在同一个局域网下,因为是通过mDNS进行配件发现的。如果你使用手机进行设备添加,且进入后是扫描添加模式,此时可以选择更多选项,应该就可以发现该配件了,再输入程序中设置的配置密码,就完成配件添加了。接下来就可以享受原生配件提供的便利了。
另外,如果你需要在公网进行访问家庭网络中的配件,可以考虑在家里放一台符合要求的homepod、ipad、apple TV。这些设备可以自动进行配件的“DDNS”功能,以支持公网访问。