[homekit] add support for QR code based pairing (#9475)
* add support qrcode based pairing Signed-off-by: Eugen Freiter <freiter@gmx.de> * add screenshots Signed-off-by: Eugen Freiter <freiter@gmx.de> * fix typo Signed-off-by: Eugen Freiter <freiter@gmx.de> * update config only if differnt to prevent endless update loop Signed-off-by: Eugen Freiter <freiter@gmx.de> * clean up Signed-off-by: Eugen Freiter <freiter@gmx.de> * add support qrcode based pairing Signed-off-by: Eugen Freiter <freiter@gmx.de> * add screenshots Signed-off-by: Eugen Freiter <freiter@gmx.de> * fix typo Signed-off-by: Eugen Freiter <freiter@gmx.de> * update config only if differnt to prevent endless update loop Signed-off-by: Eugen Freiter <freiter@gmx.de> * clean up Signed-off-by: Eugen Freiter <freiter@gmx.de> * incorporate review feedback Signed-off-by: Eugen Freiter <freiter@gmx.de> * fix Nullable based on feedback Signed-off-by: Eugen Freiter <freiter@gmx.de> * correct the java hap version Signed-off-by: Eugen Freiter <freiter@gmx.de> * Update bundles/org.openhab.io.homekit/pom.xml * adapt groupid Signed-off-by: Eugen Freiter <freiter@gmx.de> * incorporate review feedback Signed-off-by: Eugen Freiter <freiter@gmx.de> Co-authored-by: Eugen Freiter <freiter@gmx.de> Co-authored-by: J-N-K <J-N-K@users.noreply.github.com>
@ -34,6 +34,40 @@ HomeKit integration supports following accessory types:
|
||||
- Carbon Dioxide Sensor
|
||||
- Carbon Monoxide Sensor
|
||||
|
||||
## Quick start
|
||||
|
||||
- install homekit binding via UI
|
||||
|
||||
- add metadata to an existing item (see [UI based configuration](#UI-based-Configuration))
|
||||
|
||||
- go to scan QR code from UI->Setting-HomeKit Integration
|
||||
|
||||
![settings_qrcode.png](doc/settings_qrcode.png)
|
||||
|
||||
- open home app on your iPhone or iPad
|
||||
- create new home
|
||||
|
||||
![ios_add_new_home.png](doc/ios_add_new_home.png)
|
||||
|
||||
- add accessory
|
||||
|
||||
![ios_add_accessory.png](doc/ios_add_accessory.png)
|
||||
|
||||
- scan QR code from UI->Setting-HomeKit Integration
|
||||
|
||||
![ios_scan_qrcode.png](doc/ios_scan_qrcode.png)
|
||||
|
||||
- click "Add Anyway"
|
||||
|
||||
![ios_add_anyway.png](doc/ios_add_anyway.png)
|
||||
|
||||
- follow the instruction of the home app wizard
|
||||
|
||||
![ios_add_accessory_wizard.png](doc/ios_add_accessory_wizard.png)
|
||||
|
||||
Add metadata to more item or fine-tune your configuration using further settings
|
||||
|
||||
|
||||
## Global Configuration
|
||||
|
||||
Your first step will be to create the `homekit.cfg` in your `$OPENHAB_CONF/services` folder.
|
||||
@ -93,8 +127,30 @@ Complex accessories require a tag on a Group Item indicating the accessory type,
|
||||
|
||||
A HomeKit accessory has mandatory and optional characteristics (listed below in the table).
|
||||
The mapping between openHAB items and HomeKit accessory and characteristics is done by means of [metadata](https://www.openhab.org/docs/concepts/items.html#item-metadata)
|
||||
e.g.
|
||||
|
||||
### UI based Configuration
|
||||
In order to add metadata to an item:
|
||||
- select desired item in mainUI
|
||||
- click on "Add Metadata"
|
||||
|
||||
![item_add_metadata_button.png](doc/item_add_metadata_button.png)
|
||||
|
||||
- select "Apple HomeKit" namespace
|
||||
|
||||
![select_homekit_namespace.png](doc/select_homekit_namespace.png)
|
||||
|
||||
- click on "HomeKit Accessory/Characteristic"
|
||||
|
||||
![add_homekit_tag.png](doc/add_homekit_tag.png)
|
||||
|
||||
- select required HomeKit accessory type or characteristic
|
||||
|
||||
![select_homekit_accessory_type.png](doc/select_homekit_accessory_type.png)
|
||||
|
||||
- click on "Save"
|
||||
|
||||
|
||||
### Textual configuration
|
||||
```xtend
|
||||
Switch leaksensor_metadata "Leak Sensor" {homekit="LeakSensor"}
|
||||
```
|
||||
|
BIN
bundles/org.openhab.io.homekit/doc/add_homekit_tag.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
bundles/org.openhab.io.homekit/doc/ios_add_accessory.png
Normal file
After Width: | Height: | Size: 688 KiB |
BIN
bundles/org.openhab.io.homekit/doc/ios_add_accessory_wizard.png
Normal file
After Width: | Height: | Size: 312 KiB |
BIN
bundles/org.openhab.io.homekit/doc/ios_add_anyway.png
Normal file
After Width: | Height: | Size: 401 KiB |
BIN
bundles/org.openhab.io.homekit/doc/ios_add_new_home.png
Normal file
After Width: | Height: | Size: 327 KiB |
BIN
bundles/org.openhab.io.homekit/doc/ios_scan_qrcode.png
Normal file
After Width: | Height: | Size: 477 KiB |
BIN
bundles/org.openhab.io.homekit/doc/item_add_metadata_button.png
Normal file
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 87 KiB |
BIN
bundles/org.openhab.io.homekit/doc/select_homekit_namespace.png
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
bundles/org.openhab.io.homekit/doc/settings_qrcode.png
Normal file
After Width: | Height: | Size: 107 KiB |
@ -23,7 +23,7 @@
|
||||
<dependency>
|
||||
<groupId>com.github.j-n-k</groupId>
|
||||
<artifactId>hap-java</artifactId>
|
||||
<version>2.0.0.OH2</version>
|
||||
<version>2.1.0.OH</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
@ -44,11 +44,14 @@ public class HomekitAuthInfoImpl implements HomekitAuthInfo {
|
||||
private String mac;
|
||||
private BigInteger salt;
|
||||
private byte[] privateKey;
|
||||
private final String pin;
|
||||
private String pin;
|
||||
private String setupId;
|
||||
|
||||
public HomekitAuthInfoImpl(Storage<String> storage, String pin) throws InvalidAlgorithmParameterException {
|
||||
public HomekitAuthInfoImpl(Storage<String> storage, String pin, String setupId)
|
||||
throws InvalidAlgorithmParameterException {
|
||||
this.storage = storage;
|
||||
this.pin = pin;
|
||||
this.setupId = setupId;
|
||||
initializeStorage();
|
||||
}
|
||||
|
||||
@ -63,11 +66,28 @@ public class HomekitAuthInfoImpl implements HomekitAuthInfo {
|
||||
return mac;
|
||||
}
|
||||
|
||||
public void setMac(String mac) {
|
||||
this.mac = mac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPin() {
|
||||
return pin;
|
||||
}
|
||||
|
||||
public void setPin(String pin) {
|
||||
this.pin = pin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSetupId() {
|
||||
return setupId;
|
||||
}
|
||||
|
||||
public void setSetupId(String setupId) {
|
||||
this.setupId = setupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getPrivateKey() {
|
||||
return privateKey;
|
||||
|
@ -16,6 +16,8 @@ import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Dictionary;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
@ -33,6 +35,7 @@ import org.openhab.core.storage.StorageService;
|
||||
import org.openhab.io.homekit.Homekit;
|
||||
import org.osgi.framework.Constants;
|
||||
import org.osgi.framework.FrameworkUtil;
|
||||
import org.osgi.service.cm.ConfigurationAdmin;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
@ -44,45 +47,88 @@ import org.slf4j.LoggerFactory;
|
||||
import io.github.hapjava.accessories.HomekitAccessory;
|
||||
import io.github.hapjava.server.impl.HomekitRoot;
|
||||
import io.github.hapjava.server.impl.HomekitServer;
|
||||
import io.github.hapjava.server.impl.crypto.HAPSetupCodeUtils;
|
||||
|
||||
/**
|
||||
* Provides access to openHAB items via the HomeKit API
|
||||
*
|
||||
* @author Andy Lintner - Initial contribution
|
||||
*/
|
||||
@Component(service = { Homekit.class }, configurationPid = "org.openhab.homekit", property = {
|
||||
@Component(service = { Homekit.class }, configurationPid = HomekitSettings.CONFIG_PID, property = {
|
||||
Constants.SERVICE_PID + "=org.openhab.homekit", "port:Integer=9123" })
|
||||
@ConfigurableService(category = "io", label = "HomeKit Integration", description_uri = "io:homekit")
|
||||
@NonNullByDefault
|
||||
public class HomekitImpl implements Homekit {
|
||||
private final Logger logger = LoggerFactory.getLogger(HomekitImpl.class);
|
||||
private final NetworkAddressService networkAddressService;
|
||||
private final HomekitChangeListener changeListener;
|
||||
|
||||
private final NetworkAddressService networkAddressService;
|
||||
private final ConfigurationAdmin configAdmin;
|
||||
|
||||
private HomekitAuthInfoImpl authInfo;
|
||||
private HomekitSettings settings;
|
||||
private @Nullable InetAddress networkInterface;
|
||||
private @Nullable HomekitServer homekitServer;
|
||||
private @Nullable HomekitRoot bridge;
|
||||
private final HomekitAuthInfoImpl authInfo;
|
||||
private final HomekitChangeListener changeListener;
|
||||
|
||||
private final ScheduledExecutorService scheduler = ThreadPoolManager
|
||||
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
|
||||
|
||||
@Activate
|
||||
public HomekitImpl(@Reference StorageService storageService, @Reference ItemRegistry itemRegistry,
|
||||
@Reference NetworkAddressService networkAddressService, Map<String, Object> config,
|
||||
@Reference MetadataRegistry metadataRegistry) throws IOException, InvalidAlgorithmParameterException {
|
||||
@Reference NetworkAddressService networkAddressService, @Reference MetadataRegistry metadataRegistry,
|
||||
@Reference ConfigurationAdmin configAdmin, Map<String, Object> properties)
|
||||
throws IOException, InvalidAlgorithmParameterException {
|
||||
this.networkAddressService = networkAddressService;
|
||||
this.settings = processConfig(config);
|
||||
this.configAdmin = configAdmin;
|
||||
this.settings = processConfig(properties);
|
||||
this.changeListener = new HomekitChangeListener(itemRegistry, settings, metadataRegistry, storageService);
|
||||
authInfo = new HomekitAuthInfoImpl(storageService.getStorage(HomekitAuthInfoImpl.STORAGE_KEY), settings.pin);
|
||||
startHomekitServer();
|
||||
try {
|
||||
authInfo = new HomekitAuthInfoImpl(storageService.getStorage(HomekitAuthInfoImpl.STORAGE_KEY), settings.pin,
|
||||
settings.setupId);
|
||||
startHomekitServer();
|
||||
} catch (IOException | InvalidAlgorithmParameterException e) {
|
||||
logger.warn("Cannot activate HomeKit binding. {}", e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private HomekitSettings processConfig(Map<String, Object> config) {
|
||||
HomekitSettings settings = (new Configuration(config)).as(HomekitSettings.class);
|
||||
private HomekitSettings processConfig(Map<String, Object> properties) {
|
||||
HomekitSettings settings = (new Configuration(properties)).as(HomekitSettings.class);
|
||||
org.osgi.service.cm.Configuration config = null;
|
||||
Dictionary<String, Object> props = null;
|
||||
try {
|
||||
config = configAdmin.getConfiguration(HomekitSettings.CONFIG_PID);
|
||||
props = config.getProperties();
|
||||
} catch (IOException e) {
|
||||
logger.warn("Cannot retrieve config admin {}", e.getMessage());
|
||||
}
|
||||
|
||||
if (props == null) { // if null, the configuration is new
|
||||
props = new Hashtable<>();
|
||||
}
|
||||
if (settings.networkInterface == null) {
|
||||
settings.networkInterface = networkAddressService.getPrimaryIpv4HostAddress();
|
||||
props.put("networkInterface", settings.networkInterface);
|
||||
}
|
||||
if (settings.setupId == null) { // generate setupId very first time
|
||||
settings.setupId = HAPSetupCodeUtils.generateSetupId();
|
||||
props.put("setupId", settings.setupId);
|
||||
}
|
||||
|
||||
// QR Code setup URI is always generated from PIN, setup ID and accessory category (1 = bridge)
|
||||
String setupURI = HAPSetupCodeUtils.getSetupURI(settings.pin.replaceAll("-", ""), settings.setupId, 1);
|
||||
if ((settings.qrCode == null) || (!settings.qrCode.equals(setupURI))) { // QR code was changed
|
||||
settings.qrCode = setupURI;
|
||||
props.put("qrCode", settings.qrCode);
|
||||
}
|
||||
|
||||
if (config != null) {
|
||||
try {
|
||||
config.updateIfDifferent(props);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Cannot update configuration {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
@ -97,10 +143,12 @@ public class HomekitImpl implements Homekit {
|
||||
// the HomeKit server settings changed. we do a complete re-init
|
||||
stopHomekitServer();
|
||||
startHomekitServer();
|
||||
} else if (!oldSettings.name.equals(settings.name) || !oldSettings.pin.equals(settings.pin)) {
|
||||
// we change the root bridge only
|
||||
stopBridge();
|
||||
startBridge();
|
||||
} else if (!oldSettings.name.equals(settings.name) || !oldSettings.pin.equals(settings.pin)
|
||||
|| !oldSettings.setupId.equals(settings.setupId)) {
|
||||
stopHomekitServer();
|
||||
authInfo.setPin(settings.pin);
|
||||
authInfo.setSetupId(settings.setupId);
|
||||
startHomekitServer();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not initialize HomeKit connector: {}", e.getMessage());
|
||||
|
@ -18,6 +18,7 @@ package org.openhab.io.homekit.internal;
|
||||
* @author Andy Lintner - Initial contribution
|
||||
*/
|
||||
public class HomekitSettings {
|
||||
public static final String CONFIG_PID = "org.openhab.homekit";
|
||||
public static final String MANUFACTURER = "openHAB Community";
|
||||
public static final String SERIAL_NUMBER = "none";
|
||||
public static final String MODEL = "openHAB";
|
||||
@ -26,6 +27,8 @@ public class HomekitSettings {
|
||||
public String name = "openHAB";
|
||||
public int port = 9123;
|
||||
public String pin = "031-45-154";
|
||||
public String setupId;
|
||||
public String qrCode;
|
||||
public int startDelay = 30;
|
||||
public boolean useFahrenheitTemperature = false;
|
||||
public double minimumTemperature = -100;
|
||||
@ -56,6 +59,7 @@ public class HomekitSettings {
|
||||
temp = Double.doubleToLongBits(minimumTemperature);
|
||||
result = prime * result + (int) (temp ^ (temp >>> 32));
|
||||
result = prime * result + ((pin == null) ? 0 : pin.hashCode());
|
||||
result = prime * result + ((setupId == null) ? 0 : setupId.hashCode());
|
||||
result = prime * result + port;
|
||||
result = prime * result + ((thermostatTargetModeAuto == null) ? 0 : thermostatTargetModeAuto.hashCode());
|
||||
result = prime * result + ((thermostatTargetModeCool == null) ? 0 : thermostatTargetModeCool.hashCode());
|
||||
@ -89,6 +93,8 @@ public class HomekitSettings {
|
||||
}
|
||||
} else if (!pin.equals(other.pin)) {
|
||||
return false;
|
||||
} else if (!setupId.equals(other.setupId)) {
|
||||
return false;
|
||||
}
|
||||
if (port != other.port) {
|
||||
return false;
|
||||
|
@ -22,7 +22,11 @@
|
||||
<label>Thermostat Current Heating/Cooling Mapping</label>
|
||||
<description>String values used by your thermostat to set different targetHeatingCooling modes</description>
|
||||
</parameter-group>
|
||||
|
||||
<parameter name="qrCode" type="text" required="false" groupName="core">
|
||||
<label>HomeKit QR Code</label>
|
||||
<context>qrcode</context>
|
||||
<description>Scan QR code with home app to add openHAB as HomeKit bridge. </description>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer" required="true" groupName="core">
|
||||
<label>Port</label>
|
||||
<description>Defines the port the HomeKit integration listens on.</description>
|
||||
@ -33,6 +37,10 @@
|
||||
<description>Defines the pin, used for pairing, in the form ###-##-###.</description>
|
||||
<default>031-45-154</default>
|
||||
</parameter>
|
||||
<parameter name="setupId" type="text" pattern="[0-9A-Z]{4}" required="false" groupName="core">
|
||||
<label>Setup ID</label>
|
||||
<description>Setup ID used for pairing using QR Code. Alphanumeric code of length 4.</description>
|
||||
</parameter>
|
||||
<parameter name="networkInterface" type="text" required="false" groupName="core">
|
||||
<label>Network Interface</label>
|
||||
<description>Defines the IP address of the network interface to expose the HomeKit integration on.</description>
|
||||
|