1. 此实现基于HAP-Java库实现
2. HomeKit基础知识
2.1 HomeKit配件和Apple设备的通信,主要通过两种方式:
- IP
- 蓝牙
2.2 HomeKit配件有两种入网形式:
- 单个配件 Accessory
- 桥接器 Bridge(桥接器+多个配件)
2.3 HomeKit配件的逻辑结构:
- 桥接器 Bridge 包含若干 配件 Accessory(不适用于单个配件入网)
- 配件包含若干服务 Service
- 服务包含若干属性 Characteristic
3. 详解
3.1 桥接器 Bridge
- 桥接器是一个特殊形式的配件
- 在家庭app中添加桥接器,桥接器关联的所有配件都会加入到HomeKit里面来
- 在家庭app中删除桥接器后,桥接器下关联的所有配件都会被删除
- 苹果HAP限定1个桥接器的接入配件的数量上限是150,除去桥本身就是149个配件
3.2 配件 Accessory
- 苹果HAP定义的配件:一个物理配件,通俗点就是:一个灯/窗帘/风扇/空调 等实体
- 配件加入到家庭App需要被扫描或输入8位配对码
- 每个配件对应一个独立的配对码
- 每个配件是个服务器Server,包含一个或多个服务Service
- 配件和配件之间可以直接通信,这也是苹果能把HomeKit做成去中心化的原因
3.3 服务 Service
- 苹果HAP定义的服务:配件所包含的逻辑功能,通俗点就是:这个配件包含有某类的功能
- 假如HomeKit里有一个带光感的灯,那么这个实体灯配件就包含 8.23 Light Bulb 灯 和 8.24 Light
- Sensor 光照感应 的服务
- 假如HomeKit里有一个带空气质量检测的加湿器,那么这个实体灯配件就包含 8.1.9 Humidifier
- Dehumidifier 加湿除湿、8.20 Humidity Sensor 湿度传感器、8.3 Air Quality Sensor
空气质量传感器 三项服务 - 每个家庭app图标对应一个服务,有一些图标可以用户自定义
- 有些服务是隐性服务 Hidden Service,用户在家庭app中看不到,比如 8.17 HAP Protocol
Information HAP 信息 - 苹果HAP限定每个配件包含服务的数量不能超过100个
苹果HAP定义的服务章节列表如下:
3.4 属性 Characteristic
- 属性 Characteristic 是服务更细致的构成,详细描述服务功能
- 比如一个灯的服务,能做到开关/调亮度/调色温/调RGB,那开关、亮度值、色温值、RGB值 都分别是这个灯的服务的属性
- 服务可包含多项属性,至少含一项必备属性Required Characteristic,可以不包含可选属性 Optional
Characteristic
苹果HAP定义的属性章节列表如下:
4. 举例详解
假如有个HomeKit配件,是一个带灯泡的电风扇,我们来看一下三种情况下的配件构成有什么区别:
灯光可开闭/调亮度/颜色、风扇可调速/摆动模式(上下/左右扫风)
这个配件就包含2个服务:
- Light Bulb 灯
- Fan 风扇
在灯的服务中,包含如下属性:
- On 开闭
- Brightness 亮度
- Hue 色相
- Saturation 饱和度
在风扇的服务中,含如下属性:
- Active 激活
- Rotation Speed 旋转速度
- Swing Mode 摆动模式
5. 代码实现
5.1 项目依赖
<dependency>
<groupId>io.github.hap-java</groupId>
<artifactId>hap</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency>
- hap库用于解析hap协议,与设备间通信
- zxing库用于生成二维码
5.2 功能实现
import com.beowulfe.hap.sample.accessories.coldandwarmfreshair.AirConditioning;
import com.beowulfe.hap.sample.accessories.coldandwarmfreshair.Fan;
import com.beowulfe.hap.sample.accessories.doorsandwindows.ElectricSlidingDoors;
import com.beowulfe.hap.sample.accessories.light.*;
import com.beowulfe.hap.sample.accessories.sensor.AirSensor;
import com.beowulfe.hap.sample.accessories.sensor.LightSensor;
import com.beowulfe.hap.sample.accessories.sensor.MotionSensors;
import com.beowulfe.hap.sample.accessories.sunshade.HoldPosition;
import com.beowulfe.hap.sample.accessories.sunshade.HorizontalTilting;
import com.beowulfe.hap.sample.accessories.sunshade.SingleMotorCurtain;
import com.beowulfe.hap.sample.accessories.sunshade.VerticalTilting;
import com.beowulfe.hap.sample.accessories.sw.Switch;
import io.github.hapjava.server.impl.HomekitRoot;
import io.github.hapjava.server.impl.HomekitServer;
import io.github.hapjava.server.impl.crypto.HAPSetupCodeUtils;
import java.io.*;
import java.net.InetAddress;
public class Main {
private static final int PORT = 8001
private static final String PIN = "031-45-154"
public static void main(String[] args) {
try {
File authFile = new File("auth-state.bin");
MockAuthInfo mockAuth;
if (authFile.exists()) {
FileInputStream fileInputStream = new FileInputStream(authFile);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
try {
System.out.println("Using persisted auth");
AuthState authState = (AuthState) objectInputStream.readObject();
mockAuth = new MockAuthInfo(authState);
} finally {
objectInputStream.close();
}
} else {
mockAuth = new MockAuthInfo("111-11-111");
}
HomekitServer homekit = new HomekitServer(PORT);
HomekitRoot bridge = homekit.createBridge(mockAuth, PIN , 2, "Bridge, Inc.", "M1",PIN , "1.0", "1.0");
String setupURI = HAPSetupCodeUtils.getSetupURI(mockAuth.getPin().replace("-", ""), mockAuth.getSetupId(), 2);
QRtoConsole.printQR(setupURI);
mockAuth.onChange(state -> {
try {
System.out.println("State has changed! Writing");
FileOutputStream fileOutputStream = new FileOutputStream(authFile);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(state);
objectOutputStream.flush();
objectOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
});
bridge.batchUpdate();
MockLightAndFan mockLightAndFan = new MockLightAndFan(3, "light and fan", "LF", "LF", "withlight", "firmware", "hardware");
bridge.addAccessory(mockLightAndFan);
bridge.completeUpdateBatch();
bridge.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Stopping homekit server.");
homekit.stop();
}));
} catch (Exception e) {
e.printStackTrace();
}
}
}
import java.io.Serializable;
import java.math.BigInteger;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
class AuthState implements Serializable {
private static final long serialVersionUID = 1L;
String PIN;
final String mac;
final BigInteger salt;
final byte[] privateKey;
final String setupId;
final ConcurrentMap<String, byte[]> userKeyMap = new ConcurrentHashMap<>();
public AuthState(String _PIN, String _mac, BigInteger _salt, byte[] _privateKey, String _setupId) {
PIN = _PIN;
salt = _salt;
privateKey = _privateKey;
mac = _mac;
setupId = _setupId;
}
}
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.util.function.Consumer;
import io.github.hapjava.server.HomekitAuthInfo;
import io.github.hapjava.server.impl.HomekitServer;
import io.github.hapjava.server.impl.crypto.HAPSetupCodeUtils;
/**
* This is a simple implementation that should never be used in actual production. The mac, salt, and privateKey
* are being regenerated every time the application is started. The user store is also not persisted. This means pairing
* needs to be re-done every time the app restarts.
*
* @author Andy Lintner
*/
public class MockAuthInfo implements HomekitAuthInfo {
private final AuthState authState;
Consumer<AuthState> callback;
public MockAuthInfo(String pin) throws InvalidAlgorithmParameterException {
this(new AuthState(pin, HomekitServer.generateMac(), HomekitServer.generateSalt(),
HomekitServer.generateKey(), HAPSetupCodeUtils.generateSetupId()));
}
public MockAuthInfo(AuthState _authState) {
authState = _authState;
System.out.println("The PIN for pairing is " + authState.PIN);
}
@Override
public String getPin() {
return authState.PIN;
}
@Override
public String getMac() {
return authState.mac;
}
@Override
public BigInteger getSalt() {
return authState.salt;
}
@Override
public byte[] getPrivateKey() {
return authState.privateKey;
}
@Override
public String getSetupId() {
return authState.setupId;
}
@Override
public void createUser(String username, byte[] publicKey) {
if (!authState.userKeyMap.containsKey(username)) {
authState.userKeyMap.putIfAbsent(username, publicKey);
System.out.println("Added pairing for " + username);
notifyChange();
} else {
System.out.println("Already have a user for " + username);
}
}
@Override
public void removeUser(String username) {
authState.userKeyMap.remove(username);
System.out.println("Removed pairing for " + username);
notifyChange();
}
@Override
public byte[] getUserPublicKey(String username) {
return authState.userKeyMap.get(username);
}
public void onChange(Consumer<AuthState> _callback) {
callback = _callback;
notifyChange();
}
private void notifyChange() {
if (callback != null) {
callback.accept(authState);
}
}
@Override
public boolean hasUser() {
return !authState.userKeyMap.isEmpty();
}
}
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ported code from https://github.com/gtanner/qrcode-terminal
*/
public class QRtoConsole {
private static final Logger logger = LoggerFactory.getLogger(QRtoConsole.class);
private static final char WHITE_ALL = '\u2588';
private static final char WHITE_BLACK = '\u2580';
private static final char BLACK_WHITE = '\u2584';
private static final char BLACK_ALL = ' ';
public static void printQR(String setupURI) {
try {
BitMatrix matrix = new MultiFormatWriter().encode(
setupURI, BarcodeFormat.QR_CODE, 10,
30);
for(int y = 0; y < matrix.getHeight();y+=2) {
for(int x = 0; x < matrix.getWidth(); x++) {
boolean firstRow = matrix.get(x,y);
boolean secondRow = matrix.get(x,y+1);
if(firstRow && secondRow) {
System.out.print(BLACK_ALL);
} else if(firstRow) {
System.out.print(BLACK_WHITE);
} else if(secondRow) {
System.out.print(WHITE_BLACK);
} else {
System.out.print(WHITE_ALL);
}
}
System.out.println();
}
} catch(WriterException e) {
logger.error("error creating qr code", e);
}
}
}
import io.github.hapjava.accessories.FanAccessory;
import io.github.hapjava.accessories.LightbulbAccessory;
import io.github.hapjava.accessories.optionalcharacteristic.*;
import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
import io.github.hapjava.characteristics.impl.fan.CurrentFanStateEnum;
import io.github.hapjava.characteristics.impl.fan.RotationDirectionEnum;
import io.github.hapjava.characteristics.impl.fan.SwingModeEnum;
import io.github.hapjava.characteristics.impl.fan.TargetFanStateEnum;
import io.github.hapjava.services.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.concurrent.CompletableFuture;
public class MockLightAndFan implements
FanAccessory,
LightbulbAccessory,
AccessoryWithFanState,
AccessoryWithHardwareRevision,
AccessoryWithBrightness,
AccessoryWithColor,
AccessoryWithRotationDirection,
AccessoryWithRotationSpeed,
AccessoryWithSwingMode {
private static final Logger logger = LoggerFactory.getLogger(MockLightAndFan.class);
/**
* 配件唯一id
*/
private int id;
/**
* 配件名称
*/
private String name;
/**
* 配件序列号
*/
private String serialNumber;
/**
* 配件型号
*/
private String model;
/**
* 配件制造商
*/
private String manufacturer;
/**
* 配件固件版本号
*/
private String firmwareRevision;
/**
* 配件硬件版本号
*/
private String hardwareRevision;
/**
* 初始灯的状态
*/
private boolean powerState = false;
private boolean active = false;
/**
* 当前风扇状态
*/
private CurrentFanStateEnum currentFanStateEnum = CurrentFanStateEnum.IDLE;
/**
* 风扇目标状态、/手动/自动
*/
private TargetFanStateEnum targetFanStateEnum = TargetFanStateEnum.MANUAL;
/**
* 亮度
*/
private int brightness = 0;
/**
* 风扇旋转方向
*/
private RotationDirectionEnum rotationDirectionEnum = RotationDirectionEnum.CLOCKWISE;
/**
* 旋转速度
*/
private double rotationSpeed = 1.0d;
/**
* 摆动模式
*/
private SwingModeEnum swingModeEnum = SwingModeEnum.SWING_DISABLED;
/**
* 色相
*/
private double hue = 1d;
/**
* 饱和度
*/
private double saturation = 1d;
/**
* 灯回调
*/
private HomekitCharacteristicChangeCallback lightSubscribeCallback = null;
/**
* 风扇状态回调
*/
private HomekitCharacteristicChangeCallback fanSubscribeCallback = null;
/**
* 风扇目标状态回调
*/
private HomekitCharacteristicChangeCallback fanTargetSubscribeCallback = null;
/**
* 灯光亮度回调
*/
private HomekitCharacteristicChangeCallback brightnessSubscribeCallback = null;
/**
* 旋转方向回调
*/
private HomekitCharacteristicChangeCallback rotationSubscribeCallback = null;
/**
* 旋转速度回调
*/
private HomekitCharacteristicChangeCallback rotationSpeedSubscribeCallback = null;
/**
* 摆动模式回调
*/
private HomekitCharacteristicChangeCallback swingModeSubscribeCallback = null;
/**
* 色相回调
*/
private HomekitCharacteristicChangeCallback hueSubscribeCallback = null;
/**
* 饱和度回调
*/
private HomekitCharacteristicChangeCallback saturationSubscribeCallback = null;
public MockLightAndFan(int id, String name, String serialNumber, String model, String manufacturer, String firmwareRevision, String hardwareRevision) {
this.id = id;
this.name = name;
this.serialNumber = serialNumber;
this.model = model;
this.manufacturer = manufacturer;
this.firmwareRevision = firmwareRevision;
this.hardwareRevision = hardwareRevision;
}
@Override
public int getId() {
return this.id;
}
@Override
public CompletableFuture<String> getName() {
return CompletableFuture.completedFuture(this.name);
}
/**
* 触发
*/
@Override
public void identify() {
logger.info("家庭APP 点击 识别按钮后触发:Light Fan " + this.name);
}
/**
* 序列号
*/
@Override
public CompletableFuture<String> getSerialNumber() {
return CompletableFuture.completedFuture(this.serialNumber);
}
/**
* 型号
*/
@Override
public CompletableFuture<String> getModel() {
return CompletableFuture.completedFuture(this.model);
}
/**
* 生产厂商
*/
@Override
public CompletableFuture<String> getManufacturer() {
return CompletableFuture.completedFuture(this.manufacturer);
}
/**
* 固件版本号
*/
@Override
public CompletableFuture<String> getFirmwareRevision() {
return CompletableFuture.completedFuture(this.firmwareRevision);
}
/**
* 硬件版本号
*/
@Override
public CompletableFuture<String> getHardwareRevision() {
return CompletableFuture.completedFuture(this.hardwareRevision);
}
/**
* 获取灯的开关状态
*/
@Override
public CompletableFuture<Boolean> getLightbulbPowerState() {
return CompletableFuture.completedFuture(this.powerState);
}
/**
* 设置灯的开关装填
*/
@Override
public CompletableFuture<Void> setLightbulbPowerState(boolean powerState) throws Exception {
this.powerState = powerState;
if (lightSubscribeCallback != null) {
lightSubscribeCallback.changed();
}
logger.info(this.name + " The lightbulb is now " + (powerState ? "on" : "off"));
return CompletableFuture.completedFuture(null);
}
/**
* 订阅等的开关状态
*/
@Override
public void subscribeLightbulbPowerState(HomekitCharacteristicChangeCallback callback) {
this.lightSubscribeCallback = callback;
}
/**
* 取消订阅灯的开关状态
*/
@Override
public void unsubscribeLightbulbPowerState() {
this.lightSubscribeCallback = null;
}
@Override
public CompletableFuture<Boolean> isActive() {
return CompletableFuture.completedFuture(this.active);
}
@Override
public CompletableFuture<Void> setActive(boolean state) throws Exception {
this.active = state;
if (this.fanSubscribeCallback != null) {
fanSubscribeCallback.changed();
}
logger.info(this.name + " The fun is now " + (powerState ? "on" : "off"));
return CompletableFuture.completedFuture(null);
}
@Override
public void subscribeActive(HomekitCharacteristicChangeCallback callback) {
this.fanSubscribeCallback = callback;
}
@Override
public void unsubscribeActive() {
this.fanSubscribeCallback = null;
}
/**
* 获取服务
*/
@Override
public Collection<Service> getServices() {
Collection<Service> services = new LinkedHashSet<>(FanAccessory.super.getServices());
services.addAll(LightbulbAccessory.super.getServices());
return services;
}
/**
* 获取灯的主要服务
*/
@Override
public Service getPrimaryService() {
return null;
}
/**
* 获取亮度
*/
@Override
public CompletableFuture<Integer> getBrightness() {
return CompletableFuture.completedFuture(this.brightness);
}
/**
* 设置亮度
*/
@Override
public CompletableFuture<Void> setBrightness(Integer value) throws Exception {
this.brightness = value;
if (brightnessSubscribeCallback != null) {
brightnessSubscribeCallback.changed();
}
logger.info(this.name + " The light brightness is:" + value);
return CompletableFuture.completedFuture(null);
}
/**
* 订阅亮度
*/
@Override
public void subscribeBrightness(HomekitCharacteristicChangeCallback callback) {
this.brightnessSubscribeCallback = callback;
}
/**
* 取消订阅亮度
*/
@Override
public void unsubscribeBrightness() {
this.brightnessSubscribeCallback = null;
}
/**
* 获取旋转方向
*/
@Override
public CompletableFuture<RotationDirectionEnum> getRotationDirection() {
return CompletableFuture.completedFuture(this.rotationDirectionEnum);
}
/**
* 设置旋转方向
*/
@Override
public CompletableFuture<Void> setRotationDirection(RotationDirectionEnum direction) throws Exception {
this.rotationDirectionEnum = direction;
if (rotationSubscribeCallback != null) {
rotationSubscribeCallback.changed();
}
logger.info(this.name + " The fan rotation direction is:" + direction.getCode());
return CompletableFuture.completedFuture(null);
}
/**
* 订阅旋转方向
*/
@Override
public void subscribeRotationDirection(HomekitCharacteristicChangeCallback callback) {
this.rotationSubscribeCallback = callback;
}
/**
* 取消订阅旋转方向
*/
@Override
public void unsubscribeRotationDirection() {
this.rotationSubscribeCallback = null;
}
/**
* 获取旋转速度
*/
@Override
public CompletableFuture<Double> getRotationSpeed() {
return CompletableFuture.completedFuture(this.rotationSpeed);
}
/**
* 设置旋转速度
*/
@Override
public CompletableFuture<Void> setRotationSpeed(Double speed) throws Exception {
this.rotationSpeed = speed;
if (rotationSpeedSubscribeCallback != null) {
rotationSpeedSubscribeCallback.changed();
}
logger.info(this.name + " The fan rotation speed is:" + speed);
return CompletableFuture.completedFuture(null);
}
/**
* 订阅旋转速度
*/
@Override
public void subscribeRotationSpeed(HomekitCharacteristicChangeCallback callback) {
this.rotationSpeedSubscribeCallback = callback;
}
/**
* 取消订阅旋转速度
*/
@Override
public void unsubscribeRotationSpeed() {
this.rotationSpeedSubscribeCallback = null;
}
/**
* 获取摆动模式
*/
@Override
public CompletableFuture<SwingModeEnum> getSwingMode() {
return CompletableFuture.completedFuture(this.swingModeEnum);
}
/**
* 设置摆动模式
*/
@Override
public CompletableFuture<Void> setSwingMode(SwingModeEnum swingMode) {
this.swingModeEnum = swingMode;
if (swingModeSubscribeCallback != null) {
swingModeSubscribeCallback.changed();
}
logger.info(this.name + " The fan swing mode is:" + swingMode.getCode());
return CompletableFuture.completedFuture(null);
}
/**
* 订阅摆动模式
*/
@Override
public void subscribeSwingMode(HomekitCharacteristicChangeCallback callback) {
this.swingModeSubscribeCallback = callback;
}
/**
* 取消摆动模式
*/
@Override
public void unsubscribeSwingMode() {
this.swingModeSubscribeCallback = null;
}
@Override
public CompletableFuture<Double> getHue() {
return CompletableFuture.completedFuture(this.hue);
}
@Override
public CompletableFuture<Void> setHue(Double value) throws Exception {
this.hue = value;
if (hueSubscribeCallback != null) {
hueSubscribeCallback.changed();
}
logger.info(this.name + " The light hue is:" + value);
return CompletableFuture.completedFuture(null);
}
@Override
public void subscribeHue(HomekitCharacteristicChangeCallback callback) {
this.hueSubscribeCallback = callback;
}
@Override
public void unsubscribeHue() {
this.hueSubscribeCallback = null;
}
@Override
public CompletableFuture<Double> getSaturation() {
return CompletableFuture.completedFuture(this.saturation);
}
@Override
public CompletableFuture<Void> setSaturation(Double value) throws Exception {
this.saturation = value;
if (saturationSubscribeCallback != null) {
saturationSubscribeCallback.changed();
}
logger.info(this.name + " The light saturation is:" + value);
return CompletableFuture.completedFuture(null);
}
@Override
public void subscribeSaturation(HomekitCharacteristicChangeCallback callback) {
this.saturationSubscribeCallback = callback;
}
@Override
public void unsubscribeSaturation() {
this.saturationSubscribeCallback = null;
}
@Override
public CompletableFuture<CurrentFanStateEnum> getCurrentFanState() {
return CompletableFuture.completedFuture(this.currentFanStateEnum);
}
@Override
public CompletableFuture<TargetFanStateEnum> getTargetFanState() {
return CompletableFuture.completedFuture(this.targetFanStateEnum);
}
@Override
public CompletableFuture<Void> setTargetFanState(TargetFanStateEnum targetState) {
this.targetFanStateEnum = targetState;
if (fanTargetSubscribeCallback != null) {
fanTargetSubscribeCallback.changed();
}
logger.info(this.name + " The fan target is:" + targetState.getCode());
return CompletableFuture.completedFuture(null);
}
@Override
public void subscribeCurrentFanState(HomekitCharacteristicChangeCallback callback) {
this.fanSubscribeCallback = callback;
}
@Override
public void unsubscribeCurrentFanState() {
this.fanSubscribeCallback = null;
}
@Override
public void subscribeTargetFanState(HomekitCharacteristicChangeCallback callback) {
this.fanTargetSubscribeCallback = callback;
}
@Override
public void unsubscribeTargetFanState() {
this.fanTargetSubscribeCallback = null;
}
}