
+### Complex accessory
+Multiple HomeKit accessories can be combined to one accessory in order to group several functions provided by one or multiple physical devices.
+
+For example, ceiling fans often include lighting functionality. Such fans can be modeled as:
+
+- two separate HomeKit accessories - fan **and** light.
+
+ iOS home app would show them as **two tiles** that can be controlled directly from home screen.
+ 
+
+- one complex accessory - fan **with** light.
+
+ iOS home app would show them as **one tile** that opens view with two controls
+
+ 
+
+ 
+
+The provided functionality is in both cases identical.
+
+In order to combine multiple accessories to one HomeKit accessory you need:
+
+- add corresponding openHAB items to one openHAB group
+- configure HomeKit metadata of both HomeKit accessories at that group.
+
+e.g. configuration for a fan with light would look as follows
+
+```xtend
+Group FanWithLight "Fan with Light" {homekit = "Fan,Light"}
+Switch FanActiveStatus "Fan Active Status" (FanWithLight) {homekit = "Fan.ActiveStatus"}
+Number FanRotationSpeed "Fan Rotation Speed" (FanWithLight) {homekit = "Fan.RotationSpeed"}
+Switch Light "Light" (FanWithLight) {homekit = "Lighting.OnState"}
+```
+
+or in mainUI
+
+
+
+
+
+iOS home app uses by default the type of the first accessory on the list for the tile on home screen.
+e.g. an accessory defined as homekit = "Fan,Light" will be shown as a fan and an accessory defined as homekit = "Light,Fan" as a light in iOS home app.
+
+if you want to change the tile you can either change the order of types in homekit metadata or add "primary=<type>" to HomeKit metadata configuration.
+e.g. following configuration will force "fan" to be used as tile
+
+```xtend
+Group FanWithLight "Fan with Light" {homekit = "Light,Fan" [primary = "Fan"]}
+```
+
+
+
+However, home app does not support changing of tiles for already added accessory.
+If you want to change the tile after the accessory was added, you need either to rename the group, if you use textual item configuration, or to delete and to create a new group with a different name, if you use UI for configuration.
+
+You can combine more than two accessories as well as accessories linked to different physical devices.
+You can also do unusually combinations, e.g. you can combine temperature sensor with blinds and light.
+It will be represented by home app as follows
+
+
+
+#### Limitations
+
+Currently, it is not possible to combine multiple accessories of the same type, e.g. 2 lights.
+Support for this is planned for the future release of openHAB HomeKit binding.
+
## Supported accessory type
| Accessory Tag | Mandatory Characteristics | Optional Characteristics | Supported OH items | Description |
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.items.GroupItem;
for (Item accessoryGroup : HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry)) {
pendingUpdates.add(accessoryGroup.getName());
}
+
+ /*
+ * if metadata of a group item was changed, mark all group member as dirty.
+ */
+ if (item instanceof GroupItem) {
+ ((GroupItem) item).getMembers().forEach(groupMember -> pendingUpdates.add(groupMember.getName()));
+ }
applyUpdatesDebouncer.call();
}
return this.accessoryRegistry.getConfigurationRevision();
}
+ /**
+ * select primary accessory type from list of types.
+ * selection logic:
+ * - if accessory has only one type, it is the primary type
+ * - if accessory has no primary type defined per configuration, then the first type on the list is the primary type
+ * - if accessory has primary type defined per configuration and this type is on the list of types, then it is the
+ * primary
+ * - if accessory has primary type defined per configuration and this type is NOT on the list of types, then the
+ * first type on the list is the primary type
+ *
+ * @param item openhab item
+ * @param accessoryTypes list of accessory type attached to the item
+ * @return primary accessory type
+ */
+ private HomekitAccessoryType getPrimaryAccessoryType(Item item,
+ List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessoryTypes) {
+ if (accessoryTypes.size() > 1) {
+ final @Nullable Map<String, Object> configuration = HomekitAccessoryFactory.getItemConfiguration(item,
+ metadataRegistry);
+ if (configuration != null) {
+ final @Nullable Object value = configuration.get(HomekitTaggedItem.PRIMARY_SERVICE);
+ if (value instanceof String) {
+ return accessoryTypes.stream()
+ .filter(aType -> ((String) value).equalsIgnoreCase(aType.getKey().getTag())).findAny()
+ .orElse(accessoryTypes.get(0)).getKey();
+ }
+ }
+ }
+ // no primary accessory found or there is only one type, so return the first type from the list
+ return accessoryTypes.get(0).getKey();
+ }
+
/**
* creates one or more HomeKit items for given openhab item.
- * one OpenHAB item can linked to several HomeKit accessories or characteristics.
- * OpenHAB Item is a good candidate for homeKit accessory IF
- * - it has HomeKit accessory types, i.e. HomeKit accessory tag AND
- * - has no group with HomeKit tag, i.e. single line accessory ODER
- * - has groups with HomeKit tag, but all groups are with baseItem, e.g. Group:Switch,
- * so that the groups already complete accessory and group members can be a standalone HomeKit accessory.
+ * one OpenHAB item can be linked to several HomeKit accessories.
+ * OpenHAB item is a good candidate for a HomeKit accessory
+ * IF
+ * - it has HomeKit accessory types defined using HomeKit accessory metadata
+ * - AND is not part of a group with HomeKit metadata
+ * e.g.
+ * Switch light "Light" {homekit="Lighting"}
+ * Group gLight "Light Group" {homekit="Lighting"}
+ *
+ * OR
+ * - it has HomeKit accessory types defined using HomeKit accessory metadata
+ * - AND is part of groups with HomeKit metadata, but all groups have baseItem
+ * e.g.
+ * Group:Switch:OR(ON,OFF) gLight "Light Group " {homekit="Lighting"}
+ * Switch light "Light" (gLight) {homekit="Lighting.OnState"}
+ *
+ *
* In contrast, items which are part of groups without BaseItem are additional HomeKit characteristics of the
- * accessory defined by that group and dont need to be created as RootAccessory here.
+ * accessory defined by that group and don't need to be created as accessory here.
+ * e.g.
+ * Group gLight "Light Group " {homekit="Lighting"}
+ * Switch light "Light" (gLight) {homekit="Lighting.OnState"}
+ * is not the root accessory but only a characteristic "OnState"
*
* Examples:
- * // Single Line HomeKit Accessory
+ * // Single line HomeKit Accessory
* Switch light "Light" {homekit="Lighting"}
*
* // One HomeKit accessory defined using group
final List<GroupItem> groups = HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry);
if (!accessoryTypes.isEmpty()
&& (groups.isEmpty() || groups.stream().noneMatch(g -> g.getBaseItem() == null))) {
- logger.trace("Item {} is a HomeKit accessory of types {}", item.getName(), accessoryTypes);
+ final HomekitAccessoryType primaryAccessoryType = getPrimaryAccessoryType(item, accessoryTypes);
+ logger.trace("Item {} is a HomeKit accessory of types {}. Primary type is {}", item.getName(),
+ accessoryTypes, primaryAccessoryType);
final HomekitOHItemProxy itemProxy = new HomekitOHItemProxy(item);
- accessoryTypes.forEach(rootAccessory -> createRootAccessory(new HomekitTaggedItem(itemProxy,
- rootAccessory.getKey(), HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry))));
- }
- }
+ final HomekitTaggedItem taggedItem = new HomekitTaggedItem(new HomekitOHItemProxy(item),
+ primaryAccessoryType, HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry));
+ try {
+ final HomekitAccessory accessory = HomekitAccessoryFactory.create(taggedItem, metadataRegistry, updater,
+ settings);
- private void createRootAccessory(HomekitTaggedItem taggedItem) {
- try {
- accessoryRegistry.addRootAccessory(taggedItem.getName(),
- HomekitAccessoryFactory.create(taggedItem, metadataRegistry, updater, settings));
- } catch (HomekitException e) {
- logger.warn("Could not add device {}: {}", taggedItem.getItem().getUID(), e.getMessage());
+ accessoryTypes.stream().filter(aType -> !primaryAccessoryType.equals(aType.getKey()))
+ .forEach(additionalAccessoryType -> {
+ final HomekitTaggedItem additionalTaggedItem = new HomekitTaggedItem(itemProxy,
+ additionalAccessoryType.getKey(),
+ HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry));
+ try {
+ final HomekitAccessory additionalAccessory = HomekitAccessoryFactory
+ .create(additionalTaggedItem, metadataRegistry, updater, settings);
+ accessory.getServices().add(additionalAccessory.getPrimaryService());
+ } catch (HomekitException e) {
+ logger.warn("Cannot create additional accessory {}", additionalTaggedItem);
+ }
+ });
+ accessoryRegistry.addRootAccessory(taggedItem.getName(), accessory);
+ } catch (HomekitException e) {
+ logger.warn("Cannot create accessory {}", taggedItem);
+ }
}
}
}
*/
public static List<GroupItem> getAccessoryGroups(Item item, ItemRegistry itemRegistry,
MetadataRegistry metadataRegistry) {
- return item.getGroupNames().stream().flatMap(name -> {
+ return (item instanceof GroupItem) ? Collections.emptyList() : item.getGroupNames().stream().flatMap(name -> {
final @Nullable Item groupItem = itemRegistry.get(name);
if ((groupItem instanceof GroupItem) && ((GroupItem) groupItem).getBaseItem() == null) {
return Stream.of((GroupItem) groupItem);
// no mandatory characteristics linked to accessory type of mainItem. we are done
return;
}
- // check whether we adding characteristic to the main item, and if yes, use existing item proxy.
- // if we adding no to the main item (typical for groups), create new proxy item.
+ // check whether we are adding characteristic to the main item, and if yes, use existing item proxy.
+ // if we are adding not to the main item (typical for groups), create new proxy item.
final HomekitOHItemProxy itemProxy = mainItem.getItem().equals(item) ? mainItem.getProxyItem()
: new HomekitOHItemProxy(item);
// an item can have several tags, e.g. "ActiveStatus, InUse". we iterate here over all his tags
final HomekitCharacteristicType characteristic = accessory.getValue();
// check whether it is a mandatory characteristic. optional will be added later by another method.
- if (isMandatoryCharacteristic(mainItem.getAccessoryType(), characteristic)) {
+ if (belongsToType(mainItem.getAccessoryType(), accessory)
+ && isMandatoryCharacteristic(mainItem.getAccessoryType(), characteristic)) {
characteristics.add(new HomekitTaggedItem(itemProxy, accessory.getKey(), characteristic,
mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry)));
if (taggedItem.isGroup()) {
GroupItem groupItem = (GroupItem) taggedItem.getItem();
groupItem.getMembers().forEach(item -> getAccessoryTypes(item, metadataRegistry).stream()
- .filter(c -> !isRootAccessory(c))
+ .filter(c -> !isRootAccessory(c)).filter(c -> belongsToType(taggedItem.getAccessoryType(), c))
.filter(c -> !isMandatoryCharacteristic(taggedItem.getAccessoryType(), c.getValue()))
.forEach(characteristic -> characteristicItems.put(characteristic.getValue(), (GenericItem) item)));
} else {
private static boolean isRootAccessory(Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory) {
return ((accessory.getValue() == null) || (accessory.getValue() == EMPTY));
}
+
+ /**
+ * check whether characteristic belongs to the specific accessory type.
+ * characteristic with no accessory type mentioned in metadata are considered as candidates for all types.
+ *
+ * @param accessoryType accessory type
+ * @param characteristic characteristic
+ * @return true if characteristic belongs to the accessory type.
+ */
+ private static boolean belongsToType(HomekitAccessoryType accessoryType,
+ Entry<HomekitAccessoryType, HomekitCharacteristicType> characteristic) {
+ return ((characteristic.getKey() == accessoryType) || (characteristic.getKey() == DUMMY));
+ }
}