[homekit] add support for complex accessories (#12346)
* Add complex accessories Signed-off-by: Eugen Freiter <freiter@gmx.de>
|
@ -457,7 +457,73 @@ or using UI
|
|||
|
||||
![sensor_ui_config.png](doc/sensor_ui_config.png)
|
||||
|
||||
### 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.
|
||||
![ios_fan_and_light_home_screen.png](doc/ios_fan_and_light_home_screen.png)
|
||||
|
||||
- one complex accessory - fan **with** light.
|
||||
|
||||
iOS home app would show them as **one tile** that opens view with two controls
|
||||
|
||||
![ios_fan_with_light_home_screen.png](doc/ios_fan_with_light_home_screen.png)
|
||||
|
||||
![ios_fan_with_light_details.png](doc/ios_fan_with_light_details.png)
|
||||
|
||||
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
|
||||
![ui_fan_with_light_group_view.png](doc/ui_fan_with_light_group_view.png)
|
||||
![ui_fan_with_light_group_code.png](doc/ui_fan_with_light_group_code.png)
|
||||
![ui_fan_with_light_group_config.png](doc/ui_fan_with_light_group_config.png)
|
||||
|
||||
|
||||
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"]}
|
||||
```
|
||||
|
||||
![ui_fan_with_light_primary.png](doc/ui_fan_with_light_primary.png)
|
||||
|
||||
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
|
||||
![ios_complex_accessory_detail_screen.png](doc/ios_complex_accessory_detail_screen.png)
|
||||
|
||||
|
||||
#### 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 |
|
||||
|
|
After Width: | Height: | Size: 144 KiB |
After Width: | Height: | Size: 916 KiB |
After Width: | Height: | Size: 137 KiB |
After Width: | Height: | Size: 943 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 59 KiB |
After Width: | Height: | Size: 113 KiB |
After Width: | Height: | Size: 39 KiB |
|
@ -24,6 +24,7 @@ import java.util.Set;
|
|||
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;
|
||||
|
@ -189,6 +190,13 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
|
|||
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();
|
||||
}
|
||||
|
||||
|
@ -273,19 +281,66 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
|
|||
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
|
||||
|
@ -304,19 +359,33 @@ public class HomekitChangeListener implements ItemRegistryChangeListener {
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ public class HomekitTaggedItem {
|
|||
public final static String DIMMER_MODE = "dimmerMode";
|
||||
public final static String DELAY = "commandDelay";
|
||||
public final static String INVERTED = "inverted";
|
||||
public final static String PRIMARY_SERVICE = "primary";
|
||||
|
||||
private static final Map<Integer, String> CREATED_ACCESSORY_IDS = new ConcurrentHashMap<>();
|
||||
|
||||
|
|
|
@ -225,7 +225,7 @@ public class HomekitAccessoryFactory {
|
|||
*/
|
||||
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);
|
||||
|
@ -279,8 +279,8 @@ public class HomekitAccessoryFactory {
|
|||
// 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
|
||||
|
@ -300,7 +300,8 @@ public class HomekitAccessoryFactory {
|
|||
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)));
|
||||
|
@ -359,7 +360,7 @@ public class HomekitAccessoryFactory {
|
|||
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 {
|
||||
|
@ -395,4 +396,17 @@ public class HomekitAccessoryFactory {
|
|||
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));
|
||||
}
|
||||
}
|
||||
|
|