]> git.basschouten.com Git - openhab-addons.git/commitdiff
[homekit] Improve multiple instance management (#14016)
authorCody Cutrer <cody@cutrer.us>
Tue, 3 Jan 2023 22:10:42 +0000 (15:10 -0700)
committerGitHub <noreply@github.com>
Tue, 3 Jan 2023 22:10:42 +0000 (23:10 +0100)
* [homekit] improve instance management

 * allow addressing individual instances for most console commands
 * don't restart all instances if simply adding/removing instances on
   config change
 * clear stored info when removing instances

* [homekit] reset instance identity when clearing pairings
* [homekit] log the actual interface we looked up

Signed-off-by: Cody Cutrer <cody@cutrer.us>
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/Homekit.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandExtension.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitImpl.java

index 460256bbd150b0fad41398b0ef2733c8835a7409..96894b8b638f576b3fa3e755cab35670f5ea6e3c 100644 (file)
@@ -13,7 +13,7 @@
 package org.openhab.io.homekit;
 
 import java.io.IOException;
-import java.util.List;
+import java.util.Collection;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 
@@ -45,17 +45,43 @@ public interface Homekit {
     void allowUnauthenticatedRequests(boolean allow);
 
     /**
-     * returns list of HomeKit accessories registered at bridge.
+     * returns list of HomeKit accessories registered on all bridge instances.
      */
-    List<HomekitAccessory> getAccessories();
+    Collection<HomekitAccessory> getAccessories();
 
     /**
-     * clear all pairings with HomeKit clients
+     * returns list of HomeKit accessories registered on a specific instance.
+     */
+    Collection<HomekitAccessory> getAccessories(int instance);
+
+    /**
+     * clear all pairings with HomeKit clients on all bridge instances.
      */
     void clearHomekitPairings();
 
+    /**
+     * clear all pairings with HomeKit clients for a specific instance.
+     * 
+     * @param instance the instance number (1-based)
+     */
+    void clearHomekitPairings(int instance);
+
     /**
      * Prune dummy accessories (accessories that no longer have associated items)
+     * on all bridge instances.
      */
     void pruneDummyAccessories();
+
+    /**
+     * Prune dummy accessories (accessories that no longer have associated items)
+     * for a specific instance
+     *
+     * @param instance the instance number (1-based)
+     */
+    void pruneDummyAccessories(int instance);
+
+    /**
+     * returns how many bridge instances there are
+     */
+    int getInstanceCount();
 }
index 57904c2737232ff45a6fe3b240eae1c1263fb7fe..3d89d47d14873d1466e0ab08dc836772189a8db2 100644 (file)
@@ -147,15 +147,25 @@ public class HomekitAuthInfoImpl implements HomekitAuthInfo {
     }
 
     public void clear() {
-        logger.trace("clear all users");
         if (!this.blockUserDeletion) {
             for (String key : new HashSet<>(storage.getKeys())) {
                 if (isUserKey(key)) {
                     storage.remove(key);
                 }
             }
+            mac = HomekitServer.generateMac();
+            storage.put(STORAGE_MAC, mac);
+            storage.remove(STORAGE_SALT);
+            storage.remove(STORAGE_PRIVATE_KEY);
+            try {
+                initializeStorage();
+                logger.info("All users cleared from HomeKit bridge; re-pairing required.");
+            } catch (InvalidAlgorithmParameterException e) {
+                logger.warn(
+                        "Failed generating new encryption settings for HomeKit bridge; re-pairing required, but will likely fail.");
+            }
         } else {
-            logger.debug("deletion of users information was blocked by binding settings");
+            logger.warn("Deletion of HomeKit users was blocked by addon settings.");
         }
     }
 
index 4cc5c3446ebad45707c46315cd46820cfdc40afc..6d521731d5f824bdc4f48fc95b498090ef0659f4 100644 (file)
@@ -13,6 +13,7 @@
 package org.openhab.io.homekit.internal;
 
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 
@@ -30,6 +31,7 @@ import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import io.github.hapjava.accessories.HomekitAccessory;
 import io.github.hapjava.services.Service;
 
 /**
@@ -51,6 +53,9 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension {
                     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) {
@@ -70,37 +75,67 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension {
     }
 
     @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 + "'");
@@ -114,16 +149,19 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension {
 
     @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
@@ -136,9 +174,14 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension {
         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) {
@@ -146,13 +189,18 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension {
         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) {
@@ -161,8 +209,8 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension {
         });
     }
 
-    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());
@@ -191,8 +239,8 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension {
         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())))) {
@@ -206,4 +254,17 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension {
             }
         });
     }
+
+    /**
+     * 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();
+        }
+    }
 }
index 8353bedc121b2bb54dc61789b57e563ea5536fca..ebbe685101a8f45497a4cf3a37c7ed574565b93f 100644 (file)
@@ -17,10 +17,12 @@ import java.net.InetAddress;
 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;
@@ -96,8 +98,7 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener, Ready
     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;
@@ -160,14 +161,29 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener, Ready
                     || !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);
                 }
@@ -212,60 +228,74 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener, Ready
         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);
@@ -275,6 +305,26 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener, Ready
         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);
@@ -296,7 +346,7 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener, Ready
     }
 
     @Override
-    public List<HomekitAccessory> getAccessories() {
+    public Collection<HomekitAccessory> getAccessories() {
         List<HomekitAccessory> accessories = new ArrayList<>();
         for (HomekitChangeListener changeListener : changeListeners) {
             accessories.addAll(changeListener.getAccessories().values());
@@ -304,13 +354,33 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener, Ready
         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);
         }
@@ -323,6 +393,21 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener, Ready
         }
     }
 
+    @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.");