]> git.basschouten.com Git - openhab-addons.git/commitdiff
[homekit] persist all known accessories, to prevent loss of homekit information ...
authorCody Cutrer <cody@cutrer.us>
Thu, 8 Dec 2022 12:52:30 +0000 (05:52 -0700)
committerGitHub <noreply@github.com>
Thu, 8 Dec 2022 12:52:30 +0000 (13:52 +0100)
* [homekit] persist all known accessories, to prevent loss of homekit information

See the readme for more details, but basically this keeps track of every
accessory we've ever created, and if it no longer exists, presents a dummy
accessory instead. If the accessory comes back, nothing is lost in the Home
app; if you meant to prune it permanently, you have to run a console command.

there are also several fixes to prevent presenting the device with missing data -
such as when the bundle stops, _don't_ explicitly remove the accessories until
the server has stopped.

we also don't increment the configuration version unless the configuration
has _actually_ changed (so removing and re-adding the exact same thing
won't trigger the device to reconnect). this even works across restarts
of the bundle, because we're persisting all the accessory information
for dummy information anyway.

* [homekit] Address review comments for accessory persistence

* update hap-java to 2.0.4
* remove unused local variable
* [homekit] ensure accessories are replaced in a batch

so that HAP-Java can maintain subscriptions with the new objects

* [homekit] log individual dummy accessories up to 5
* [homekit] Tweak readme for dummy accessories slightly.

 * Be consistent with proper usage of useDummyAccessories.
 * Make the sentence more clear about the effects of having dummy accessories.

Signed-off-by: Cody Cutrer <cody@cutrer.us>
17 files changed:
bundles/org.openhab.io.homekit/README.md
bundles/org.openhab.io.homekit/pom.xml
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/Debouncer.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryRegistry.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/HomekitChangeListener.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
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitSettings.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/BooleanItemReader.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/DummyHomekitAccessory.java [new file with mode: 0644]
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java
bundles/org.openhab.io.homekit/src/main/resources/OH-INF/config/config.xml
bundles/org.openhab.io.homekit/src/main/resources/OH-INF/i18n/homekit.properties

index 71d48f186efbdf6dc79e1c126aeeaf71609051e2..475802dd5492b2e678ddbf779a22e30a966d5594 100644 (file)
@@ -72,7 +72,7 @@ HomeKit integration supports following accessory types:
   
   ![ios_add_accessory_wizard.png](doc/ios_add_accessory_wizard.png)
   
-Add metadata to more item or fine-tune your configuration using further settings
+Add metadata to more items or fine-tune your configuration using further settings
 
 
 ## Global Configuration
@@ -98,6 +98,7 @@ org.openhab.homekit:useOHmDNS=false
 org.openhab.homekit:blockUserDeletion=false
 org.openhab.homekit:name=openHAB
 org.openhab.homekit:instances=1
+org.openhab.homekit:useDummyAccessories=false
 ```
 
 Some settings are only visible in UI if the checkbox "Show advanced" is activated.
@@ -108,10 +109,10 @@ Some settings are only visible in UI if the checkbox "Show advanced" is activate
 |:-------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------|
 | networkInterface         | IP address or domain name under which the HomeKit bridge can be reached. If no value is configured, the add-on uses the primary IP address configured for openHAB. If unsure, keep it empty                                                                                                       | (none)        |
 | port                     | Port under which the HomeKit bridge can be reached.                                                                                                                                                                                                                                               | 9123          |
-| useOHmDNS                | mDNS service is used to advertise openHAB as HomeKit bridge in the network so that HomeKit clients can find it. openHAB has already mDNS service running. This option defines whether the mDNS service of openHAB or a separate service should be used.                                           | false  |
+| useOHmDNS                | mDNS service is used to advertise openHAB as HomeKit bridge in the network so that HomeKit clients can find it. openHAB has already mDNS service running. This option defines whether the mDNS service of openHAB or a separate service should be used.                                           | false         |
 | blockUserDeletion        | Blocks HomeKit user deletion in openHAB and as result unpairing of devices. If you experience an issue with accessories becoming non-responsive after some time, try to enable this setting. You can also enable this setting if your HomeKit setup is done and you will not re-pair ios devices. | false         |
 | 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. Ignored if `useDummyAccessories` is on.                                   | 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         |
 | 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        |
@@ -119,6 +120,7 @@ Some settings are only visible in UI if the checkbox "Show advanced" is activate
 | 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       |
 | 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             |
+| useDummyAccessories      | When an accessory is missing, substitute a dummy in its place instead of removing it. See [Dummy Accessories](#dummy-accessories). | false     |
 
 ## Item Configuration
 
@@ -208,6 +210,22 @@ Switch light1 "Light 1" (gLight) {homekit="Lighting.OnState"}
 Switch light2 "Light 2" (gLight) {homekit="Lighting.OnState"}
 ```
 
+## Dummy Accessories
+
+OpenHAB is a highly dynamic system, and prone to occasional misconfigurations where items can't be loaded for various reasons, especially if you're using something besides the UI to manage your items.
+This is a problem for Homekit because if the bridge makes a connection, but accessories are missing, then the Homekit database will simply remove that accessory.
+When the accessory does come back (i.e. because you corrected a syntax error in an .items file, or OpenHAB completes booting), all customization of that accessory will be lost - the room assignment, customized  name, custom icon, status/home screen/favorite preferences, etc.
+In order to work around this, the Homekit addon can create dummy accessories for any accessory it has previously published to Homekit.
+To enable this behavior, turn on the `useDummyAccessories` setting.
+OpenHAB will then simply present a non-interactive accessory for any that are missing.
+The OpenHAB log will also contain information whenever a dummy accessory is created.
+If the item backing the accessory is later re-created, everything will sync back up and nothing will be lost.
+You can also run the console command `openhab:homekit listDummyAccessories` to see which items are missing.
+Apple devices may or may not show "Not Responding" for some or all accessories when there are dummy accessories, since they will no longer be backed by actual items with state.
+It's recommended that you resolve this state as soon as possible, since Homekit may decide your entire bridge is being uncooperative, and remove everything itself.
+If you actually meant to remove an item, you will need to purge the dummy items from the database so that they'll disappear from the Home app altogether.
+In order to do so, run the console command `openhab:homekit pruneDummyAccessories`.
+Alternatively, disabling, saving, and then re-enabling `useDummyAccessories` in the addon settings will have the same effect.
 
 ## Accessory Configuration Details
 
index 1674048644485a6402923a21115854fc6088fe47..3893f87398c83620646d50dd24347a16ce010755 100644 (file)
@@ -22,7 +22,7 @@
     <dependency>
       <groupId>io.github.hap-java</groupId>
       <artifactId>hap</artifactId>
-      <version>2.0.1</version>
+      <version>2.0.4</version>
       <scope>compile</scope>
     </dependency>
     <dependency>
index ce0a31637a836c1ec9ab1036522821dcbff8d4cd..460256bbd150b0fad41398b0ef2733c8835a7409 100644 (file)
@@ -53,4 +53,9 @@ public interface Homekit {
      * clear all pairings with HomeKit clients
      */
     void clearHomekitPairings();
+
+    /**
+     * Prune dummy accessories (accessories that no longer have associated items)
+     */
+    void pruneDummyAccessories();
 }
index 00e2e921c8fe6145812670341799e486eef63228..4e0922e5d88e3e79cf385d4c507ee57a98214d1b 100644 (file)
@@ -101,7 +101,7 @@ class Debouncer {
                 try {
                     action.run();
                 } catch (Exception e) {
-                    logger.warn("Debouncer {} action resulted in error: {}", name, e.getMessage());
+                    logger.warn("Debouncer {} action resulted in error", name, e);
                 }
             } else {
                 logger.warn("Invalid state in debouncer. Should not have reached here!");
index ee7296dfd664dd1705ed5888a756b683e1e03432..ee941626f362e489e104ddecfd8a3115656b7785 100644 (file)
@@ -84,13 +84,13 @@ class HomekitAccessoryRegistry {
     }
 
     public synchronized void unsetBridge() {
-        final HomekitRoot oldBridge = bridge;
-        if (oldBridge != null) {
-            createdAccessories.values().forEach(oldBridge::removeAccessory);
-        }
         bridge = null;
     }
 
+    public synchronized HomekitRoot getBridge() {
+        return bridge;
+    }
+
     public synchronized void addRootAccessory(String itemName, HomekitAccessory accessory) {
         createdAccessories.put(itemName, accessory);
         final HomekitRoot bridge = this.bridge;
index 8a3cafc6db6cd8c9500e668c93ced021be69a386..ba538f8a7cc471380b963c6f3fbbac305bfa1f83 100644 (file)
@@ -40,7 +40,7 @@ public class HomekitAuthInfoImpl implements HomekitAuthInfo {
     private static final String STORAGE_PRIVATE_KEY = "privateKey";
     private static final String STORAGE_USER_PREFIX = "user_";
 
-    private final Storage<String> storage;
+    private final Storage<Object> storage;
     private String mac;
     private BigInteger salt;
     private byte[] privateKey;
@@ -48,7 +48,7 @@ public class HomekitAuthInfoImpl implements HomekitAuthInfo {
     private String setupId;
     private boolean blockUserDeletion;
 
-    public HomekitAuthInfoImpl(Storage<String> storage, String pin, String setupId, boolean blockUserDeletion)
+    public HomekitAuthInfoImpl(Storage<Object> storage, String pin, String setupId, boolean blockUserDeletion)
             throws InvalidAlgorithmParameterException {
         this.storage = storage;
         this.pin = pin;
@@ -105,7 +105,7 @@ public class HomekitAuthInfoImpl implements HomekitAuthInfo {
 
     @Override
     public byte[] getUserPublicKey(String username) {
-        final String encodedKey = storage.get(createUserKey(username));
+        final String encodedKey = (String) storage.get(createUserKey(username));
         if (encodedKey != null) {
             return Base64.getDecoder().decode(encodedKey);
         } else {
@@ -151,7 +151,7 @@ public class HomekitAuthInfoImpl implements HomekitAuthInfo {
     }
 
     private void initializeStorage() throws InvalidAlgorithmParameterException {
-        mac = storage.get(STORAGE_MAC);
+        mac = (String) storage.get(STORAGE_MAC);
         final @Nullable Object saltConfig = storage.get(STORAGE_SALT);
         final @Nullable Object privateKeyConfig = storage.get(STORAGE_PRIVATE_KEY);
         if (mac == null) {
index c1e0e46e1399f50369679fa0644fa74cd381a4c0..970669b494f742006ec20b9c3fabc59533b29e87 100644 (file)
@@ -14,14 +14,18 @@ package org.openhab.io.homekit.internal;
 
 import java.time.Clock;
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.stream.Collectors;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -36,6 +40,8 @@ import org.openhab.core.items.Metadata;
 import org.openhab.core.items.MetadataKey;
 import org.openhab.core.items.MetadataRegistry;
 import org.openhab.core.storage.Storage;
+import org.openhab.io.homekit.internal.accessories.AbstractHomekitAccessoryImpl;
+import org.openhab.io.homekit.internal.accessories.DummyHomekitAccessory;
 import org.openhab.io.homekit.internal.accessories.HomekitAccessoryFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -54,15 +60,18 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
     private final Logger logger = LoggerFactory.getLogger(HomekitChangeListener.class);
     private final static String REVISION_CONFIG = "revision";
     private final static String ACCESSORY_COUNT = "accessory_count";
+    private final static String KNOWN_ACCESSORIES = "known_accessories";
     private final ItemRegistry itemRegistry;
     private final HomekitAccessoryRegistry accessoryRegistry = new HomekitAccessoryRegistry();
     private final MetadataRegistry metadataRegistry;
-    private final Storage<String> storage;
+    private final Storage<Object> storage;
     private final RegistryChangeListener<Metadata> metadataChangeListener;
     private HomekitAccessoryUpdater updater = new HomekitAccessoryUpdater();
     private HomekitSettings settings;
     private int lastAccessoryCount;
+    private Map<String, String> knownAccessories = new HashMap<>();
     private int instance;
+    private List<String> priorDummies = new ArrayList<>();
 
     private final Set<String> pendingUpdates = new HashSet<>();
 
@@ -80,14 +89,14 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
     private final Debouncer applyUpdatesDebouncer;
 
     HomekitChangeListener(ItemRegistry itemRegistry, HomekitSettings settings, MetadataRegistry metadataRegistry,
-            Storage<String> storage, int instance) {
+            Storage<Object> storage, int instance) {
         this.itemRegistry = itemRegistry;
         this.settings = settings;
         this.metadataRegistry = metadataRegistry;
         this.storage = storage;
         this.instance = instance;
-        this.applyUpdatesDebouncer = new Debouncer("update-homekit-devices", scheduler, Duration.ofMillis(1000),
-                Clock.systemUTC(), this::applyUpdates);
+        this.applyUpdatesDebouncer = new Debouncer("update-homekit-devices-" + instance, scheduler,
+                Duration.ofMillis(1000), Clock.systemUTC(), this::applyUpdates);
         metadataChangeListener = new RegistryChangeListener<Metadata>() {
             @Override
             public void added(final Metadata metadata) {
@@ -96,7 +105,7 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
                     try {
                         markDirty(itemRegistry.getItem(uid.getItemName()));
                     } catch (ItemNotFoundException e) {
-                        logger.debug("Could not find item for metadata {}", metadata);
+                        logger.trace("Could not find item for metadata {}", metadata);
                     }
                 }
             }
@@ -108,7 +117,7 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
                     try {
                         markDirty(itemRegistry.getItem(uid.getItemName()));
                     } catch (ItemNotFoundException e) {
-                        logger.debug("Could not find item for metadata {}", metadata);
+                        logger.trace("Could not find item for metadata {}", metadata);
                     }
                 }
             }
@@ -130,35 +139,57 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
         };
         itemRegistry.addRegistryChangeListener(this);
         metadataRegistry.addRegistryChangeListener(metadataChangeListener);
-        itemRegistry.getItems().forEach(this::createRootAccessories);
         initialiseRevision();
-        makeNewConfigurationRevision();
-        logger.info("Created {} HomeKit items in instance {}.", accessoryRegistry.getAllAccessories().size(), instance);
+        boolean changed = false;
+        for (var i : itemRegistry.getItems()) {
+            String oldValue = knownAccessories.get(i.getName());
+            createRootAccessories(i);
+            if (accessoryChanged(i.getName(), oldValue)) {
+                logger.debug("Accessory {} changed:\n{}\n{}", i.getName(), oldValue, knownAccessories.get(i.getName()));
+                changed = true;
+            }
+        }
+        // order of this conditional is important - checkMissingAccessories has side effects that need to always happen
+        if (checkMissingAccessories() || changed) {
+            makeNewConfigurationRevision();
+        } else {
+            logger.info("Created {} HomeKit items in instance {} (no change from prior configuration).",
+                    accessoryRegistry.getAllAccessories().size(), instance);
+            if (settings.useDummyAccessories) {
+                checkForDummyAccessories();
+            }
+        }
     }
 
     private void initialiseRevision() {
-        int revision;
+        int revision = 1;
         try {
-            String revisionString = storage.get(REVISION_CONFIG);
+            String revisionString = (String) storage.get(REVISION_CONFIG);
             if (revisionString == null) {
                 throw new NumberFormatException();
             }
             revision = Integer.parseInt(revisionString);
         } catch (NumberFormatException e) {
-            revision = 1;
-            storage.put(REVISION_CONFIG, "" + revision);
         }
-        try {
-            String accessoryCountString = storage.get(ACCESSORY_COUNT);
-            if (accessoryCountString == null) {
-                throw new NumberFormatException();
+        accessoryRegistry.setConfigurationRevision(revision);
+
+        lastAccessoryCount = 0;
+        var localKnownAccessories = (Map<String, String>) storage.get(KNOWN_ACCESSORIES);
+        if (localKnownAccessories == null) {
+            knownAccessories = new HashMap<>();
+            // Back-compat
+            try {
+                String accessoryCountString = (String) storage.get(ACCESSORY_COUNT);
+                if (accessoryCountString == null) {
+                    throw new NumberFormatException();
+                }
+                lastAccessoryCount = Integer.parseInt(accessoryCountString);
+            } catch (NumberFormatException e) {
             }
-            lastAccessoryCount = Integer.parseInt(accessoryCountString);
-        } catch (NumberFormatException e) {
-            lastAccessoryCount = 0;
-            storage.put(ACCESSORY_COUNT, "" + accessoryRegistry.getAllAccessories().size());
+        } else {
+            knownAccessories = localKnownAccessories;
+            lastAccessoryCount = knownAccessories.size();
         }
-        accessoryRegistry.setConfigurationRevision(revision);
     }
 
     private boolean hasHomeKitMetadata(Item item) {
@@ -221,25 +252,74 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
     public void makeNewConfigurationRevision() {
         final int newRevision = accessoryRegistry.makeNewConfigurationRevision();
         lastAccessoryCount = accessoryRegistry.getAllAccessories().size();
-        logger.trace("Make new configuration revision. new revision number {}, number of accessories {}", newRevision,
-                lastAccessoryCount);
+        logger.info("Created {} HomeKit items in instance {}.", accessoryRegistry.getAllAccessories().size(), instance);
+        logger.trace("Making new configuration revision {}", newRevision);
         storage.put(REVISION_CONFIG, "" + newRevision);
-        storage.put(ACCESSORY_COUNT, "" + lastAccessoryCount);
+        storage.put(KNOWN_ACCESSORIES, knownAccessories);
+    }
+
+    public synchronized void pruneDummyAccessories() {
+        boolean removed = false;
+        for (HomekitAccessory accessory : accessoryRegistry.getAllAccessories().values()
+                .toArray(new HomekitAccessory[0])) {
+            if (accessory instanceof DummyHomekitAccessory) {
+                try {
+                    String name = accessory.getName().get();
+                    logger.info("Pruning dummy accessory {}.", name);
+                    knownAccessories.remove(name);
+                    accessoryRegistry.remove(name);
+                    removed = true;
+                } catch (ExecutionException | InterruptedException e) {
+                    // will never happen; it's a always completed future
+                }
+            }
+        }
+        if (removed) {
+            makeNewConfigurationRevision();
+        }
     }
 
     private synchronized void applyUpdates() {
         logger.trace("Apply updates");
-        for (final String name : pendingUpdates) {
-            accessoryRegistry.remove(name);
-            logger.trace(" Add items {}", name);
-            getItemOptional(name).ifPresent(this::createRootAccessories);
+
+        HomekitRoot bridge = accessoryRegistry.getBridge();
+        if (bridge != null) {
+            bridge.batchUpdate();
         }
-        if (!pendingUpdates.isEmpty()) {
-            makeNewConfigurationRevision();
+
+        try {
+            boolean changed = false;
+            boolean removed = false;
+            for (final String name : pendingUpdates) {
+                String oldValue = knownAccessories.get(name);
+                accessoryRegistry.remove(name);
+                logger.trace(" Add items {}", name);
+                getItemOptional(name).ifPresent(this::createRootAccessories);
+                if (accessoryChanged(name, oldValue)) {
+                    changed = true;
+                }
+            }
             pendingUpdates.clear();
+            if (checkMissingAccessories() || changed) {
+                makeNewConfigurationRevision();
+            }
+            checkForDummyAccessories();
+        } finally {
+            if (bridge != null) {
+                bridge.completeUpdateBatch();
+            }
         }
     }
 
+    private boolean accessoryChanged(String name, @Nullable String oldValue) {
+        String newValue = knownAccessories.get(name);
+        if (oldValue == null && newValue == null) {
+            return false;
+        }
+        return oldValue == null && newValue != null || oldValue != null && newValue == null
+                || !oldValue.equals(newValue);
+    }
+
     @Override
     public void updated(Item oldElement, Item element) {
         markDirty(oldElement);
@@ -263,7 +343,12 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
     }
 
     public void updateSettings(HomekitSettings settings) {
+        boolean wasUsingDummyAccessories = this.settings.useDummyAccessories;
         this.settings = settings;
+        // If they turned off dummy accessories, immediately prune them
+        if (wasUsingDummyAccessories && !settings.useDummyAccessories) {
+            pruneDummyAccessories();
+        }
     }
 
     public synchronized void stop() {
@@ -369,8 +454,8 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
         final HomekitTaggedItem taggedItem = new HomekitTaggedItem(new HomekitOHItemProxy(item), primaryAccessoryType,
                 itemConfiguration);
         try {
-            final HomekitAccessory accessory = HomekitAccessoryFactory.create(taggedItem, metadataRegistry, updater,
-                    settings);
+            final AbstractHomekitAccessoryImpl accessory = HomekitAccessoryFactory.create(taggedItem, metadataRegistry,
+                    updater, settings);
 
             accessoryTypes.stream().filter(aType -> !primaryAccessoryType.equals(aType.getKey()))
                     .forEach(additionalAccessoryType -> {
@@ -384,6 +469,7 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
                             logger.warn("Cannot create additional accessory {}", additionalTaggedItem);
                         }
                     });
+            knownAccessories.put(taggedItem.getName(), accessory.toJson());
             accessoryRegistry.addRootAccessory(taggedItem.getName(), accessory);
         } catch (HomekitException e) {
             logger.warn("Cannot create accessory {}", taggedItem);
@@ -407,4 +493,74 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
                 value.getClass(), item.getName());
         return (instance == 1);
     }
+
+    /**
+     * Check for any missing accessories.
+     *
+     * If there are, return true so we know to increment the config version. UNLESS
+     * we're configured to use dummy accessories, in which case backfill it with a dummy.
+     *
+     * @return if we need to increment the configuration version
+     */
+    private boolean checkMissingAccessories() {
+        List<String> toRemove = new ArrayList<>();
+        for (Map.Entry<String, String> accessory : knownAccessories.entrySet()) {
+            if (!accessoryRegistry.getAllAccessories().containsKey(accessory.getKey())) {
+                if (settings.useDummyAccessories) {
+                    logger.debug("Creating dummy accessory for missing item {}.", accessory.getKey());
+                    accessoryRegistry.addRootAccessory(accessory.getKey(),
+                            new DummyHomekitAccessory(accessory.getKey(), accessory.getValue()));
+                } else {
+                    toRemove.add(accessory.getKey());
+                }
+            }
+        }
+
+        toRemove.forEach(k -> knownAccessories.remove(k));
+        return !toRemove.isEmpty();
+    }
+
+    private void checkForDummyAccessories() {
+        List<String> currentDummies = accessoryRegistry.getAllAccessories().values().stream()
+                .filter(a -> a instanceof DummyHomekitAccessory).map(a -> {
+                    try {
+                        return a.getSerialNumber().get();
+                    } catch (InterruptedException | ExecutionException e) {
+                        return "<unknown>";
+                    }
+                }).collect(Collectors.toList());
+
+        List<String> resolvedDummies = new ArrayList(priorDummies);
+        resolvedDummies.removeAll(currentDummies);
+        List<String> newDummies = new ArrayList(currentDummies);
+        newDummies.removeAll(priorDummies);
+
+        if (resolvedDummies.size() <= 5) {
+            for (String item : resolvedDummies) {
+                logger.info("{} has been resolved to an actual accessory, and is no longer a dummy.", item);
+            }
+        } else if (currentDummies.isEmpty() && !resolvedDummies.isEmpty()) {
+            logger.info("All dummy accessories have been resolved to actual accessories.");
+        } else if (!resolvedDummies.isEmpty()) {
+            logger.info("{} dummy accessories have been resolved to actual accessories.", resolvedDummies.size());
+        }
+
+        if (newDummies.size() <= 5) {
+            for (String item : newDummies) {
+                logger.warn(
+                        "{} has been replaced with a dummy. See https://www.openhab.org/addons/integrations/homekit/#dummy-accessories for more information.",
+                        item);
+            }
+        } else if (!newDummies.isEmpty()) {
+            logger.warn(
+                    "{} accessories have been replaced with dummies. See https://www.openhab.org/addons/integrations/homekit/#dummy-accessories for more information.",
+                    newDummies.size());
+        } else if (!currentDummies.isEmpty()) {
+            logger.warn(
+                    "{} accessories are still dummies. See https://www.openhab.org/addons/integrations/homekit/#dummy-accessories for more information.",
+                    currentDummies.size());
+        }
+        priorDummies.clear();
+        priorDummies.addAll(currentDummies);
+    }
 }
index 0904832023f2ae9d905cf4b80b73b4c2d845680d..c9b8b79213cc528e8cb15530a20ef8d3ff5d2a48 100644 (file)
@@ -21,6 +21,7 @@ import org.openhab.core.io.console.Console;
 import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
 import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
 import org.openhab.io.homekit.Homekit;
+import org.openhab.io.homekit.internal.accessories.DummyHomekitAccessory;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
@@ -40,6 +41,8 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension {
     private static final String SUBCMD_LIST_ACCESSORIES = "list";
     private static final String SUBCMD_PRINT_ACCESSORY = "show";
     private static final String SUBCMD_ALLOW_UNAUTHENTICATED = "allowUnauthenticated";
+    private static final String SUBCMD_PRUNE_DUMMY_ACCESSORIES = "pruneDummyAccessories";
+    private static final String SUBCMD_LIST_DUMMY_ACCESSORIES = "listDummyAccessories";
 
     private final Logger logger = LoggerFactory.getLogger(HomekitCommandExtension.class);
 
@@ -76,6 +79,12 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension {
                         console.println("accessory id or name is required as an argument");
                     }
                     break;
+                case SUBCMD_PRUNE_DUMMY_ACCESSORIES:
+                    pruneDummyAccessories(console);
+                    break;
+                case SUBCMD_LIST_DUMMY_ACCESSORIES:
+                    listDummyAccessories(console);
+                    break;
                 default:
                     console.println("Unknown command '" + subCommand + "'");
                     printUsage(console);
@@ -93,7 +102,11 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension {
                         "print additional details of the accessories which partially match provided ID or name."),
                 buildCommandUsage(SUBCMD_CLEAR_PAIRINGS, "removes all pairings with HomeKit clients."),
                 buildCommandUsage(SUBCMD_ALLOW_UNAUTHENTICATED + " <boolean>",
-                        "enables or disables unauthenticated access to facilitate debugging"));
+                        "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."));
     }
 
     @Reference
@@ -111,6 +124,11 @@ 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 listAccessories(Console console) {
         homekit.getAccessories().forEach(v -> {
             try {
@@ -121,6 +139,18 @@ public class HomekitCommandExtension extends AbstractConsoleCommandExtension {
         });
     }
 
+    private void listDummyAccessories(Console console) {
+        homekit.getAccessories().forEach(v -> {
+            try {
+                if (v instanceof DummyHomekitAccessory) {
+                    console.println(v.getSerialNumber().get());
+                }
+            } catch (InterruptedException | ExecutionException e) {
+                logger.warn("Cannot list accessories", e);
+            }
+        });
+    }
+
     private void printService(Console console, Service service, int indent) {
         console.println(" ".repeat(indent) + "Service Type: " + service.getClass().getSimpleName() + " ("
                 + service.getType() + ")");
index 5fbb3f3bc3f5cee5255ac217f23bfd6c3a2580ea..294ba08d1cdeb7fbc2fab83226361800119a8981 100644 (file)
@@ -184,9 +184,10 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener {
         bridges.add(bridge);
         bridge.setConfigurationIndex(changeListener.getConfigurationRevision());
         bridge.refreshAuthInfo();
+
         final int lastAccessoryCount = changeListener.getLastAccessoryCount();
         int currentAccessoryCount = changeListener.getAccessories().size();
-        if (currentAccessoryCount < lastAccessoryCount) {
+        if (!settings.useDummyAccessories && currentAccessoryCount < lastAccessoryCount) {
             logger.debug(
                     "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.",
                     lastAccessoryCount, currentAccessoryCount, settings.startDelay);
@@ -222,7 +223,7 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener {
                 if (i != 0) {
                     storage_key += i;
                 }
-                Storage<String> storage = storageService.getStorage(storage_key);
+                Storage<Object> storage = storageService.getStorage(storage_key);
                 HomekitAuthInfoImpl authInfo = new HomekitAuthInfoImpl(storage, settings.pin, settings.setupId,
                         settings.blockUserDeletion);
 
@@ -270,9 +271,6 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener {
     @Deactivate
     protected void deactivate() {
         networkAddressService.removeNetworkAddressChangeListener(this);
-        for (HomekitChangeListener changeListener : changeListeners) {
-            changeListener.clearAccessories();
-        }
         stopHomekitServer();
     }
 
@@ -311,6 +309,13 @@ public class HomekitImpl implements Homekit, NetworkAddressChangeListener {
         }
     }
 
+    @Override
+    public void pruneDummyAccessories() {
+        for (HomekitChangeListener changeListener : changeListeners) {
+            changeListener.pruneDummyAccessories();
+        }
+    }
+
     @Override
     public synchronized void onChanged(final List<CidrAddress> added, final List<CidrAddress> removed) {
         logger.trace("HomeKit bridge reacting on network interface changes.");
index 9529187563a0dcdd94b05e8681524d4aacac8f1b..0f0a7dc5d6698af234da15b5226c9c544a56fb0d 100644 (file)
@@ -31,6 +31,7 @@ public class HomekitSettings {
     public String setupId;
     public String qrCode;
     public int startDelay = 30;
+    public boolean useDummyAccessories = false;
     public boolean useFahrenheitTemperature = false;
     public boolean useOHmDNS = false;
     public boolean blockUserDeletion = false;
@@ -62,6 +63,7 @@ public class HomekitSettings {
         result = prime * result + ((thermostatTargetModeHeat == null) ? 0 : thermostatTargetModeHeat.hashCode());
         result = prime * result + ((thermostatTargetModeOff == null) ? 0 : thermostatTargetModeOff.hashCode());
         result = prime * result + (useFahrenheitTemperature ? 1231 : 1237);
+        result = prime * result + (useDummyAccessories ? 1249 : 1259);
         return result;
     }
 
@@ -127,6 +129,9 @@ public class HomekitSettings {
         if (useFahrenheitTemperature != other.useFahrenheitTemperature) {
             return false;
         }
+        if (useDummyAccessories != other.useDummyAccessories) {
+            return false;
+        }
         return true;
     }
 }
index e479b7833f648554607c7baa83a20b66d7b686e5..417459394872719ff8e6c3a7195846b377c1234b 100644 (file)
@@ -90,7 +90,7 @@ public class HomekitTaggedItem {
         this.homekitAccessoryType = homekitAccessoryType;
         this.homekitCharacteristicType = HomekitCharacteristicType.EMPTY;
         if (homekitAccessoryType != DUMMY) {
-            this.id = calculateId(item.getItem());
+            this.id = calculateId(item.getItem().getName());
         } else {
             this.id = 0;
         }
@@ -467,24 +467,25 @@ public class HomekitTaggedItem {
         }
     }
 
-    private int calculateId(Item item) {
+    public static int calculateId(String name) {
         // magic number 629 is the legacy from apache HashCodeBuilder (17*37)
-        int id = 629 + item.getName().hashCode();
+        int id = 629 + name.hashCode();
         if (id < 0) {
             id += Integer.MAX_VALUE;
         }
         if (id < 2) {
             id = 2; // 0 and 1 are reserved
         }
+
         if (CREATED_ACCESSORY_IDS.containsKey(id)) {
-            if (!CREATED_ACCESSORY_IDS.get(id).equals(item.getName())) {
-                logger.warn(
+            if (!CREATED_ACCESSORY_IDS.get(id).equals(name)) {
+                LoggerFactory.getLogger(HomekitTaggedItem.class).warn(
                         "Could not create HomeKit accessory {} because its hash conflicts with {}. This is a 1:1,000,000 chance occurrence. Change one of the names and consider playing the lottery. See https://github.com/openhab/openhab-addons/issues/257#issuecomment-125886562",
-                        item.getName(), CREATED_ACCESSORY_IDS.get(id));
+                        name, CREATED_ACCESSORY_IDS.get(id));
                 return 0;
             }
         } else {
-            CREATED_ACCESSORY_IDS.put(id, item.getName());
+            CREATED_ACCESSORY_IDS.put(id, name);
         }
         return id;
     }
index 1c0d3fb3e9aa24e598ab265200516195f14830c9..6b1b656bf825cecae5b0c9490e95e09e2c65a420 100644 (file)
@@ -20,6 +20,10 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import javax.json.Json;
+import javax.json.JsonObjectBuilder;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -38,6 +42,7 @@ import org.slf4j.LoggerFactory;
 
 import io.github.hapjava.accessories.HomekitAccessory;
 import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
+import io.github.hapjava.characteristics.impl.base.BaseCharacteristic;
 import io.github.hapjava.services.Service;
 
 /**
@@ -46,7 +51,7 @@ import io.github.hapjava.services.Service;
  *
  * @author Andy Lintner - Initial contribution
  */
-abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory {
+public abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory {
     private final Logger logger = LoggerFactory.getLogger(AbstractHomekitAccessoryImpl.class);
     private final List<HomekitTaggedItem> characteristics;
     private final HomekitTaggedItem accessory;
@@ -349,4 +354,60 @@ abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory {
         return new BooleanItemReader(taggedItem.getItem(), taggedItem.isInverted() ? OnOffType.OFF : OnOffType.ON,
                 taggedItem.isInverted() ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
     }
+
+    /**
+     * Calculates a string as json of the configuration for this accessory, suitable for seeing
+     * if the structure has changed, and building a dummy accessory for it. It is _not_ suitable
+     * for actual publishing to by HAP-Java to iOS devices, since all the IIDs will be set to 0.
+     * The IIDs will get replaced by actual values by HAP-Java inside of DummyHomekitCharacteristic.
+     */
+    public String toJson() {
+        var builder = Json.createArrayBuilder();
+        getServices().forEach(s -> {
+            builder.add(serviceToJson(s));
+        });
+        return builder.build().toString();
+    }
+
+    private JsonObjectBuilder serviceToJson(Service service) {
+        var serviceBuilder = Json.createObjectBuilder();
+        serviceBuilder.add("type", service.getType());
+        var characteristics = Json.createArrayBuilder();
+
+        service.getCharacteristics().stream().sorted((l, r) -> l.getClass().getName().compareTo(r.getClass().getName()))
+                .forEach(c -> {
+                    try {
+                        var cJson = c.toJson(0).get();
+                        var cBuilder = Json.createObjectBuilder();
+                        // Need to copy over everything except the current value, which we instead
+                        // reach in and get the default value
+                        cJson.forEach((k, v) -> {
+                            if (k.equals("value")) {
+                                Object defaultValue = ((BaseCharacteristic) c).getDefault();
+                                if (defaultValue instanceof Boolean) {
+                                    cBuilder.add("value", (boolean) defaultValue);
+                                } else if (defaultValue instanceof Integer) {
+                                    cBuilder.add("value", (int) defaultValue);
+                                } else if (defaultValue instanceof Double) {
+                                    cBuilder.add("value", (double) defaultValue);
+                                } else {
+                                    cBuilder.add("value", defaultValue.toString());
+                                }
+                            } else {
+                                cBuilder.add(k, v);
+                            }
+                        });
+                        characteristics.add(cBuilder.build());
+                    } catch (InterruptedException | ExecutionException e) {
+                    }
+                });
+        serviceBuilder.add("c", characteristics);
+
+        if (!service.getLinkedServices().isEmpty()) {
+            var linkedServices = Json.createArrayBuilder();
+            service.getLinkedServices().forEach(s -> linkedServices.add(serviceToJson(s)));
+            serviceBuilder.add("ls", linkedServices);
+        }
+        return serviceBuilder;
+    }
 }
index d543a9755f300af6661b86776fec19c9fd7070cb..dc5ed356e95bfbbedb6cb07a2662312b10eb504c 100644 (file)
@@ -79,10 +79,10 @@ public class BooleanItemReader {
                 || (trueThreshold != null && baseItem instanceof NumberItem))) {
             if (trueThreshold != null) {
                 logger.warn("Item {} is a {} instead of the expected SwitchItem, ContactItem, NumberItem or StringItem",
-                        item.getName(), item.getClass().getName());
+                        item.getName(), item.getClass().getSimpleName());
             } else {
                 logger.warn("Item {} is a {} instead of the expected SwitchItem, ContactItem or StringItem",
-                        item.getName(), item.getClass().getName());
+                        item.getName(), item.getClass().getSimpleName());
             }
         }
     }
diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/DummyHomekitAccessory.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/DummyHomekitAccessory.java
new file mode 100644 (file)
index 0000000..cf9c332
--- /dev/null
@@ -0,0 +1,164 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.io.homekit.internal.accessories;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonValue;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.io.homekit.internal.HomekitTaggedItem;
+
+import io.github.hapjava.accessories.HomekitAccessory;
+import io.github.hapjava.characteristics.Characteristic;
+import io.github.hapjava.services.Service;
+
+/**
+ * Implements a dummy placeholder accessory for when configuration is missing
+ *
+ * @author Cody Cutrer - Initial contribution
+ */
+@NonNullByDefault({})
+public class DummyHomekitAccessory implements HomekitAccessory {
+    private static class DummyCharacteristic implements Characteristic {
+        private JsonObject json;
+        private String type;
+
+        public DummyCharacteristic(JsonObject json) {
+            this.json = json;
+            type = json.getString("type");
+            // reconstitute shortened IDs
+            if (type.length() < 8) {
+                type = "0".repeat(8 - type.length()) + type + "-0000-1000-8000-0026BB765291";
+            }
+        }
+
+        @Override
+        public String getType() {
+            return type;
+        }
+
+        @Override
+        public void supplyValue(JsonObjectBuilder characteristicBuilder) {
+            characteristicBuilder.add("value", json.get("value"));
+        }
+
+        @Override
+        public CompletableFuture<JsonObject> toJson(int iid) {
+            var builder = Json.createObjectBuilder();
+            json.forEach((k, v) -> builder.add(k, v));
+            builder.add("iid", iid);
+            return CompletableFuture.completedFuture(builder.build());
+        }
+
+        @Override
+        public void setValue(JsonValue jsonValue) {
+        }
+    }
+
+    private static class DummyService implements Service {
+        private String type;
+        private List<Characteristic> characteristics = new ArrayList();
+        private List<Service> linkedServices = new ArrayList();
+
+        public DummyService(JsonObject json) {
+            type = json.getString("type");
+            json.getJsonArray("c").forEach(c -> {
+                characteristics.add(new DummyCharacteristic((JsonObject) c));
+            });
+            var ls = json.getJsonArray("ls");
+            if (ls != null) {
+                ls.forEach(s -> {
+                    addLinkedService(new DummyService((JsonObject) s));
+                });
+            }
+        }
+
+        @Override
+        public String getType() {
+            return type;
+        }
+
+        @Override
+        public List<Characteristic> getCharacteristics() {
+            return characteristics;
+        }
+
+        @Override
+        public List<Service> getLinkedServices() {
+            return linkedServices;
+        }
+
+        @Override
+        public void addLinkedService(Service service) {
+            linkedServices.add(service);
+        }
+    };
+
+    int id;
+    String item;
+    List<Service> services = new ArrayList();
+
+    public DummyHomekitAccessory(String item, String data) {
+        this.id = HomekitTaggedItem.calculateId(item);
+        this.item = item;
+
+        var reader = Json.createReader(new StringReader(data));
+        var services = reader.readArray();
+        reader.close();
+
+        services.forEach(s -> {
+            this.services.add(new DummyService((JsonObject) s));
+        });
+    }
+
+    @Override
+    public int getId() {
+        return id;
+    }
+
+    @Override
+    public CompletableFuture<String> getName() {
+        return CompletableFuture.completedFuture(item);
+    }
+
+    @Override
+    public void identify() {
+    }
+
+    @Override
+    public CompletableFuture<String> getSerialNumber() {
+        return CompletableFuture.completedFuture(item);
+    }
+
+    @Override
+    public CompletableFuture<String> getModel() {
+        return CompletableFuture.completedFuture("none");
+    }
+
+    @Override
+    public CompletableFuture<String> getManufacturer() {
+        return CompletableFuture.completedFuture("none");
+    }
+
+    @Override
+    public CompletableFuture<String> getFirmwareRevision() {
+        return CompletableFuture.completedFuture("none");
+    }
+}
index 298ccca8d892b27629cbd082fef3515f962025dc..f0bf4a188a796be5a9b8fce0aac01343fedf19a2 100644 (file)
@@ -26,6 +26,7 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.TreeMap;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -48,7 +49,6 @@ import org.openhab.io.homekit.internal.HomekitTaggedItem;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import io.github.hapjava.accessories.HomekitAccessory;
 import io.github.hapjava.characteristics.Characteristic;
 import io.github.hapjava.services.Service;
 
@@ -168,7 +168,7 @@ public class HomekitAccessoryFactory {
      *             characteristic
      */
     @SuppressWarnings("null")
-    public static HomekitAccessory create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
+    public static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
             HomekitAccessoryUpdater updater, HomekitSettings settings) throws HomekitException {
         final HomekitAccessoryType accessoryType = taggedItem.getAccessoryType();
         logger.trace("Constructing {} of accessory type {}", taggedItem.getName(), accessoryType.getTag());
@@ -335,7 +335,7 @@ public class HomekitAccessoryFactory {
     }
 
     /**
-     * add optional characteristic for given accessory.
+     * add optional characteristics for given accessory.
      *
      * @param taggedItem main item
      * @param accessory accessory
@@ -380,7 +380,7 @@ public class HomekitAccessoryFactory {
      */
     private static Map<HomekitCharacteristicType, GenericItem> getOptionalCharacteristics(HomekitTaggedItem taggedItem,
             MetadataRegistry metadataRegistry) {
-        Map<HomekitCharacteristicType, GenericItem> characteristicItems = new HashMap<>();
+        Map<HomekitCharacteristicType, GenericItem> characteristicItems = new TreeMap<>();
         if (taggedItem.isGroup()) {
             GroupItem groupItem = (GroupItem) taggedItem.getItem();
             groupItem.getMembers().forEach(item -> getAccessoryTypes(item, metadataRegistry).stream()
index 2b7210559481088cb093c175da773e133db2df65..ed04873c784377d62a2765d3dd30d7a8db929a37 100644 (file)
                        <default>30</default>
                        <advanced>true</advanced>
                </parameter>
+               <parameter name="useDummyAccessories" type="boolean" required="true" groupName="core">
+                       <label>Use Dummy Accessories</label>
+                       <description><![CDATA[Create dummy accessories when an item is missing. See <a href="https://www.openhab.org/addons/integrations/homekit/#dummy-accessories">the documentation</a> for more information.
+                       ]]></description>
+                       <default>false</default>
+               </parameter>
                <parameter name="useFahrenheitTemperature" type="boolean" required="true" groupName="thermostat">
                        <label>Use Fahrenheit Temperature</label>
                        <description>Defines whether or not to direct HomeKit clients to use fahrenheit temperatures instead of celsius.</description>
index 38dc170b368166d5bfa7cacf6a371bb1694c2ea7..fc586100248a0862d16d265328bdd02826c7ea98 100644 (file)
@@ -39,6 +39,8 @@ io.config.homekit.thermostatTargetModeHeat.label = Heat Value
 io.config.homekit.thermostatTargetModeHeat.description = Word used to set the target heatingCoolingMode to HEAT (if a thermostat is defined).
 io.config.homekit.thermostatTargetModeOff.label = Off Value
 io.config.homekit.thermostatTargetModeOff.description = Word used to set the target heatingCoolingMode to OFF (if a thermostat is defined).
+io.config.homekit.useDummyAccessories.label = Use Dummy Accessories
+io.config.homekit.useDummyAccessories.description = Create dummy accessories when an item is missing. See <a href="https://www.openhab.org/addons/integrations/homekit/#dummy-accessories">the documentation</a> for more information.
 io.config.homekit.useFahrenheitTemperature.label = Use Fahrenheit Temperature
 io.config.homekit.useFahrenheitTemperature.description = Defines whether or not to direct HomeKit clients to use fahrenheit temperatures instead of celsius.
 io.config.homekit.useOHmDNS.label = Use openHAB mDNS service