Jul 5

Homekit初探

Lrdcq , 2017/07/05 17:32 , 程序 , 閱讀(5992) , Via 本站原創
Homekit是啥,它是苹果公司发起的一个物联网智能家居平台,支持Homekit的智能家居可以通过苹果的设备进行控制和统一管理。当然,世界上并不缺乏类似的智能家居中枢,小米的全家桶,飞利浦的Hue都是这样的东西。但是不一样的是,它们大多数是app或者rom级的中枢,而苹果提供的是手机系统级的,当ios10第一次把“家庭”这个系统应用介绍给用户时,也就把自家的Homekit同时铺到了千家万户。
而更好的消息是,在Homekit核心,也就是智能硬件开发上,从今年wwdc开始,苹果正式对普通开发者开放了Homekit的硬件开发文档(虽然并没有sdk或者现成的代码),在这里可以下载到6月刚出炉的新鲜的HomeKit Accessory Protocol Specification的pdf。因此,现在切入Homekit是一个绝佳的机会。

原理

当然,Homekit的原理在两年前刚刚出现的时候就被geeker们逆向得八九不离十了,而现在有了文档,我们可以更清晰的梳理这个过程。

硬件

物联网或者智能家居,其实本质上做的就是硬件与硬件之间的通信,因此最关键的设计无外乎硬件之间的通信协议。苹果为Homekit设计的通信协议叫做HomeKit Accessory Protocol简称HAP,虽然这么叫了,但是实际根据通信方式的不同,这个协议分为两部分:

互联网通信(IP):直接通过网线或者wifi进行通信,在一个子网中的设备均可以被搜索到。实际通信使用的http协议和加密过的json数据进行通信的。

低能耗蓝牙(BLE):通过低能耗蓝牙进行通信,真正的点对点通信,通信协议是完全自主设计的真正的HAP。

可以看到苹果大陆商城上出售的homekit设备,绝大部分轻量级设备,比如那个七巧板灯各种传感器智能插座,很明显都是通过BLE通信的,而感觉中枢性质的东西如hue bridge和传输数据量较大的比如那个摄像头,则是通过wifi通信的。由于互联网通信的硬件成本和用户使用复杂度明显高太多了,很明显没必要的话大家都不太喜欢用互联网而喜欢蓝牙通信。

软件

至于通信的协议传输的内容,就是软件层面的事情了。在设备上,苹果将每一个设备抽象为一个Accessory对象。每一个设备提供复数个服务(Service),而每一个服务提供复数个特性(Characteristic),每个特性有一个固定的类型id和一个参数,实际设备之间的通信就是通过这个参数传递的。

举个栗子,每一个设备都需要提供一个AccessoryInformation的服务,它的特性包括Name,Manufacturer,Model,SerialNumber之类的,这些特性的value都是只读的,即只可以从设备读取。再举一个栗子是我们的灯泡类型设备需要提供Lightbulb类型的服务,它包括的特性比如On表示开关,Brightness表述亮度,这两个特性都是可读可写的。

那Homekit是怎么使用这些信息的呢。Homekit找到一个设备时并不知道设备到底是个啥,只能从AccessoryInformation中读取到设备的基本信息。这时如果发现设备提供了Lightbulb服务,则说明这个设备提供灯泡的功能,这个服务里面有的特性则说明这个设备支持的特性,比如一个灯泡有Brightness但是没有On,这说明这个灯泡可以通过Homekit调整亮度,但不能通过Homekit进行开关。这本来就是应该由硬件决定的事情。当然,一个设备可以提供多种服务,毕竟现在高科技玩意儿花样多,一个设备同时是个摄像头也是一个音乐播放器也不是不可能嘛~。

Accessory -> Service -> Characteristic

點擊在新視窗中瀏覽此圖片

在硬件设备之上,则是iOS从设备管理层面的软件封装,也是多层的。每一个用户对应多个home,每一个home对应多个room,而每一个room对应多个Accessory或者AccessoryGroup。非常简单明了的抽象。

User -> Home -> Room -> Accessory

而这一层抽象,可以非常直观的从ios10自带的“家庭”应用中看到。

其它玩意儿

Homekit还有一个关键功能是动作集和触发器,也就是家庭应用的tab页第三部分自动化。通过设定一定的条件,我们可以把不同设备的行为非常方便的串联在一起。比如以下场景:

卧室 时间>晚上8:00<早上6:00 -> 开启空调
厕所 动作传感器反馈 -> 开启灯泡
客厅 空气质量检测100 -> 开启空气净化器

这种逻辑通过一个ios中枢就可以实现。当然,这需要一个在家庭中常驻的设备作为家庭中枢,官方推荐的是一个ipad或者一个apple tv,或者最新出的Homepod,而我们吃瓜群众希望的是哪天苹果推出一个智能路由器就最好不过了。

硬件

