[homekit] allow multiple bridge instances to break the 150 limit (#13226)

fixes #11508, #12927

Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
Cody Cutrer 2022-08-12 01:55:39 -06:00 committed by GitHub
parent b075d3781c
commit e4c9a40d03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 203 additions and 137 deletions

View File

@ -97,6 +97,7 @@ org.openhab.homekit:networkInterface=192.168.0.6
org.openhab.homekit:useOHmDNS=false org.openhab.homekit:useOHmDNS=false
org.openhab.homekit:blockUserDeletion=false org.openhab.homekit:blockUserDeletion=false
org.openhab.homekit:name=openHAB org.openhab.homekit:name=openHAB
org.openhab.homekit:instances=1
``` ```
### Overview of all settings ### Overview of all settings
@ -110,11 +111,12 @@ org.openhab.homekit:name=openHAB
| pin | Pin code used for pairing with iOS devices. Apparently, pin codes are provided by Apple and represent specific device types, so they cannot be chosen freely. The pin code 031-45-154 is used in sample applications and known to work. | 031-45-154 | | pin | Pin code used for pairing with iOS devices. Apparently, pin codes are provided by Apple and represent specific device types, so they cannot be chosen freely. The pin code 031-45-154 is used in sample applications and known to work. | 031-45-154 |
| startDelay | HomeKit start delay in seconds in case the number of accessories is lower than last time. This helps to avoid resetting home app in case not all items have been initialised properly before HomeKit integration start. | 30 | | startDelay | HomeKit start delay in seconds in case the number of accessories is lower than last time. This helps to avoid resetting home app in case not all items have been initialised properly before HomeKit integration start. | 30 |
| useFahrenheitTemperature | Set to true to use Fahrenheit degrees, or false to use Celsius degrees. Note if an item has a QuantityType as its state, this configuration is ignored and it's always converted properly. | false | | useFahrenheitTemperature | Set to true to use Fahrenheit degrees, or false to use Celsius degrees. Note if an item has a QuantityType as its state, this configuration is ignored and it's always converted properly. | false |
| thermostatTargetModeCool | Word used for activating the cooling mode of the device (if applicable). It can be overwritten at item level. | CoolOn | | thermostatTargetModeCool | Word used for activating the cooling mode of the device (if applicable). It can be overwritten at item level. | CoolOn |
| thermostatTargetModeHeat | Word used for activating the heating mode of the device (if applicable). It can be overwritten at item level. | HeatOn | | thermostatTargetModeHeat | Word used for activating the heating mode of the device (if applicable). It can be overwritten at item level. | HeatOn |
| thermostatTargetModeAuto | Word used for activating the automatic mode of the device (if applicable). It can be overwritten at item level. | Auto | | thermostatTargetModeAuto | Word used for activating the automatic mode of the device (if applicable). It can be overwritten at item level. | Auto |
| thermostatTargetModeOff | Word used to set the thermostat mode of the device to off (if applicable). It can be overwritten at item level. | Off | | thermostatTargetModeOff | Word used to set the thermostat mode of the device to off (if applicable). It can be overwritten at item level. | Off |
| name | Name under which this HomeKit bridge is announced on the network. This is also the name displayed on the iOS device when searching for available bridges. | openHAB | | name | Name under which this HomeKit bridge is announced on the network. This is also the name displayed on the iOS device when searching for available bridges. | openHAB |
| instances | Defines how many bridges to expose. Necessary if you have more than 149 accessories. Accessories must be assigned to additional instances via metadata. Additional bridges will use incrementing port numbers. | 1 |
## Item Configuration ## Item Configuration
@ -836,6 +838,29 @@ Number cooler_cool_thrs "Cooler Cool Threshold Temp [%.1f °C
Number cooler_heat_thrs "Cooler Heat Threshold Temp [%.1f °C]" (gCooler) {homekit="HeatingThresholdTemperature" [minValue=0.5, maxValue=20]} Number cooler_heat_thrs "Cooler Heat Threshold Temp [%.1f °C]" (gCooler) {homekit="HeatingThresholdTemperature" [minValue=0.5, maxValue=20]}
``` ```
## Multiple Instances
Homekit has a limitation of 150 accessories per bridge.
The bridge itself counts as an accessory, so in practice it's 149.
In order to overcome this limitation, you can instruct OpenHAB to expose multiple bridges to Homekit, and then manually assign specific accessories to different instances.
You will need to manually add each additional bridge in the Home app, since the QR Code in settings will only be for the primary bridge; however the same PIN is still used.
In order to assign a particular accessory to a different bridge, set the `instance` metadata parameter:
```
Switch kitchen_light {homekit="Lighting" [instance=2]}
```
Note that instances are numbered starting at 1.
If you reference an instance that doesn't exist, then that accessory won't be exposed on _any_ bridge.
For complex items, only the root group needs to be tagged for the specific instance:
```
Group gSecuritySystem "Security System Group" {homekit="SecuritySystem" [instance=2]}
String security_current_state "Security Current State" (gSecuritySystem) {homekit="SecuritySystem.CurrentSecuritySystemState"}
String security_target_state "Security Target State" (gSecuritySystem) {homekit="SecuritySystem.TargetSecuritySystemState"}
```
## Additional Notes ## Additional Notes
HomeKit allows only a single pairing to be established with the bridge. HomeKit allows only a single pairing to be established with the bridge.

View File

@ -62,6 +62,7 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
private HomekitAccessoryUpdater updater = new HomekitAccessoryUpdater(); private HomekitAccessoryUpdater updater = new HomekitAccessoryUpdater();
private HomekitSettings settings; private HomekitSettings settings;
private int lastAccessoryCount; private int lastAccessoryCount;
private int instance;
private final Set<String> pendingUpdates = new HashSet<>(); private final Set<String> pendingUpdates = new HashSet<>();
@ -79,11 +80,12 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
private final Debouncer applyUpdatesDebouncer; private final Debouncer applyUpdatesDebouncer;
HomekitChangeListener(ItemRegistry itemRegistry, HomekitSettings settings, MetadataRegistry metadataRegistry, HomekitChangeListener(ItemRegistry itemRegistry, HomekitSettings settings, MetadataRegistry metadataRegistry,
Storage<String> storage) { Storage<String> storage, int instance) {
this.itemRegistry = itemRegistry; this.itemRegistry = itemRegistry;
this.settings = settings; this.settings = settings;
this.metadataRegistry = metadataRegistry; this.metadataRegistry = metadataRegistry;
this.storage = storage; this.storage = storage;
this.instance = instance;
this.applyUpdatesDebouncer = new Debouncer("update-homekit-devices", scheduler, Duration.ofMillis(1000), this.applyUpdatesDebouncer = new Debouncer("update-homekit-devices", scheduler, Duration.ofMillis(1000),
Clock.systemUTC(), this::applyUpdates); Clock.systemUTC(), this::applyUpdates);
metadataChangeListener = new RegistryChangeListener<Metadata>() { metadataChangeListener = new RegistryChangeListener<Metadata>() {
@ -131,7 +133,7 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
itemRegistry.getItems().forEach(this::createRootAccessories); itemRegistry.getItems().forEach(this::createRootAccessories);
initialiseRevision(); initialiseRevision();
makeNewConfigurationRevision(); makeNewConfigurationRevision();
logger.info("Created {} HomeKit items.", accessoryRegistry.getAllAccessories().size()); logger.info("Created {} HomeKit items in instance {}.", accessoryRegistry.getAllAccessories().size(), instance);
} }
private void initialiseRevision() { private void initialiseRevision() {
@ -297,17 +299,14 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
* @return primary accessory type * @return primary accessory type
*/ */
private HomekitAccessoryType getPrimaryAccessoryType(Item item, private HomekitAccessoryType getPrimaryAccessoryType(Item item,
List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessoryTypes) { List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessoryTypes,
if (accessoryTypes.size() > 1) { @Nullable Map<String, Object> configuration) {
final @Nullable Map<String, Object> configuration = HomekitAccessoryFactory.getItemConfiguration(item, if (accessoryTypes.size() > 1 && configuration != null) {
metadataRegistry); final @Nullable Object value = configuration.get(HomekitTaggedItem.PRIMARY_SERVICE);
if (configuration != null) { if (value instanceof String) {
final @Nullable Object value = configuration.get(HomekitTaggedItem.PRIMARY_SERVICE); return accessoryTypes.stream()
if (value instanceof String) { .filter(aType -> ((String) value).equalsIgnoreCase(aType.getKey().getTag())).findAny()
return accessoryTypes.stream() .orElse(accessoryTypes.get(0)).getKey();
.filter(aType -> ((String) value).equalsIgnoreCase(aType.getKey().getTag())).findAny()
.orElse(accessoryTypes.get(0)).getKey();
}
} }
} }
// no primary accessory found or there is only one type, so return the first type from the list // no primary accessory found or there is only one type, so return the first type from the list
@ -358,35 +357,57 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessoryTypes = HomekitAccessoryFactory final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessoryTypes = HomekitAccessoryFactory
.getAccessoryTypes(item, metadataRegistry); .getAccessoryTypes(item, metadataRegistry);
final List<GroupItem> groups = HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry); final List<GroupItem> groups = HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry);
if (!accessoryTypes.isEmpty() final @Nullable Map<String, Object> itemConfiguration = HomekitAccessoryFactory.getItemConfiguration(item,
&& (groups.isEmpty() || groups.stream().noneMatch(g -> g.getBaseItem() == null))) { metadataRegistry);
final HomekitAccessoryType primaryAccessoryType = getPrimaryAccessoryType(item, accessoryTypes); if (accessoryTypes.isEmpty() || !(groups.isEmpty() || groups.stream().noneMatch(g -> g.getBaseItem() == null))
logger.trace("Item {} is a HomeKit accessory of types {}. Primary type is {}", item.getName(), || !itemIsForThisBridge(item, itemConfiguration)) {
accessoryTypes, primaryAccessoryType); return;
final HomekitOHItemProxy itemProxy = new HomekitOHItemProxy(item); }
final HomekitTaggedItem taggedItem = new HomekitTaggedItem(new HomekitOHItemProxy(item),
primaryAccessoryType, HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry));
try {
final HomekitAccessory accessory = HomekitAccessoryFactory.create(taggedItem, metadataRegistry, updater,
settings);
accessoryTypes.stream().filter(aType -> !primaryAccessoryType.equals(aType.getKey())) final HomekitAccessoryType primaryAccessoryType = getPrimaryAccessoryType(item, accessoryTypes,
.forEach(additionalAccessoryType -> { itemConfiguration);
final HomekitTaggedItem additionalTaggedItem = new HomekitTaggedItem(itemProxy, logger.trace("Item {} is a HomeKit accessory of types {}. Primary type is {}", item.getName(), accessoryTypes,
additionalAccessoryType.getKey(), primaryAccessoryType);
HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry)); final HomekitOHItemProxy itemProxy = new HomekitOHItemProxy(item);
try { final HomekitTaggedItem taggedItem = new HomekitTaggedItem(new HomekitOHItemProxy(item), primaryAccessoryType,
final HomekitAccessory additionalAccessory = HomekitAccessoryFactory itemConfiguration);
.create(additionalTaggedItem, metadataRegistry, updater, settings); try {
accessory.getServices().add(additionalAccessory.getPrimaryService()); final HomekitAccessory accessory = HomekitAccessoryFactory.create(taggedItem, metadataRegistry, updater,
} catch (HomekitException e) { settings);
logger.warn("Cannot create additional accessory {}", additionalTaggedItem);
} accessoryTypes.stream().filter(aType -> !primaryAccessoryType.equals(aType.getKey()))
}); .forEach(additionalAccessoryType -> {
accessoryRegistry.addRootAccessory(taggedItem.getName(), accessory); final HomekitTaggedItem additionalTaggedItem = new HomekitTaggedItem(itemProxy,
} catch (HomekitException e) { additionalAccessoryType.getKey(), itemConfiguration);
logger.warn("Cannot create accessory {}", taggedItem); try {
} final HomekitAccessory additionalAccessory = HomekitAccessoryFactory
.create(additionalTaggedItem, metadataRegistry, updater, settings);
accessory.getServices().add(additionalAccessory.getPrimaryService());
} catch (HomekitException e) {
logger.warn("Cannot create additional accessory {}", additionalTaggedItem);
}
});
accessoryRegistry.addRootAccessory(taggedItem.getName(), accessory);
} catch (HomekitException e) {
logger.warn("Cannot create accessory {}", taggedItem);
} }
} }
private boolean itemIsForThisBridge(Item item, @Nullable Map<String, Object> configuration) {
// non-tagged accessories belong to the first instance
if (configuration == null) {
return (instance == 1);
}
final @Nullable Object value = configuration.get(HomekitTaggedItem.INSTANCE);
if (value == null) {
return (instance == 1);
}
if (value instanceof Number) {
return (instance == ((Number) value).intValue());
}
logger.warn("Unrecognized instance tag {} ({}) for item {}; assigning to default instance.", value,
value.getClass(), item.getName());
return (instance == 1);
}
} }

