package org.openhab.io.homekit.internal;
import java.util.Arrays;
+import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import io.github.hapjava.accessories.HomekitAccessory;
import io.github.hapjava.services.Service;
/**
SUBCMD_ALLOW_UNAUTHENTICATED, SUBCMD_PRUNE_DUMMY_ACCESSORIES, SUBCMD_LIST_DUMMY_ACCESSORIES),
false);
+ private static final String PARAM_INSTANCE = "--instance";
+ private static final String PARAM_INSTANCE_HELP = " [--instance <instance id>]";
+
private class CommandCompleter implements ConsoleCommandCompleter {
public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List<String> candidates) {
if (cursorArgumentIndex == 0) {
}
@Override
- public void execute(String[] args, Console console) {
- if (args.length > 0) {
- String subCommand = args[0];
+ public void execute(String[] argsArray, Console console) {
+ if (argsArray.length > 0) {
+ List<String> args = Arrays.asList(argsArray);
+ Integer instance = null;
+
+ // capture the common instance argument and take it out of args
+ for (int i = 0; i < args.size() - 1; ++i) {
+ if (PARAM_INSTANCE.equals(args.get(i))) {
+ instance = Integer.parseInt(args.get(i + 1));
+ int instanceCount = homekit.getInstanceCount();
+ if (instance < 1 || instance > instanceCount) {
+ console.println("Instance " + args.get(i + 1) + " out of range 1.." + instanceCount);
+ return;
+ }
+
+ List<String> newArgs = args.subList(0, i);
+ if (i < args.size() - 2) {
+ newArgs.addAll(args.subList(i + 2, args.size() - 1));
+ }
+ args = newArgs;
+ break;
+ }
+ }
+
+ String subCommand = args.get(0);
switch (subCommand) {
case SUBCMD_CLEAR_PAIRINGS:
- clearHomekitPairings(console);
+ if (args.size() != 1) {
+ console.println("Unknown arguments; not clearing pairings");
+ } else {
+ clearHomekitPairings(console, instance);
+ }
break;
case SUBCMD_ALLOW_UNAUTHENTICATED:
- if (args.length > 1) {
- boolean allow = Boolean.parseBoolean(args[1]);
+ if (args.size() > 1) {
+ boolean allow = Boolean.parseBoolean(args.get(1));
allowUnauthenticatedHomekitRequests(allow, console);
} else {
console.println("true/false is required as an argument");
}
break;
case SUBCMD_LIST_ACCESSORIES:
- listAccessories(console);
+ listAccessories(console, instance);
break;
case SUBCMD_PRINT_ACCESSORY:
- if (args.length > 1) {
- printAccessory(args[1], console);
+ if (args.size() > 1) {
+ printAccessory(args.get(1), console, instance);
} else {
console.println("accessory id or name is required as an argument");
}
break;
case SUBCMD_PRUNE_DUMMY_ACCESSORIES:
- pruneDummyAccessories(console);
+ if (args.size() != 1) {
+ console.println("Unknown arguments; not pruning dummy accessories");
+ } else {
+ pruneDummyAccessories(console, instance);
+ }
break;
case SUBCMD_LIST_DUMMY_ACCESSORIES:
- listDummyAccessories(console);
+ listDummyAccessories(console, instance);
break;
default:
console.println("Unknown command '" + subCommand + "'");
@Override
public List<String> getUsages() {
- return Arrays.asList(buildCommandUsage(SUBCMD_LIST_ACCESSORIES, "list all HomeKit accessories"),
- buildCommandUsage(SUBCMD_PRINT_ACCESSORY + " <accessory id | accessory name>",
- "print additional details of the accessories which partially match provided ID or name."),
- buildCommandUsage(SUBCMD_CLEAR_PAIRINGS, "removes all pairings with HomeKit clients."),
+ return Arrays.asList(
+ buildCommandUsage(SUBCMD_LIST_ACCESSORIES + PARAM_INSTANCE_HELP,
+ "list all HomeKit accessories, optionally for a specific instance."),
+ buildCommandUsage(SUBCMD_PRINT_ACCESSORY + PARAM_INSTANCE_HELP + " <accessory id | accessory name>",
+ "print additional details of the accessories which partially match provided ID or name, optionally searching a specific instance."),
+ buildCommandUsage(SUBCMD_CLEAR_PAIRINGS + PARAM_INSTANCE_HELP,
+ "removes all pairings with HomeKit clients, optionally for a specific instance."),
buildCommandUsage(SUBCMD_ALLOW_UNAUTHENTICATED + " <boolean>",
"enables or disables unauthenticated access to facilitate debugging"),
- buildCommandUsage(SUBCMD_PRUNE_DUMMY_ACCESSORIES,
- "removes dummy accessories whose items no longer exist."),
- buildCommandUsage(SUBCMD_LIST_DUMMY_ACCESSORIES,
- "list dummy accessories whose items no longer exist."));
+ buildCommandUsage(SUBCMD_PRUNE_DUMMY_ACCESSORIES + PARAM_INSTANCE_HELP,
+ "removes dummy accessories whose items no longer exist, optionally for a specific instance."),
+ buildCommandUsage(SUBCMD_LIST_DUMMY_ACCESSORIES + PARAM_INSTANCE_HELP,
+ "list dummy accessories whose items no longer exist, optionally for a specific instance."));
}
@Reference
return new CommandCompleter();
}
- private void clearHomekitPairings(Console console) {
- homekit.clearHomekitPairings();
- console.println("Cleared HomeKit pairings");
+ private void clearHomekitPairings(Console console, @Nullable Integer instance) {
+ if (instance != null) {
+ homekit.clearHomekitPairings(instance);
+ console.println("Cleared HomeKit pairings for instance " + instance);
+ } else {
+ homekit.clearHomekitPairings();
+ console.println("Cleared HomeKit pairings");
+ }
}
private void allowUnauthenticatedHomekitRequests(boolean allow, Console console) {
console.println((allow ? "Enabled " : "Disabled ") + "unauthenticated HomeKit access");
}
- private void pruneDummyAccessories(Console console) {
- homekit.pruneDummyAccessories();
- console.println("Dummy accessories pruned.");
+ private void pruneDummyAccessories(Console console, @Nullable Integer instance) {
+ if (instance != null) {
+ homekit.pruneDummyAccessories(instance);
+ console.println("Dummy accessories pruned for instance " + instance);
+ } else {
+ homekit.pruneDummyAccessories();
+ console.println("Dummy accessories pruned");
+ }
}
- private void listAccessories(Console console) {
- homekit.getAccessories().forEach(v -> {
+ private void listAccessories(Console console, @Nullable Integer instance) {
+ getInstanceAccessories(instance).forEach(v -> {
try {
console.println(v.getId() + " " + v.getName().get());
} catch (InterruptedException | ExecutionException e) {
});
}
- private void listDummyAccessories(Console console) {
- homekit.getAccessories().forEach(v -> {
+ private void listDummyAccessories(Console console, @Nullable Integer instance) {
+ getInstanceAccessories(instance).forEach(v -> {
try {
if (v instanceof DummyHomekitAccessory) {
console.println(v.getSerialNumber().get());
service.getLinkedServices().forEach((s) -> printService(console, s, indent + 2));
}
- private void printAccessory(String id, Console console) {
- homekit.getAccessories().forEach(v -> {
+ private void printAccessory(String id, Console console, @Nullable Integer instance) {
+ getInstanceAccessories(instance).forEach(v -> {
try {
if (("" + v.getId()).contains(id) || ((v.getName().get() != null)
&& (v.getName().get().toUpperCase().contains(id.toUpperCase())))) {
}
});
}
+
+ /**
+ * Get in-scope accessories
+ *
+ * @param instance if null, means all accessories from all instances
+ */
+ private Collection<HomekitAccessory> getInstanceAccessories(@Nullable Integer instance) {
+ if (instance != null) {
+ return homekit.getAccessories(instance);
+ } else {
+ return homekit.getAccessories();
+ }
+ }
}
import java.net.UnknownHostException;
import java.security.InvalidAlgorithmParameterException;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.ScheduledExecutorService;
import javax.jmdns.JmDNS;
public HomekitImpl(@Reference StorageService storageService, @Reference ItemRegistry itemRegistry,
@Reference NetworkAddressService networkAddressService, @Reference MetadataRegistry metadataRegistry,
@Reference ConfigurationAdmin configAdmin, @Reference MDNSClient mdnsClient,
- @Reference ReadyService readyService, Map<String, Object> properties)
- throws IOException, InvalidAlgorithmParameterException {
+ @Reference ReadyService readyService, Map<String, Object> properties) {
this.storageService = storageService;
this.networkAddressService = networkAddressService;
this.configAdmin = configAdmin;
|| !oldSettings.setupId.equals(settings.setupId)
|| (oldSettings.networkInterface != null
&& !oldSettings.networkInterface.equals(settings.networkInterface))
- || oldSettings.port != settings.port || oldSettings.useOHmDNS != settings.useOHmDNS
- || oldSettings.instances != settings.instances) {
+ || oldSettings.port != settings.port || oldSettings.useOHmDNS != settings.useOHmDNS) {
// the HomeKit server settings changed. we do a complete re-init
+ networkInterface = null;
+
+ // Clear out pairing info for instances that have been removed
+ for (int i = oldSettings.instances - 1; i >= settings.instances; --i) {
+ clearStorage(i);
+ }
stopHomekitServer();
if (currentStartLevel >= StartLevelService.STARTLEVEL_STATES) {
startHomekitServer();
}
} else {
+ // Stop removed instances
+ for (int i = oldSettings.instances - 1; i >= settings.instances; --i) {
+ clearStorage(i);
+ stopHomekitServer(i);
+ }
+ // Start up new instances
+ for (int i = oldSettings.instances; i < settings.instances; ++i) {
+ startHomekitServer(i);
+ }
+ // Notify remaining instances of the change
for (HomekitChangeListener changeListener : changeListeners) {
changeListener.updateSettings(settings);
}
return bridge;
}
- private void startHomekitServer() throws IOException, InvalidAlgorithmParameterException {
- logger.trace("start HomeKit bridge");
- if (homekitServers.isEmpty()) {
- try {
- networkInterface = InetAddress
- .getByName(((settings.networkInterface != null) && (!settings.networkInterface.isEmpty()))
- ? settings.networkInterface
- : networkAddressService.getPrimaryIpv4HostAddress());
- } catch (UnknownHostException e) {
- logger.warn("cannot resolve the Pv4 address / hostname {}.",
- networkAddressService.getPrimaryIpv4HostAddress());
+ private void startHomekitServer(int instance) throws IOException, InvalidAlgorithmParameterException {
+ logger.trace("starting HomeKit bridge instance {}", instance + 1);
+
+ InetAddress localNetworkInterface = ensureNetworkInterface();
+
+ String storageKey = HomekitAuthInfoImpl.STORAGE_KEY;
+ if (instance != 0) {
+ storageKey += instance;
+ }
+ Storage<Object> storage = storageService.getStorage(storageKey);
+ HomekitAuthInfoImpl authInfo = new HomekitAuthInfoImpl(storage, settings.pin, settings.setupId,
+ settings.blockUserDeletion);
+
+ @Nullable
+ HomekitServer homekitServer = null;
+ if (settings.useOHmDNS) {
+ for (JmDNS mdns : mdnsClient.getClientInstances()) {
+ if (mdns.getInetAddress().equals(localNetworkInterface)) {
+ logger.trace("suitable mDNS client for IP {} found and will be used for HomeKit",
+ localNetworkInterface);
+ homekitServer = new HomekitServer(mdns, settings.port + instance);
+ }
+ }
+ }
+ if (homekitServer == null) {
+ if (settings.useOHmDNS) {
+ logger.trace("no suitable mDNS server for IP {} found", localNetworkInterface);
}
+ logger.trace("create HomeKit server with dedicated mDNS server");
+ homekitServer = new HomekitServer(localNetworkInterface, settings.port + instance);
+ }
+ homekitServers.add(homekitServer);
+ HomekitChangeListener changeListener = new HomekitChangeListener(itemRegistry, settings, metadataRegistry,
+ storage, instance + 1);
+ changeListeners.add(changeListener);
+ startBridge(homekitServer, authInfo, changeListener, instance + 1);
+ authInfos.add(authInfo);
+ }
+ private void startHomekitServer() throws IOException, InvalidAlgorithmParameterException {
+ if (homekitServers.isEmpty()) {
for (int i = 0; i < settings.instances; ++i) {
- String storage_key = HomekitAuthInfoImpl.STORAGE_KEY;
- if (i != 0) {
- storage_key += i;
- }
- Storage<Object> storage = storageService.getStorage(storage_key);
- HomekitAuthInfoImpl authInfo = new HomekitAuthInfoImpl(storage, settings.pin, settings.setupId,
- settings.blockUserDeletion);
-
- @Nullable
- HomekitServer homekitServer = null;
- if (settings.useOHmDNS) {
- for (JmDNS mdns : mdnsClient.getClientInstances()) {
- if (mdns.getInetAddress().equals(networkInterface)) {
- logger.trace("suitable mDNS client for IP {} found and will be used for HomeKit",
- networkInterface);
- homekitServer = new HomekitServer(mdns, settings.port + i);
- }
- }
- }
- if (homekitServer == null) {
- if (settings.useOHmDNS) {
- logger.trace("no suitable mDNS server for IP {} found", networkInterface);
- }
- logger.trace("create HomeKit server with dedicated mDNS server");
- homekitServer = new HomekitServer(networkInterface, settings.port + i);
- }
- homekitServers.add(homekitServer);
- HomekitChangeListener changeListener = new HomekitChangeListener(itemRegistry, settings,
- metadataRegistry, storage, i + 1);
- changeListeners.add(changeListener);
- bridges.add(startBridge(homekitServer, authInfo, changeListener, i + 1));
- authInfos.add(authInfo);
+ startHomekitServer(i);
}
} else {
logger.warn("trying to start HomeKit server but it is already initialized");
}
}
+ private InetAddress ensureNetworkInterface() throws IOException {
+ InetAddress localNetworkInterface = networkInterface;
+ if (localNetworkInterface != null) {
+ return localNetworkInterface;
+ }
+
+ String interfaceName = ((settings.networkInterface != null) && (!settings.networkInterface.isEmpty()))
+ ? settings.networkInterface
+ : networkAddressService.getPrimaryIpv4HostAddress();
+ try {
+ return (networkInterface = Objects.requireNonNull(InetAddress.getByName(interfaceName)));
+ } catch (UnknownHostException e) {
+ logger.warn("cannot resolve the IPv4 address / hostname {}.", interfaceName);
+ throw e;
+ }
+ }
+
private void stopHomekitServer() {
- logger.trace("stop HomeKit bridge");
+ logger.trace("stopping HomeKit bridge");
changeListeners.parallelStream().forEach(HomekitChangeListener::stop);
bridges.parallelStream().forEach(HomekitRoot::stop);
homekitServers.parallelStream().forEach(HomekitServer::stop);
authInfos.clear();
}
+ private void stopHomekitServer(int instance) {
+ logger.trace("stopping HomeKit bridge instance {}", instance + 1);
+ changeListeners.get(instance).stop();
+ bridges.get(instance).stop();
+ homekitServers.get(instance).stop();
+ changeListeners.remove(instance);
+ bridges.remove(instance);
+ homekitServers.remove(instance);
+ authInfos.remove(instance);
+ }
+
+ private void clearStorage(int index) {
+ String storageKey = HomekitAuthInfoImpl.STORAGE_KEY;
+ if (index != 0) {
+ storageKey += index;
+ }
+ Storage<Object> storage = storageService.getStorage(storageKey);
+ storage.getKeys().forEach(k -> storage.remove(k));
+ }
+
@Deactivate
protected void deactivate() {
networkAddressService.removeNetworkAddressChangeListener(this);
}
@Override
- public List<HomekitAccessory> getAccessories() {
+ public Collection<HomekitAccessory> getAccessories() {
List<HomekitAccessory> accessories = new ArrayList<>();
for (HomekitChangeListener changeListener : changeListeners) {
accessories.addAll(changeListener.getAccessories().values());
return accessories;
}
+ @Override
+ public Collection<HomekitAccessory> getAccessories(int instance) {
+ if (instance < 1 || instance > changeListeners.size()) {
+ logger.warn("Instance {} is out of range 1..{}.", instance, changeListeners.size());
+ return List.of();
+ }
+
+ return changeListeners.get(instance - 1).getAccessories().values();
+ }
+
@Override
public void clearHomekitPairings() {
+ for (int i = 1; i <= authInfos.size(); ++i) {
+ clearHomekitPairings(i);
+ }
+ }
+
+ @Override
+ public void clearHomekitPairings(int instance) {
+ if (instance < 1 || instance > authInfos.size()) {
+ logger.warn("Instance {} is out of range 1..{}.", instance, authInfos.size());
+ return;
+ }
+
try {
- for (HomekitAuthInfoImpl authInfo : authInfos) {
- authInfo.clear();
- }
- refreshAuthInfo();
+ authInfos.get(instance - 1).clear();
+ bridges.get(instance - 1).refreshAuthInfo();
} catch (Exception e) {
logger.warn("could not clear HomeKit pairings", e);
}
}
}
+ @Override
+ public void pruneDummyAccessories(int instance) {
+ if (instance < 1 || instance > authInfos.size()) {
+ logger.warn("Instance {} is out of range 1..{}.", instance, authInfos.size());
+ return;
+ }
+
+ changeListeners.get(instance - 1).pruneDummyAccessories();
+ }
+
+ @Override
+ public int getInstanceCount() {
+ return homekitServers.size();
+ }
+
@Override
public synchronized void onChanged(final List<CidrAddress> added, final List<CidrAddress> removed) {
logger.trace("HomeKit bridge reacting on network interface changes.");