[homekit] persist all known accessories, to prevent loss of homekit information (#13484)
* [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>
This commit is contained in:
parent
3c936dbda9
commit
6a16c889f2
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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!");
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
lastAccessoryCount = Integer.parseInt(accessoryCountString);
|
||||
} catch (NumberFormatException e) {
|
||||
lastAccessoryCount = 0;
|
||||
storage.put(ACCESSORY_COUNT, "" + accessoryRegistry.getAllAccessories().size());
|
||||
}
|
||||
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) {
|
||||
}
|
||||
} 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<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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() + ")");
|
||||
|
||||
@ -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.");
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -74,6 +74,12 @@
|
||||
<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>
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user