搞一个Homekit的硬件是一个很简单的事情,除了官网上的商城,淘宝上也有很多。当然,太贵了,反正现在苹果也开放了普通开发者的硬件开发权限,我们可以自己搭建用于测试的Homekit硬件。

如上文硬件有两种,通过蓝牙的HAP似乎非常困难,在github上只有一个开放前逆向工程依葫芦画瓢的实现(https://github.com/aanon4/HomeKit),现在已经不能使用了。而通过互联网访问的,有一个超级火的node实现(https://github.com/KhaosT/HAP-NodeJS),晚上不少通过arm开发板或者树莓派做bridge桥接小米什么的Homekit非智能硬件的都是通过它来实现的。因此这里,我们直接使用这个库进行硬件模拟。

建立服务

结合上文提到的原理和概念,下面这段模拟了一个彩色灯泡的代码应该就非常清晰了:
var LightController = {
    name: "XGFE Test Light", 
    manufacturer: "HAP-NodeJS", 
    model: "v1.0",
    serialNumber: "A12S345KGB",

    power: false,
    brightness: 100,
    hue: 0,
    saturation: 0
};

var lightUUID = uuid.generate('hap-nodejs:accessories:light' + LightController.name);

var lightAccessory = new Accessory(LightController.name, lightUUID);

lightAccessory
    .getService(Service.AccessoryInformation)
    .setCharacteristic(Characteristic.Manufacturer, LightController.manufacturer)
    .setCharacteristic(Characteristic.Model, LightController.model)
    .setCharacteristic(Characteristic.SerialNumber, LightController.serialNumber);

lightAccessory.on('identify', function (paired, callback) {
    callback();
});

lightAccessory
    .addService(Service.Lightbulb, LightController.name)
    .getCharacteristic(Characteristic.On)
    .on('set', function (value, callback) {
        LightController.power = value;
        callback();
    })
    .on('get', function (callback) {
        callback(null, LightController.power);
    });

lightAccessory
    .getService(Service.Lightbulb)
    .addCharacteristic(Characteristic.Brightness)
    .on('set', function (value, callback) {
        LightController.brightness = value;
        callback();
    })
    .on('get', function (callback) {
        callback(null, LightController.brightness);
    });

lightAccessory
    .getService(Service.Lightbulb)
    .addCharacteristic(Characteristic.Saturation)
    .on('set', function (value, callback) {
        LightController.saturation = value;
        callback();
    })
    .on('get', function (callback) {
        callback(null, LightController.saturation);
    });

lightAccessory
    .getService(Service.Lightbulb)
    .addCharacteristic(Characteristic.Hue)
    .on('set', function (value, callback) {
        LightController.hue = value;
        callback();
    })
    .on('get', function (callback) {
        callback(null, LightController.hue);
    });

lightAccessory.publish({
    username: "FA:3C:ED:5A:1A:1A",
    port: 51062,
    pincode: "031-45-154",
    category: Accessory.Categories.LIGHTBULB
}, true);

除了原理部分涉及到的概念,注意到启动设备服务publish的时候,传入的信息:

username:就像是设备mac地址一样的东西
pincode:是提供给用户的配对码
category:是当前设备的类型

这个类型包括:
Accessory.Categories = {
  OTHER: 1,
  BRIDGE: 2,
  FAN: 3,
  GARAGE_DOOR_OPENER: 4,
  LIGHTBULB: 5,
  DOOR_LOCK: 6,
  OUTLET: 7,
  SWITCH: 8,
  THERMOSTAT: 9,
  SENSOR: 10,
  ALARM_SYSTEM: 11,
  DOOR: 12,
  WINDOW: 13,
  WINDOW_COVERING: 14,
  PROGRAMMABLE_SWITCH: 15,
  RANGE_EXTENDER: 16,
  CAMERA: 17,
  VIDEO_DOORBELL: 18,
  AIR_PURIFIER: 19,
  AIR_HEATER: 20,
  AIR_CONDITIONER: 21,
  AIR_HUMIDIFIER: 22,
  AIR_DEHUMIDIFIER: 23
}

而这个类型的作用只是作为查找到设备后的icon展示,和其功能无关(功能还是靠service提供)。最后启动这个js,我们的服务就启动起来了。

真机调试

将一个ios10以上的设备连入和服务同一子网下,打开“家庭”app点加号添加设备,如果没问题的话,应该很快就能搜索到刚才我们新建的这个设备了:

點擊在新視窗中瀏覽此圖片

点击进去进行配对,输入刚才我们确定的配对码即pincode:

點擊在新視窗中瀏覽此圖片

配对成功,我们接下来需要确认这个设备的room等信息:

點擊在新視窗中瀏覽此圖片

最后,我们就可以在房间标签下看到它,并且能够正常操作了。通过服务端的命令行,我们可以看到剧透的通信信息和结果。

點擊在新視窗中瀏覽此圖片

软件

当然,Homekit除了ios本身和硬件通信之外,还有很大一部分就是给app提供了全部权限调用全部的api(HomeKit.framework)来实现自动化操作。进行homekit开发需要一个付费开发者账号和一个确认的appid,在xcode上我们只需要在capabilities点开homekit就自动完成这些操作了。

当然,app帮助用户进行homekit操作还需要独立向用户申请权限,我们需要前置在plist中配置NSHomeKitUsageDescription来求用户给权限。

对homekit操作,发起方是HMHomeManager对象,由于家庭信息是储存在数据库或者云中的,我们需要通过它来异步获取到家庭的全部信息。

有了家庭信息,接下来的对象大家就很眼熟了:

HMHome -> HMRoom -> HMAccessory -> HMService -> HMCharacteristic

我们简单的通过一层层的循环,就可以获取到全部信息了。而HMCharacteristic是我们最关键的操作方法,它有两个最关键的方法:
- (void)writeValue:(nullable id)value completionHandler:(void (^)(NSError * __nullable error))completion;
- (void)readValueWithCompletionHandler:(void (^)(NSError * __nullable error))completion;

即从设备读取或者写入相应的信息。当然这是超级异步的事情,所以很方便的回调方法也在这里了。

这次实际编写的demo中,我在设备上注册了一组名字特定的灯泡,希望通过遍历用户添加的设备找出这些灯泡并准备好操作。实际的关键代码如下:
@interface XGFELightHandler :NSObject
@property(nonatomic,assign) NSInteger aid;
@property(nonatomic,strong) HMAccessory *accessory;
@property(nonatomic,strong) HMCharacteristic *on;
@property(nonatomic,strong) HMCharacteristic *brightness;
@property(nonatomic,strong) HMCharacteristic *saturation;
@property(nonatomic,strong) HMCharacteristic *hue;
@end

NSMutableArray<XGFELightHandler *> *_as;

- (void)homeManagerDidUpdateHomes:(HMHomeManager *)manager {
    for (HMHome *h in manager.homes) {
        NSLog(@"HMHome = %@",h.name);
        for (HMRoom *r in h.rooms) {
            NSLog(@"HMRoom = %@",r.name);
            for (HMAccessory *a in r.accessories) {
                NSLog(@"HMAccessory = %@",a.name);
                if ([a.name containsString:@"XGFELight"]) {
                    NSUInteger aid = [[a.name stringByReplacingOccurrencesOfString:@"XGFELight" withString:@""] integerValue];
                    NSLog(@"XGFELight = %ld",aid);
                    XGFELightHandler *l = [[XGFELightHandler alloc] init];
                    l.aid = aid;
                    l.accessory = a;
                    for (HMService *s in a.services) {
                        NSLog(@"HMService = %@",s.name);
                        for (HMCharacteristic *c in s.characteristics) {
                            NSLog(@"HMCharacteristic = %@",c.characteristicType);
                            if ([c.characteristicType isEqualToString:@"00000025-0000-1000-8000-0026BB765291"]) {
                                l.on = c;
                            } else if ([c.characteristicType isEqualToString:@"00000008-0000-1000-8000-0026BB765291"]) {
                                l.brightness = c;
                            } else if ([c.characteristicType isEqualToString:@"0000002F-0000-1000-8000-0026BB765291"]) {
                                l.saturation = c;
                            } else if ([c.characteristicType isEqualToString:@"00000013-0000-1000-8000-0026BB765291"]) {
                                l.hue = c;
                            }
                        }
                    }
                    [_as setObject:l atIndexedSubscript:aid];
                }
            }
        }
    }
}

当然,以上流程针对的是用户已经添加的设备,我们当然也可以自助查找设备,通过HMAccessoryBrowser对象的方法:
- (void)startSearchingForNewAccessories;
- (void)stopSearchingForNewAccessories;

@protocol HMAccessoryBrowserDelegate <NSObject>

@optional
- (void)accessoryBrowser:(HMAccessoryBrowser *)browser didFindNewAccessory:(HMAccessory *)accessory;
- (void)accessoryBrowser:(HMAccessoryBrowser *)browser didRemoveNewAccessory:(HMAccessory *)accessory;
@end

可以搜索出HMAccessory对象,通过HMHome的:
- (void)assignAccessory:(HMAccessory *)accessory toRoom:(HMRoom *)room completionHandler:(void (^)(NSError * __nullable error))completion __WATCHOS_PROHIBITED __TVOS_PROHIBITED;

方法就可以把搜索到的设备注册到home和room上去。当然添加过程中系统会弹出配对码输入框让用户手动输入的。

总结

综上所述,正常的使用,ios设备就像一个万能遥控器一样可以方便的控制我们所有的设备。而通过一个中枢或者第三方软件,我们可以将这些设备串联起来,实现真正的智能家庭。这应该才是Homekit的发展方向和最终目标。(满分作文结尾smoke
logo