diff --git a/bundles/org.openhab.io.homekit/README.md b/bundles/org.openhab.io.homekit/README.md
index 71d48f186..475802dd5 100644
--- a/bundles/org.openhab.io.homekit/README.md
+++ b/bundles/org.openhab.io.homekit/README.md
@@ -72,7 +72,7 @@ HomeKit integration supports following accessory types:

-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
diff --git a/bundles/org.openhab.io.homekit/pom.xml b/bundles/org.openhab.io.homekit/pom.xml
index 167404864..3893f8739 100644
--- a/bundles/org.openhab.io.homekit/pom.xml
+++ b/bundles/org.openhab.io.homekit/pom.xml
@@ -22,7 +22,7 @@
io.github.hap-java
hap
- 2.0.1
+ 2.0.4
compile
diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/Homekit.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/Homekit.java
index ce0a31637..460256bbd 100644
--- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/Homekit.java
+++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/Homekit.java
@@ -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();
}
diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/Debouncer.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/Debouncer.java
index 00e2e921c..4e0922e5d 100644
--- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/Debouncer.java
+++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/Debouncer.java
@@ -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!");
diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryRegistry.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryRegistry.java
index ee7296dfd..ee941626f 100644
--- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryRegistry.java
+++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryRegistry.java
@@ -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;
diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java
index 8a3cafc6d..ba538f8a7 100644
--- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java
+++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java
@@ -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 storage;
+ private final Storage 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 storage, String pin, String setupId, boolean blockUserDeletion)
+ public HomekitAuthInfoImpl(Storage 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) {
diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitChangeListener.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitChangeListener.java
index c1e0e46e1..970669b49 100644
--- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitChangeListener.java
+++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitChangeListener.java
@@ -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 storage;
+ private final Storage storage;
private final RegistryChangeListener metadataChangeListener;
private HomekitAccessoryUpdater updater = new HomekitAccessoryUpdater();
private HomekitSettings settings;
private int lastAccessoryCount;
+ private Map knownAccessories = new HashMap<>();
private int instance;
+ private List priorDummies = new ArrayList<>();
private final Set pendingUpdates = new HashSet<>();
@@ -80,14 +89,14 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
private final Debouncer applyUpdatesDebouncer;
HomekitChangeListener(ItemRegistry itemRegistry, HomekitSettings settings, MetadataRegistry metadataRegistry,
- Storage storage, int instance) {
+ Storage 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() {
@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();
- }
- lastAccessoryCount = Integer.parseInt(accessoryCountString);
- } catch (NumberFormatException e) {
- lastAccessoryCount = 0;
- storage.put(ACCESSORY_COUNT, "" + accessoryRegistry.getAllAccessories().size());
}
accessoryRegistry.setConfigurationRevision(revision);
+
+ lastAccessoryCount = 0;
+ var localKnownAccessories = (Map) 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) {
+ }
+ } else {
+ knownAccessories = localKnownAccessories;
+ lastAccessoryCount = knownAccessories.size();
+ }
}
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 toRemove = new ArrayList<>();
+ for (Map.Entry 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 currentDummies = accessoryRegistry.getAllAccessories().values().stream()
+ .filter(a -> a instanceof DummyHomekitAccessory).map(a -> {
+ try {
+ return a.getSerialNumber().get();
+ } catch (InterruptedException | ExecutionException e) {
+ return "";
+ }
+ }).collect(Collectors.toList());
+
+ List resolvedDummies = new ArrayList(priorDummies);
+ resolvedDummies.removeAll(currentDummies);
+ List 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);
+ }
}
diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandExtension.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandExtension.java
index 090483202..c9b8b7921 100644
--- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandExtension.java
+++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCommandExtension.java
@@ -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 + " ",
- "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() + ")");
diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitImpl.java
index 5fbb3f3bc..294ba08d1 100644
--- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitImpl.java
+++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitImpl.java
@@ -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 storage = storageService.getStorage(storage_key);
+ Storage 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 added, final List removed) {
logger.trace("HomeKit bridge reacting on network interface changes.");
diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitSettings.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitSettings.java
index 952918756..0f0a7dc5d 100644
--- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitSettings.java
+++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitSettings.java
@@ -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;
}
}
diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java
index e479b7833..417459394 100644
--- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java
+++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java
@@ -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;
}
diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java
index 1c0d3fb3e..6b1b656bf 100644
--- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java
+++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java
@@ -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 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;
+ }
}
diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/BooleanItemReader.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/BooleanItemReader.java
index d543a9755..dc5ed356e 100644
--- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/BooleanItemReader.java
+++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/BooleanItemReader.java
@@ -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
index 000000000..cf9c3323b
--- /dev/null
+++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/DummyHomekitAccessory.java
@@ -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 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 characteristics = new ArrayList();
+ private List 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 getCharacteristics() {
+ return characteristics;
+ }
+
+ @Override
+ public List getLinkedServices() {
+ return linkedServices;
+ }
+
+ @Override
+ public void addLinkedService(Service service) {
+ linkedServices.add(service);
+ }
+ };
+
+ int id;
+ String item;
+ List 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 getName() {
+ return CompletableFuture.completedFuture(item);
+ }
+
+ @Override
+ public void identify() {
+ }
+
+ @Override
+ public CompletableFuture getSerialNumber() {
+ return CompletableFuture.completedFuture(item);
+ }
+
+ @Override
+ public CompletableFuture getModel() {
+ return CompletableFuture.completedFuture("none");
+ }
+
+ @Override
+ public CompletableFuture getManufacturer() {
+ return CompletableFuture.completedFuture("none");
+ }
+
+ @Override
+ public CompletableFuture getFirmwareRevision() {
+ return CompletableFuture.completedFuture("none");
+ }
+}
diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java
index 298ccca8d..f0bf4a188 100644
--- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java
+++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java
@@ -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 getOptionalCharacteristics(HomekitTaggedItem taggedItem,
MetadataRegistry metadataRegistry) {
- Map characteristicItems = new HashMap<>();
+ Map characteristicItems = new TreeMap<>();
if (taggedItem.isGroup()) {
GroupItem groupItem = (GroupItem) taggedItem.getItem();
groupItem.getMembers().forEach(item -> getAccessoryTypes(item, metadataRegistry).stream()
diff --git a/bundles/org.openhab.io.homekit/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.io.homekit/src/main/resources/OH-INF/config/config.xml
index 2b7210559..ed04873c7 100644
--- a/bundles/org.openhab.io.homekit/src/main/resources/OH-INF/config/config.xml
+++ b/bundles/org.openhab.io.homekit/src/main/resources/OH-INF/config/config.xml
@@ -74,6 +74,12 @@
30
true
+
+ Use Dummy Accessories
+ the documentation for more information.
+ ]]>
+ false
+
Use Fahrenheit Temperature
Defines whether or not to direct HomeKit clients to use fahrenheit temperatures instead of celsius.
diff --git a/bundles/org.openhab.io.homekit/src/main/resources/OH-INF/i18n/homekit.properties b/bundles/org.openhab.io.homekit/src/main/resources/OH-INF/i18n/homekit.properties
index 38dc170b3..fc5861002 100644
--- a/bundles/org.openhab.io.homekit/src/main/resources/OH-INF/i18n/homekit.properties
+++ b/bundles/org.openhab.io.homekit/src/main/resources/OH-INF/i18n/homekit.properties
@@ -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 the documentation 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