View File

@ -69,18 +69,20 @@ import io.github.hapjava.server.impl.crypto.HAPSetupCodeUtils;
public class HomekitImpl implements Homekit, NetworkAddressChangeListener { public class HomekitImpl implements Homekit, NetworkAddressChangeListener {
private final Logger logger = LoggerFactory.getLogger(HomekitImpl.class); private final Logger logger = LoggerFactory.getLogger(HomekitImpl.class);
private final StorageService storageService;
private final NetworkAddressService networkAddressService; private final NetworkAddressService networkAddressService;
private final ConfigurationAdmin configAdmin; private final ConfigurationAdmin configAdmin;
private final Storage<String> storage; private final ItemRegistry itemRegistry;
private final MetadataRegistry metadataRegistry;
private HomekitAuthInfoImpl authInfo; private final List<HomekitAuthInfoImpl> authInfos = new ArrayList<>();
private HomekitSettings settings; private HomekitSettings settings;
private @Nullable InetAddress networkInterface; private @Nullable InetAddress networkInterface;
private @Nullable HomekitServer homekitServer; private final List<HomekitServer> homekitServers = new ArrayList<>();
private @Nullable HomekitRoot bridge; private final List<HomekitRoot> bridges = new ArrayList<>();
private MDNSClient mdnsClient; private MDNSClient mdnsClient;
private final HomekitChangeListener changeListener; private final List<HomekitChangeListener> changeListeners = new ArrayList<>();
private final ScheduledExecutorService scheduler = ThreadPoolManager private final ScheduledExecutorService scheduler = ThreadPoolManager
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON); .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
@ -90,15 +92,15 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener {
@Reference NetworkAddressService networkAddressService, @Reference MetadataRegistry metadataRegistry, @Reference NetworkAddressService networkAddressService, @Reference MetadataRegistry metadataRegistry,
@Reference ConfigurationAdmin configAdmin, @Reference MDNSClient mdnsClient, Map<String, Object> properties) @Reference ConfigurationAdmin configAdmin, @Reference MDNSClient mdnsClient, Map<String, Object> properties)
throws IOException, InvalidAlgorithmParameterException { throws IOException, InvalidAlgorithmParameterException {
this.storageService = storageService;
this.networkAddressService = networkAddressService; this.networkAddressService = networkAddressService;
this.configAdmin = configAdmin; this.configAdmin = configAdmin;
this.settings = processConfig(properties); this.settings = processConfig(properties);
this.mdnsClient = mdnsClient; this.mdnsClient = mdnsClient;
this.storage = storageService.getStorage(HomekitAuthInfoImpl.STORAGE_KEY); this.itemRegistry = itemRegistry;
this.metadataRegistry = metadataRegistry;
networkAddressService.addNetworkAddressChangeListener(this); networkAddressService.addNetworkAddressChangeListener(this);
this.changeListener = new HomekitChangeListener(itemRegistry, settings, metadataRegistry, storage);
try { try {
authInfo = new HomekitAuthInfoImpl(storage, settings.pin, settings.setupId, settings.blockUserDeletion);
startHomekitServer(); startHomekitServer();
} catch (IOException | InvalidAlgorithmParameterException e) { } catch (IOException | InvalidAlgorithmParameterException e) {
logger.warn("cannot activate HomeKit binding. {}", e.getMessage()); logger.warn("cannot activate HomeKit binding. {}", e.getMessage());
@ -150,81 +152,87 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener {
settings = processConfig(config); settings = processConfig(config);
if ((oldSettings == null) || (settings == null)) if ((oldSettings == null) || (settings == null))
return; return;
changeListener.updateSettings(settings); if (!oldSettings.name.equals(settings.name) || !oldSettings.pin.equals(settings.pin)
if (!oldSettings.networkInterface.equals(settings.networkInterface) || oldSettings.port != settings.port || !oldSettings.setupId.equals(settings.setupId)
|| oldSettings.useOHmDNS != settings.useOHmDNS) { || !oldSettings.networkInterface.equals(settings.networkInterface)
|| oldSettings.port != settings.port || oldSettings.useOHmDNS != settings.useOHmDNS
|| oldSettings.instances != settings.instances) {
// the HomeKit server settings changed. we do a complete re-init // the HomeKit server settings changed. we do a complete re-init
stopHomekitServer(); stopHomekitServer();
startHomekitServer(); startHomekitServer();
} else if (!oldSettings.name.equals(settings.name) || !oldSettings.pin.equals(settings.pin) } else {
|| !oldSettings.setupId.equals(settings.setupId)) { for (HomekitChangeListener changeListener : changeListeners) {
stopHomekitServer(); changeListener.updateSettings(settings);
authInfo.setPin(settings.pin); }
authInfo.setSetupId(settings.setupId);
startHomekitServer();
} }
} catch (IOException e) { } catch (IOException | InvalidAlgorithmParameterException e) {
logger.warn("could not initialize HomeKit bridge: {}", e.getMessage()); logger.warn("could not initialize HomeKit bridge: {}", e.getMessage());
} }
} }
private void stopBridge() { private HomekitRoot startBridge(HomekitServer homekitServer, HomekitAuthInfoImpl authInfo,
final @Nullable HomekitRoot bridge = this.bridge; HomekitChangeListener changeListener, int instance) throws IOException {
if (bridge != null) { String name = settings.name;
changeListener.unsetBridge(); if (instance != 1) {
bridge.stop(); name += " (" + instance + ")";
this.bridge = null;
} }
} final HomekitRoot bridge = homekitServer.createBridge(authInfo, name, HomekitAccessoryCategories.BRIDGES,
HomekitSettings.MANUFACTURER, HomekitSettings.MODEL, HomekitSettings.SERIAL_NUMBER,
private void startBridge() throws IOException { FrameworkUtil.getBundle(getClass()).getVersion().toString(), HomekitSettings.HARDWARE_REVISION);
final @Nullable HomekitServer homekitServer = this.homekitServer; changeListener.setBridge(bridge);
if (homekitServer != null && bridge == null) { bridges.add(bridge);
final HomekitRoot bridge = homekitServer.createBridge(authInfo, settings.name, bridge.setConfigurationIndex(changeListener.getConfigurationRevision());
HomekitAccessoryCategories.BRIDGES, HomekitSettings.MANUFACTURER, HomekitSettings.MODEL, bridge.refreshAuthInfo();
HomekitSettings.SERIAL_NUMBER, FrameworkUtil.getBundle(getClass()).getVersion().toString(), final int lastAccessoryCount = changeListener.getLastAccessoryCount();
HomekitSettings.HARDWARE_REVISION); int currentAccessoryCount = changeListener.getAccessories().size();
changeListener.setBridge(bridge); if (currentAccessoryCount < lastAccessoryCount) {
this.bridge = bridge; logger.debug(
bridge.setConfigurationIndex(changeListener.getConfigurationRevision()); "it looks like not all items were initialized yet. Old configuration had {} accessories, the current one has only {} accessories. Delay HomeKit bridge start for {} seconds.",
bridge.refreshAuthInfo(); lastAccessoryCount, currentAccessoryCount, settings.startDelay);
final int lastAccessoryCount = changeListener.getLastAccessoryCount(); scheduler.schedule(() -> {
int currentAccessoryCount = changeListener.getAccessories().size(); if (currentAccessoryCount < lastAccessoryCount) {
if (currentAccessoryCount < lastAccessoryCount) { // the number of items is still different, maybe it is desired.
logger.debug( // make new configuration revision.
"it looks like not all items were initialized yet. Old configuration had {} accessories, the current one has only {} accessories. Delay HomeKit bridge start for {} seconds.", changeListener.makeNewConfigurationRevision();
lastAccessoryCount, currentAccessoryCount, settings.startDelay); }
scheduler.schedule(() -> {
if (currentAccessoryCount < lastAccessoryCount) {
// the number of items is still different, maybe it is desired.
// make new configuration revision.
changeListener.makeNewConfigurationRevision();
}
bridge.start();
}, settings.startDelay, TimeUnit.SECONDS);
} else { // start bridge immediately.
bridge.start(); bridge.start();
} }, settings.startDelay, TimeUnit.SECONDS);
} else { } else { // start bridge immediately.
logger.warn( bridge.start();
"trying to start bridge but HomeKit server is not initialized or bridge is already initialized");
} }
return bridge;
} }
private void startHomekitServer() throws IOException { private void startHomekitServer() throws IOException, InvalidAlgorithmParameterException {
logger.trace("start HomeKit bridge"); logger.trace("start HomeKit bridge");
if (homekitServer == null) { if (homekitServers.isEmpty()) {
try { try {
networkInterface = InetAddress networkInterface = InetAddress
.getByName(((settings.networkInterface != null) && (!settings.networkInterface.isEmpty())) .getByName(((settings.networkInterface != null) && (!settings.networkInterface.isEmpty()))
? settings.networkInterface ? settings.networkInterface
: networkAddressService.getPrimaryIpv4HostAddress()); : networkAddressService.getPrimaryIpv4HostAddress());
} catch (UnknownHostException e) {
logger.warn("cannot resolve the Pv4 address / hostname {}.",
networkAddressService.getPrimaryIpv4HostAddress());
}
for (int i = 0; i < settings.instances; ++i) {
String storage_key = HomekitAuthInfoImpl.STORAGE_KEY;
if (i != 0) {
storage_key += i;
}
Storage<String> storage = storageService.getStorage(storage_key);
HomekitAuthInfoImpl authInfo = new HomekitAuthInfoImpl(storage, settings.pin, settings.setupId,
settings.blockUserDeletion);
@Nullable
HomekitServer homekitServer = null;
if (settings.useOHmDNS) { if (settings.useOHmDNS) {
for (JmDNS mdns : mdnsClient.getClientInstances()) { for (JmDNS mdns : mdnsClient.getClientInstances()) {
if (mdns.getInetAddress().equals(networkInterface)) { if (mdns.getInetAddress().equals(networkInterface)) {
logger.trace("suitable mDNS client for IP {} found and will be used for HomeKit", logger.trace("suitable mDNS client for IP {} found and will be used for HomeKit",
networkInterface); networkInterface);
homekitServer = new HomekitServer(mdns, settings.port); homekitServer = new HomekitServer(mdns, settings.port + i);
} }
} }
} }
@ -233,12 +241,14 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener {
logger.trace("no suitable mDNS server for IP {} found", networkInterface); logger.trace("no suitable mDNS server for IP {} found", networkInterface);
} }
logger.trace("create HomeKit server with dedicated mDNS server"); logger.trace("create HomeKit server with dedicated mDNS server");
homekitServer = new HomekitServer(networkInterface, settings.port); homekitServer = new HomekitServer(networkInterface, settings.port + i);
} }
startBridge(); homekitServers.add(homekitServer);
} catch (UnknownHostException e) { HomekitChangeListener changeListener = new HomekitChangeListener(itemRegistry, settings,
logger.warn("cannot resolve the Pv4 address / hostname {}.", metadataRegistry, storage, i + 1);
networkAddressService.getPrimaryIpv4HostAddress()); changeListeners.add(changeListener);
bridges.add(startBridge(homekitServer, authInfo, changeListener, i + 1));
authInfos.add(authInfo);
} }
} else { } else {
logger.warn("trying to start HomeKit server but it is already initialized"); logger.warn("trying to start HomeKit server but it is already initialized");
@ -247,49 +257,56 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener {
private void stopHomekitServer() { private void stopHomekitServer() {
logger.trace("stop HomeKit bridge"); logger.trace("stop HomeKit bridge");
final @Nullable HomekitServer homekit = this.homekitServer; for (int i = 0; i < homekitServers.size(); ++i) {
if (homekit != null) { changeListeners.get(i).unsetBridge();
if (bridge != null) { bridges.get(i).stop();
stopBridge(); homekitServers.get(i).stop();
} changeListeners.get(i).stop();
homekit.stop();
this.homekitServer = null;
} }
homekitServers.clear();
bridges.clear();
changeListeners.clear();
authInfos.clear();
} }
@Deactivate @Deactivate
protected void deactivate() { protected void deactivate() {
networkAddressService.removeNetworkAddressChangeListener(this); networkAddressService.removeNetworkAddressChangeListener(this);
changeListener.clearAccessories(); for (HomekitChangeListener changeListener : changeListeners) {
changeListener.clearAccessories();
}
stopHomekitServer(); stopHomekitServer();
changeListener.stop();
} }
@Override @Override
public void refreshAuthInfo() throws IOException { public void refreshAuthInfo() throws IOException {
final @Nullable HomekitRoot bridge = this.bridge; for (HomekitRoot bridge : bridges) {
if (bridge != null) {
bridge.refreshAuthInfo(); bridge.refreshAuthInfo();
} }
} }
@Override @Override
public void allowUnauthenticatedRequests(boolean allow) { public void allowUnauthenticatedRequests(boolean allow) {
final @Nullable HomekitRoot bridge = this.bridge; for (HomekitRoot bridge : bridges) {
if (bridge != null) {
bridge.allowUnauthenticatedRequests(allow); bridge.allowUnauthenticatedRequests(allow);
} }
} }
@Override @Override
public List<HomekitAccessory> getAccessories() { public List<HomekitAccessory> getAccessories() {
return new ArrayList<>(this.changeListener.getAccessories().values()); List<HomekitAccessory> accessories = new ArrayList<>();
for (HomekitChangeListener changeListener : changeListeners) {
accessories.addAll(changeListener.getAccessories().values());
}
return accessories;
} }
@Override @Override
public void clearHomekitPairings() { public void clearHomekitPairings() {
try { try {
authInfo.clear(); for (HomekitAuthInfoImpl authInfo : authInfos) {
authInfo.clear();
}
refreshAuthInfo(); refreshAuthInfo();
} catch (Exception e) { } catch (Exception e) {
logger.warn("could not clear HomeKit pairings", e); logger.warn("could not clear HomeKit pairings", e);
@ -302,23 +319,13 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener {
removed.forEach(i -> { removed.forEach(i -> {
logger.trace("removed interface {}", i.getAddress().toString()); logger.trace("removed interface {}", i.getAddress().toString());
if (i.getAddress().equals(networkInterface)) { if (i.getAddress().equals(networkInterface)) {
final @Nullable HomekitRoot bridge = this.bridge; stopHomekitServer();
if (bridge != null) {
bridge.stop();
this.bridge = null;
}
final @Nullable HomekitServer homekitServer = this.homekitServer;
if (homekitServer != null) {
homekitServer.stop();
this.homekitServer = null;
}
logger.trace("bridge stopped");
} }
}); });
if ((this.bridge == null) && (!added.isEmpty())) { if (bridges.isEmpty() && !added.isEmpty()) {
try { try {
startHomekitServer(); startHomekitServer();
} catch (IOException e) { } catch (IOException | InvalidAlgorithmParameterException e) {
logger.warn("could not initialize HomeKit bridge: {}", e.getMessage()); logger.warn("could not initialize HomeKit bridge: {}", e.getMessage());
} }
} }

View File

@ -26,6 +26,7 @@ public class HomekitSettings {
public String name = "openHAB"; public String name = "openHAB";
public int port = 9123; public int port = 9123;
public int instances = 1;
public String pin = "031-45-154"; public String pin = "031-45-154";
public String setupId; public String setupId;
public String qrCode; public String qrCode;
@ -92,6 +93,9 @@ public class HomekitSettings {
if (port != other.port) { if (port != other.port) {
return false; return false;
} }
if (instances != other.instances) {
return false;
}
if (thermostatTargetModeAuto == null) { if (thermostatTargetModeAuto == null) {
if (other.thermostatTargetModeAuto != null) { if (other.thermostatTargetModeAuto != null) {
return false; return false;

View File

@ -43,6 +43,7 @@ public class HomekitTaggedItem {
public final static String DELAY = "commandDelay"; public final static String DELAY = "commandDelay";
public final static String INVERTED = "inverted"; public final static String INVERTED = "inverted";
public final static String PRIMARY_SERVICE = "primary"; public final static String PRIMARY_SERVICE = "primary";
public final static String INSTANCE = "instance";
private static final Map<Integer, String> CREATED_ACCESSORY_IDS = new ConcurrentHashMap<>(); private static final Map<Integer, String> CREATED_ACCESSORY_IDS = new ConcurrentHashMap<>();

View File

@ -41,6 +41,12 @@
<description>Defines the port the HomeKit integration listens on.</description> <description>Defines the port the HomeKit integration listens on.</description>
<default>9123</default> <default>9123</default>
</parameter> </parameter>
<parameter name="instances" type="integer" required="true" groupName="core">
<label>Instances</label>
<description>Defines how many bridges to expose. Necessary if you have more than 149 accessories. Accessories must be
assigned to additional instances via metadata. Additional bridges will use incrementing port numbers.</description>
<default>1</default>
</parameter>
<parameter name="pin" type="text" pattern="\d{3}-\d{2}-\d{3}" required="true" groupName="core"> <parameter name="pin" type="text" pattern="\d{3}-\d{2}-\d{3}" required="true" groupName="core">
<label>Pin</label> <label>Pin</label>
<description>Defines the pin, used for pairing, in the form ###-##-###.</description> <description>Defines the pin, used for pairing, in the form ###-##-###.</description>

View File

@ -9,6 +9,8 @@ io.config.homekit.group.thermostatCurrentHeatingCooling.label = Thermostat Curre
io.config.homekit.group.thermostatCurrentHeatingCooling.description = String values used by your thermostat to set different targetHeatingCooling modes io.config.homekit.group.thermostatCurrentHeatingCooling.description = String values used by your thermostat to set different targetHeatingCooling modes
io.config.homekit.group.thermostatTargetHeatingCooling.label = Thermostat Target Heating/Cooling Mapping io.config.homekit.group.thermostatTargetHeatingCooling.label = Thermostat Target Heating/Cooling Mapping
io.config.homekit.group.thermostatTargetHeatingCooling.description = String values used by your thermostat to set different targetHeatingCooling modes io.config.homekit.group.thermostatTargetHeatingCooling.description = String values used by your thermostat to set different targetHeatingCooling modes
io.config.homekit.instances.label = Instances
io.config.homekit.instances.description = Defines how many bridges to expose. Necessary if you have more than 149 accessories. Accessories must be assigned to additional instances via metadata. Additional bridges will use incrementing port numbers.
io.config.homekit.name.label = Bridge name io.config.homekit.name.label = Bridge name
io.config.homekit.name.description = Name of the HomeKit bridge io.config.homekit.name.description = Name of the HomeKit bridge
io.config.homekit.networkInterface.label = Network Interface io.config.homekit.networkInterface.label = Network Interface