]> git.basschouten.com Git - openhab-addons.git/commitdiff
[homekit] add support for complex accessories (#12346)
authoreugen <freiter@gmail.com>
Sun, 27 Mar 2022 22:11:44 +0000 (00:11 +0200)
committerGitHub <noreply@github.com>
Sun, 27 Mar 2022 22:11:44 +0000 (00:11 +0200)
* Add complex accessories

Signed-off-by: Eugen Freiter <freiter@gmx.de>
12 files changed:
bundles/org.openhab.io.homekit/README.md
bundles/org.openhab.io.homekit/doc/ios_complex_accessory_detail_screen.png [new file with mode: 0755]
bundles/org.openhab.io.homekit/doc/ios_fan_and_light_home_screen.png [new file with mode: 0755]
bundles/org.openhab.io.homekit/doc/ios_fan_with_light_details.png [new file with mode: 0755]
bundles/org.openhab.io.homekit/doc/ios_fan_with_light_home_screen.png [new file with mode: 0755]
bundles/org.openhab.io.homekit/doc/ui_fan_with_light_group_code.png [new file with mode: 0644]
bundles/org.openhab.io.homekit/doc/ui_fan_with_light_group_config.png [new file with mode: 0644]
bundles/org.openhab.io.homekit/doc/ui_fan_with_light_group_view.png [new file with mode: 0644]
bundles/org.openhab.io.homekit/doc/ui_fan_with_light_primary.png [new file with mode: 0644]
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitChangeListener.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java

index 55e21ef3d17fddda9780b63d3c7ac030ce992bf4..2957f583b19f87b47a32ec8c4326808ad7e7d8c4 100644 (file)
@@ -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                                                      |
diff --git a/bundles/org.openhab.io.homekit/doc/ios_complex_accessory_detail_screen.png b/bundles/org.openhab.io.homekit/doc/ios_complex_accessory_detail_screen.png
new file mode 100755 (executable)
index 0000000..5022271
Binary files /dev/null and b/bundles/org.openhab.io.homekit/doc/ios_complex_accessory_detail_screen.png differ
diff --git a/bundles/org.openhab.io.homekit/doc/ios_fan_and_light_home_screen.png b/bundles/org.openhab.io.homekit/doc/ios_fan_and_light_home_screen.png
new file mode 100755 (executable)
index 0000000..a8dd927
Binary files /dev/null and b/bundles/org.openhab.io.homekit/doc/ios_fan_and_light_home_screen.png differ
diff --git a/bundles/org.openhab.io.homekit/doc/ios_fan_with_light_details.png b/bundles/org.openhab.io.homekit/doc/ios_fan_with_light_details.png
new file mode 100755 (executable)
index 0000000..55dea07
Binary files /dev/null and b/bundles/org.openhab.io.homekit/doc/ios_fan_with_light_details.png differ
diff --git a/bundles/org.openhab.io.homekit/doc/ios_fan_with_light_home_screen.png b/bundles/org.openhab.io.homekit/doc/ios_fan_with_light_home_screen.png
new file mode 100755 (executable)
index 0000000..ac7caf4
Binary files /dev/null and b/bundles/org.openhab.io.homekit/doc/ios_fan_with_light_home_screen.png differ
diff --git a/bundles/org.openhab.io.homekit/doc/ui_fan_with_light_group_code.png b/bundles/org.openhab.io.homekit/doc/ui_fan_with_light_group_code.png
new file mode 100644 (file)
index 0000000..bc4eead
Binary files /dev/null and b/bundles/org.openhab.io.homekit/doc/ui_fan_with_light_group_code.png differ
diff --git a/bundles/org.openhab.io.homekit/doc/ui_fan_with_light_group_config.png b/bundles/org.openhab.io.homekit/doc/ui_fan_with_light_group_config.png
new file mode 100644 (file)
index 0000000..a05054b
Binary files /dev/null and b/bundles/org.openhab.io.homekit/doc/ui_fan_with_light_group_config.png differ
diff --git a/bundles/org.openhab.io.homekit/doc/ui_fan_with_light_group_view.png b/bundles/org.openhab.io.homekit/doc/ui_fan_with_light_group_view.png
new file mode 100644 (file)
index 0000000..4ba4a53
Binary files /dev/null and b/bundles/org.openhab.io.homekit/doc/ui_fan_with_light_group_view.png differ
diff --git a/bundles/org.openhab.io.homekit/doc/ui_fan_with_light_primary.png b/bundles/org.openhab.io.homekit/doc/ui_fan_with_light_primary.png
new file mode 100644 (file)
index 0000000..fbdf0df
Binary files /dev/null and b/bundles/org.openhab.io.homekit/doc/ui_fan_with_light_primary.png differ
index 2a44a93b80a0cb46683ad53bdf9a1be4005a6985..ed8390cfd8f3a8081330de5fb1704bfcc034f637 100644 (file)
@@ -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);
+            }
         }
     }
 }
index a366f755ac0bf1957cc9e1147147602569b2de99..224e7169d3c3d6e2c65ae1b843af1b7b495da309 100644 (file)
@@ -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<>();
 
index 0b9e65f3d8c7fcb9a18917d490a529de82e6d4f8..50cfebac6889992de25c93e12bc65b26de643203 100644 (file)
@@ -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));
+    }
 }