]> git.basschouten.com Git - openhab-addons.git/blob
8da2c84730cefda59c4731e9c46bde42d564b480
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.io.homekit.internal.accessories;
14
15 import static org.openhab.io.homekit.internal.HomekitAccessoryType.*;
16 import static org.openhab.io.homekit.internal.HomekitCharacteristicType.*;
17
18 import java.lang.reflect.InvocationTargetException;
19 import java.util.AbstractMap.SimpleEntry;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Map.Entry;
28 import java.util.Objects;
29 import java.util.Optional;
30 import java.util.Set;
31 import java.util.TreeMap;
32 import java.util.stream.Collectors;
33 import java.util.stream.Stream;
34
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.openhab.core.items.GenericItem;
38 import org.openhab.core.items.GroupItem;
39 import org.openhab.core.items.Item;
40 import org.openhab.core.items.ItemRegistry;
41 import org.openhab.core.items.Metadata;
42 import org.openhab.core.items.MetadataKey;
43 import org.openhab.core.items.MetadataRegistry;
44 import org.openhab.io.homekit.internal.HomekitAccessoryType;
45 import org.openhab.io.homekit.internal.HomekitAccessoryUpdater;
46 import org.openhab.io.homekit.internal.HomekitCharacteristicType;
47 import org.openhab.io.homekit.internal.HomekitException;
48 import org.openhab.io.homekit.internal.HomekitOHItemProxy;
49 import org.openhab.io.homekit.internal.HomekitSettings;
50 import org.openhab.io.homekit.internal.HomekitTaggedItem;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 import io.github.hapjava.characteristics.Characteristic;
55 import io.github.hapjava.characteristics.impl.common.NameCharacteristic;
56
57 /**
58  * Creates a HomekitAccessory for a given HomekitTaggedItem.
59  *
60  * @author Andy Lintner - Initial contribution
61  * @author Eugen Freiter - refactoring for optional characteristics
62  */
63 @NonNullByDefault
64 public class HomekitAccessoryFactory {
65     private static final Logger logger = LoggerFactory.getLogger(HomekitAccessoryFactory.class);
66     public final static String METADATA_KEY = "homekit"; // prefix for HomeKit meta information in items.xml
67
68     /** List of mandatory attributes for each accessory type. **/
69     private final static Map<HomekitAccessoryType, HomekitCharacteristicType[]> MANDATORY_CHARACTERISTICS = new HashMap<HomekitAccessoryType, HomekitCharacteristicType[]>() {
70         {
71             put(ACCESSORY_GROUP, new HomekitCharacteristicType[] {});
72             put(LEAK_SENSOR, new HomekitCharacteristicType[] { LEAK_DETECTED_STATE });
73             put(MOTION_SENSOR, new HomekitCharacteristicType[] { MOTION_DETECTED_STATE });
74             put(OCCUPANCY_SENSOR, new HomekitCharacteristicType[] { OCCUPANCY_DETECTED_STATE });
75             put(CONTACT_SENSOR, new HomekitCharacteristicType[] { CONTACT_SENSOR_STATE });
76             put(SMOKE_SENSOR, new HomekitCharacteristicType[] { SMOKE_DETECTED_STATE });
77             put(HUMIDITY_SENSOR, new HomekitCharacteristicType[] { RELATIVE_HUMIDITY });
78             put(AIR_QUALITY_SENSOR, new HomekitCharacteristicType[] { AIR_QUALITY });
79             put(SWITCH, new HomekitCharacteristicType[] { ON_STATE });
80             put(CARBON_DIOXIDE_SENSOR, new HomekitCharacteristicType[] { CARBON_DIOXIDE_DETECTED_STATE });
81             put(CARBON_MONOXIDE_SENSOR, new HomekitCharacteristicType[] { CARBON_MONOXIDE_DETECTED_STATE });
82             put(WINDOW_COVERING, new HomekitCharacteristicType[] { TARGET_POSITION, CURRENT_POSITION, POSITION_STATE });
83             put(LIGHTBULB, new HomekitCharacteristicType[] { ON_STATE });
84             put(BASIC_FAN, new HomekitCharacteristicType[] { ON_STATE });
85             put(FAN, new HomekitCharacteristicType[] { ACTIVE_STATUS });
86             put(LIGHT_SENSOR, new HomekitCharacteristicType[] { LIGHT_LEVEL });
87             put(TEMPERATURE_SENSOR, new HomekitCharacteristicType[] { CURRENT_TEMPERATURE });
88             put(THERMOSTAT, new HomekitCharacteristicType[] { CURRENT_HEATING_COOLING_STATE,
89                     TARGET_HEATING_COOLING_STATE, CURRENT_TEMPERATURE, TARGET_TEMPERATURE });
90             put(LOCK, new HomekitCharacteristicType[] { LOCK_CURRENT_STATE, LOCK_TARGET_STATE });
91             put(VALVE, new HomekitCharacteristicType[] { ACTIVE_STATUS, INUSE_STATUS });
92             put(SECURITY_SYSTEM,
93                     new HomekitCharacteristicType[] { SECURITY_SYSTEM_CURRENT_STATE, SECURITY_SYSTEM_TARGET_STATE });
94             put(OUTLET, new HomekitCharacteristicType[] { ON_STATE, INUSE_STATUS });
95             put(SPEAKER, new HomekitCharacteristicType[] { MUTE });
96             put(SMART_SPEAKER, new HomekitCharacteristicType[] { CURRENT_MEDIA_STATE, TARGET_MEDIA_STATE });
97             put(GARAGE_DOOR_OPENER,
98                     new HomekitCharacteristicType[] { CURRENT_DOOR_STATE, TARGET_DOOR_STATE, OBSTRUCTION_STATUS });
99             put(HEATER_COOLER, new HomekitCharacteristicType[] { ACTIVE_STATUS, CURRENT_HEATER_COOLER_STATE,
100                     TARGET_HEATER_COOLER_STATE, CURRENT_TEMPERATURE });
101             put(WINDOW, new HomekitCharacteristicType[] { CURRENT_POSITION, TARGET_POSITION, POSITION_STATE });
102             put(DOOR, new HomekitCharacteristicType[] { CURRENT_POSITION, TARGET_POSITION, POSITION_STATE });
103             put(BATTERY, new HomekitCharacteristicType[] { BATTERY_LEVEL, BATTERY_LOW_STATUS });
104             put(FILTER_MAINTENANCE, new HomekitCharacteristicType[] { FILTER_CHANGE_INDICATION });
105             put(SLAT, new HomekitCharacteristicType[] { CURRENT_SLAT_STATE });
106             put(FAUCET, new HomekitCharacteristicType[] { ACTIVE_STATUS });
107             put(MICROPHONE, new HomekitCharacteristicType[] { MUTE });
108         }
109     };
110
111     /** List of service implementation for each accessory type. **/
112     private final static Map<HomekitAccessoryType, Class<? extends AbstractHomekitAccessoryImpl>> SERVICE_IMPL_MAP = new HashMap<HomekitAccessoryType, Class<? extends AbstractHomekitAccessoryImpl>>() {
113         {
114             put(ACCESSORY_GROUP, HomekitAccessoryGroupImpl.class);
115             put(LEAK_SENSOR, HomekitLeakSensorImpl.class);
116             put(MOTION_SENSOR, HomekitMotionSensorImpl.class);
117             put(OCCUPANCY_SENSOR, HomekitOccupancySensorImpl.class);
118             put(CONTACT_SENSOR, HomekitContactSensorImpl.class);
119             put(SMOKE_SENSOR, HomekitSmokeSensorImpl.class);
120             put(HUMIDITY_SENSOR, HomekitHumiditySensorImpl.class);
121             put(AIR_QUALITY_SENSOR, HomekitAirQualitySensorImpl.class);
122             put(SWITCH, HomekitSwitchImpl.class);
123             put(CARBON_DIOXIDE_SENSOR, HomekitCarbonDioxideSensorImpl.class);
124             put(CARBON_MONOXIDE_SENSOR, HomekitCarbonMonoxideSensorImpl.class);
125             put(WINDOW_COVERING, HomekitWindowCoveringImpl.class);
126             put(LIGHTBULB, HomekitLightbulbImpl.class);
127             put(BASIC_FAN, HomekitBasicFanImpl.class);
128             put(FAN, HomekitFanImpl.class);
129             put(LIGHT_SENSOR, HomekitLightSensorImpl.class);
130             put(TEMPERATURE_SENSOR, HomekitTemperatureSensorImpl.class);
131             put(THERMOSTAT, HomekitThermostatImpl.class);
132             put(LOCK, HomekitLockImpl.class);
133             put(VALVE, HomekitValveImpl.class);
134             put(SECURITY_SYSTEM, HomekitSecuritySystemImpl.class);
135             put(OUTLET, HomekitOutletImpl.class);
136             put(SPEAKER, HomekitSpeakerImpl.class);
137             put(SMART_SPEAKER, HomekitSmartSpeakerImpl.class);
138             put(GARAGE_DOOR_OPENER, HomekitGarageDoorOpenerImpl.class);
139             put(DOOR, HomekitDoorImpl.class);
140             put(WINDOW, HomekitWindowImpl.class);
141             put(HEATER_COOLER, HomekitHeaterCoolerImpl.class);
142             put(BATTERY, HomekitBatteryImpl.class);
143             put(FILTER_MAINTENANCE, HomekitFilterMaintenanceImpl.class);
144             put(SLAT, HomekitSlatImpl.class);
145             put(FAUCET, HomekitFaucetImpl.class);
146             put(MICROPHONE, HomekitMicrophoneImpl.class);
147         }
148     };
149
150     private static List<HomekitCharacteristicType> getRequiredCharacteristics(HomekitTaggedItem taggedItem) {
151         final List<HomekitCharacteristicType> characteristics = new ArrayList<>();
152         if (MANDATORY_CHARACTERISTICS.containsKey(taggedItem.getAccessoryType())) {
153             characteristics.addAll(Arrays.asList(MANDATORY_CHARACTERISTICS.get(taggedItem.getAccessoryType())));
154         }
155         if (taggedItem.getAccessoryType() == BATTERY) {
156             final boolean isChargeable = taggedItem.getConfigurationAsBoolean(HomekitBatteryImpl.BATTERY_TYPE, false);
157             if (isChargeable) {
158                 characteristics.add(BATTERY_CHARGING_STATE);
159             }
160         }
161         return characteristics;
162     }
163
164     /**
165      * creates HomeKit accessory for an openhab item.
166      *
167      * @param taggedItem openhab item tagged as HomeKit item
168      * @param metadataRegistry openhab metadata registry required to get item meta information
169      * @param updater OH HomeKit update class that ensure the status sync between OH item and corresponding HomeKit
170      *            characteristic.
171      * @param settings OH settings
172      * @return HomeKit accessory
173      * @throws HomekitException exception in case HomeKit accessory could not be created, e.g. due missing mandatory
174      *             characteristic
175      */
176     public static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
177             HomekitAccessoryUpdater updater, HomekitSettings settings) throws HomekitException {
178         Set<HomekitTaggedItem> ancestorServices = new HashSet<>();
179         return create(taggedItem, metadataRegistry, updater, settings, ancestorServices);
180     }
181
182     @SuppressWarnings("null")
183     private static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
184             HomekitAccessoryUpdater updater, HomekitSettings settings, Set<HomekitTaggedItem> ancestorServices)
185             throws HomekitException {
186         final HomekitAccessoryType accessoryType = taggedItem.getAccessoryType();
187         logger.trace("Constructing {} of accessory type {}", taggedItem.getName(), accessoryType.getTag());
188         final List<HomekitTaggedItem> foundCharacteristics = getMandatoryCharacteristicsFromItem(taggedItem,
189                 metadataRegistry);
190         final List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(taggedItem);
191         if (foundCharacteristics.size() < mandatoryCharacteristics.size()) {
192             logger.warn("Accessory of type {} must have following characteristics {}. Found only {}",
193                     accessoryType.getTag(), mandatoryCharacteristics, foundCharacteristics);
194             throw new HomekitException("Missing mandatory characteristics");
195         }
196         AbstractHomekitAccessoryImpl accessoryImpl;
197         try {
198             final @Nullable Class<? extends AbstractHomekitAccessoryImpl> accessoryImplClass = SERVICE_IMPL_MAP
199                     .get(accessoryType);
200             if (accessoryImplClass != null) {
201                 if (ancestorServices.contains(taggedItem)) {
202                     logger.warn("Item {} has already been created. Perhaps you have circular Homekit accessory groups?",
203                             taggedItem.getName());
204                     throw new HomekitException("Circular accessory references");
205                 }
206                 ancestorServices.add(taggedItem);
207                 accessoryImpl = accessoryImplClass.getConstructor(HomekitTaggedItem.class, List.class,
208                         HomekitAccessoryUpdater.class, HomekitSettings.class)
209                         .newInstance(taggedItem, foundCharacteristics, updater, settings);
210                 addOptionalCharacteristics(taggedItem, accessoryImpl, metadataRegistry);
211                 addLinkedServices(taggedItem, accessoryImpl, metadataRegistry, updater, settings, ancestorServices);
212                 return accessoryImpl;
213             } else {
214                 logger.warn("Unsupported HomeKit type: {}", accessoryType.getTag());
215                 throw new HomekitException("Unsupported HomeKit type: " + accessoryType);
216             }
217         } catch (NoSuchMethodException | IllegalAccessException | InstantiationException
218                 | InvocationTargetException e) {
219             logger.warn("Cannot instantiate accessory implementation for accessory {}", accessoryType.getTag(), e);
220             throw new HomekitException("Cannot instantiate accessory implementation for accessory " + accessoryType);
221         }
222     }
223
224     /**
225      * return HomeKit accessory types for an OH item based on meta data
226      *
227      * @param item OH item
228      * @param metadataRegistry meta data registry
229      * @return list of HomeKit accessory types and characteristics.
230      */
231     public static List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> getAccessoryTypes(Item item,
232             MetadataRegistry metadataRegistry) {
233         final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessories = new ArrayList<>();
234         final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
235         if (metadata != null) {
236             String[] tags = metadata.getValue().split(",");
237             for (String tag : tags) {
238                 final String[] meta = tag.split("\\.");
239                 Optional<HomekitAccessoryType> accessoryType = HomekitAccessoryType.valueOfTag(meta[0].trim());
240                 if (accessoryType.isPresent()) { // it accessory, check for characteristic
241                     HomekitAccessoryType type = accessoryType.get();
242                     if (meta.length > 1) {
243                         // it has characteristic as well
244                         accessories.add(new SimpleEntry<>(type,
245                                 HomekitCharacteristicType.valueOfTag(meta[1].trim()).orElse(EMPTY)));
246                     } else {// it has no characteristic
247                         accessories.add(new SimpleEntry<>(type, EMPTY));
248                     }
249                 } else { // it is no accessory, so, maybe it is a characteristic
250                     HomekitCharacteristicType.valueOfTag(meta[0].trim())
251                             .ifPresent(c -> accessories.add(new SimpleEntry<>(DUMMY, c)));
252                 }
253             }
254         }
255         return accessories;
256     }
257
258     public static @Nullable Map<String, Object> getItemConfiguration(Item item, MetadataRegistry metadataRegistry) {
259         final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
260         return metadata != null ? metadata.getConfiguration() : null;
261     }
262
263     /**
264      * return list of HomeKit relevant groups linked to an accessory
265      *
266      * @param item OH item
267      * @param itemRegistry item registry
268      * @param metadataRegistry metadata registry
269      * @return list of relevant group items
270      */
271     public static List<GroupItem> getAccessoryGroups(Item item, ItemRegistry itemRegistry,
272             MetadataRegistry metadataRegistry) {
273         return item.getGroupNames().stream().flatMap(name -> {
274             final @Nullable Item groupItem = itemRegistry.get(name);
275             if (groupItem instanceof GroupItem) {
276                 return Stream.of((GroupItem) groupItem);
277             } else {
278                 return Stream.empty();
279             }
280         }).filter(groupItem -> !getAccessoryTypes(groupItem, metadataRegistry).isEmpty()).collect(Collectors.toList());
281     }
282
283     /**
284      * collect all mandatory characteristics for a given tagged item, e.g. collect all mandatory HomeKit items from a
285      * GroupItem
286      *
287      * @param taggedItem HomeKit tagged item
288      * @param metadataRegistry meta data registry
289      * @return list of mandatory
290      */
291     private static List<HomekitTaggedItem> getMandatoryCharacteristicsFromItem(HomekitTaggedItem taggedItem,
292             MetadataRegistry metadataRegistry) {
293         List<HomekitTaggedItem> collectedCharacteristics = new ArrayList<>();
294         if (taggedItem.isGroup()) {
295             for (Item item : ((GroupItem) taggedItem.getItem()).getMembers()) {
296                 addMandatoryCharacteristics(taggedItem, collectedCharacteristics, item, metadataRegistry);
297             }
298         } else {
299             addMandatoryCharacteristics(taggedItem, collectedCharacteristics, taggedItem.getItem(), metadataRegistry);
300         }
301         logger.trace("Mandatory characteristics: {}", collectedCharacteristics);
302         return collectedCharacteristics;
303     }
304
305     /**
306      * add mandatory HomeKit items for a given main item to a list of characteristics.
307      * Main item is use only to determine, which characteristics are mandatory.
308      * The characteristics are added to item.
309      * e.g. mainItem could be a group tagged as "thermostat" and item could be item linked to the group and marked as
310      * TargetTemperature
311      *
312      * @param mainItem main item
313      * @param characteristics list of characteristics
314      * @param item current item
315      * @param metadataRegistry meta date registry
316      */
317     private static void addMandatoryCharacteristics(HomekitTaggedItem mainItem, List<HomekitTaggedItem> characteristics,
318             Item item, MetadataRegistry metadataRegistry) {
319         // get list of mandatory characteristics
320         List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(mainItem);
321         if (mandatoryCharacteristics.isEmpty()) {
322             // no mandatory characteristics linked to accessory type of mainItem. we are done
323             return;
324         }
325         // check whether we are adding characteristic to the main item, and if yes, use existing item proxy.
326         // if we are adding not to the main item (typical for groups), create new proxy item.
327         final HomekitOHItemProxy itemProxy = mainItem.getItem().equals(item) ? mainItem.getProxyItem()
328                 : new HomekitOHItemProxy(item);
329         // an item can have several tags, e.g. "ActiveStatus, InUse". we iterate here over all his tags
330         for (Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory : getAccessoryTypes(item,
331                 metadataRegistry)) {
332             // if the item has only accessory tag, e.g. TemperatureSensor,
333             // then we will link all mandatory characteristic to this item,
334             // e.g. we will link CurrentTemperature in case of TemperatureSensor.
335             // Note that accessories that are members of other accessories do _not_
336             // count - we're already constructing another root accessory.
337             if (isRootAccessory(accessory) && mainItem.getItem().equals(item)) {
338                 mandatoryCharacteristics.forEach(c -> characteristics.add(new HomekitTaggedItem(itemProxy,
339                         accessory.getKey(), c, mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
340                         HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry))));
341             } else {
342                 // item has characteristic tag on it, so, adding it as that characteristic.
343
344                 final HomekitCharacteristicType characteristic = accessory.getValue();
345
346                 // check whether it is a mandatory characteristic. optional will be added later by another method.
347                 if (belongsToType(mainItem.getAccessoryType(), accessory)
348                         && isMandatoryCharacteristic(mainItem, characteristic)) {
349                     characteristics.add(new HomekitTaggedItem(itemProxy, accessory.getKey(), characteristic,
350                             mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
351                             HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry)));
352                 }
353             }
354         }
355     }
356
357     /**
358      * add optional characteristics for given accessory.
359      *
360      * @param taggedItem main item
361      * @param accessory accessory
362      * @param metadataRegistry metadata registry
363      */
364     private static void addOptionalCharacteristics(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
365             MetadataRegistry metadataRegistry) {
366         Map<HomekitCharacteristicType, GenericItem> characteristics = getOptionalCharacteristics(
367                 accessory.getRootAccessory(), metadataRegistry);
368         HashMap<String, HomekitOHItemProxy> proxyItems = new HashMap<>();
369         proxyItems.put(taggedItem.getItem().getUID(), taggedItem.getProxyItem());
370         // an accessory can have multiple optional characteristics. iterate over them.
371         characteristics.forEach((type, item) -> {
372             try {
373                 // check whether a proxyItem already exists, if not create one.
374                 final HomekitOHItemProxy proxyItem = Objects
375                         .requireNonNull(proxyItems.computeIfAbsent(item.getUID(), k -> new HomekitOHItemProxy(item)));
376                 final HomekitTaggedItem optionalItem = new HomekitTaggedItem(proxyItem,
377                         accessory.getRootAccessory().getAccessoryType(), type,
378                         accessory.getRootAccessory().getRootDeviceGroupItem(),
379                         getItemConfiguration(item, metadataRegistry));
380                 final Characteristic characteristic = HomekitCharacteristicFactory.createCharacteristic(optionalItem,
381                         accessory.getUpdater());
382                 accessory.addCharacteristic(optionalItem, characteristic);
383             } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | HomekitException e) {
384                 logger.warn("Unsupported optional HomeKit characteristic: type {}, characteristic type {}",
385                         accessory.getPrimaryService(), type.getTag());
386             }
387         });
388     }
389
390     /**
391      * creates HomeKit services for an openhab item that are members of this group item.
392      *
393      * @param taggedItem openhab item tagged as HomeKit item
394      * @param AbstractHomekitAccessoryImpl the accessory to add services to
395      * @param metadataRegistry openhab metadata registry required to get item meta information
396      * @param updater OH HomeKit update class that ensure the status sync between OH item and corresponding HomeKit
397      *            characteristic.
398      * @param settings OH settings
399      * @param ancestorServices set of all accessories/services under the same root accessory, for
400      *            for preventing circular references
401      * @throws HomekitException exception in case HomeKit accessory could not be created, e.g. due missing mandatory
402      *             characteristic
403      */
404     private static void addLinkedServices(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
405             MetadataRegistry metadataRegistry, HomekitAccessoryUpdater updater, HomekitSettings settings,
406             Set<HomekitTaggedItem> ancestorServices) throws HomekitException {
407         final var item = taggedItem.getItem();
408         if (!(item instanceof GroupItem))
409             return;
410
411         for (var groupMember : ((GroupItem) item).getMembers().stream()
412                 .sorted((lhs, rhs) -> lhs.getName().compareTo(rhs.getName())).collect(Collectors.toList())) {
413             final var characteristicTypes = getAccessoryTypes(groupMember, metadataRegistry);
414             var accessoryTypes = characteristicTypes.stream().filter(c -> c.getValue() == EMPTY)
415                     .collect(Collectors.toList());
416
417             logger.trace("accessory types for {} are {}", groupMember.getName(), accessoryTypes);
418             if (accessoryTypes.isEmpty())
419                 continue;
420
421             if (accessoryTypes.size() > 1) {
422                 logger.warn("Item {} is a HomeKit sub-accessory, but multiple accessory types are not allowed.",
423                         groupMember.getName());
424                 continue;
425             }
426
427             final @Nullable Map<String, Object> itemConfiguration = getItemConfiguration(groupMember, metadataRegistry);
428
429             final var accessoryType = accessoryTypes.iterator().next().getKey();
430             logger.trace("Item {} is a HomeKit sub-accessory of type {}.", groupMember.getName(), accessoryType);
431             final var itemProxy = new HomekitOHItemProxy(groupMember);
432             final var subTaggedItem = new HomekitTaggedItem(itemProxy, accessoryType, itemConfiguration);
433             final var subAccessory = create(subTaggedItem, metadataRegistry, updater, settings, ancestorServices);
434
435             try {
436                 subAccessory.addCharacteristic(new NameCharacteristic(() -> subAccessory.getName()));
437             } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
438                 // This should never happen; all services should support NameCharacteristic as an optional
439                 // Characteristic.
440                 // If HAP-Java defined a service that doesn't support addOptionalCharacteristic(NameCharacteristic),
441                 // Then it's a bug there, and we're just going to ignore the exception here.
442             }
443
444             if (subAccessory.isLinkable(accessory)) {
445                 accessory.getPrimaryService().addLinkedService(subAccessory.getPrimaryService());
446             } else {
447                 accessory.getServices().add(subAccessory.getPrimaryService());
448             }
449         }
450     }
451
452     /**
453      * collect optional HomeKit characteristics for a OH item.
454      *
455      * @param taggedItem main OH item
456      * @param metadataRegistry OH metadata registry
457      * @return a map with characteristics and corresponding OH items
458      */
459     private static Map<HomekitCharacteristicType, GenericItem> getOptionalCharacteristics(HomekitTaggedItem taggedItem,
460             MetadataRegistry metadataRegistry) {
461         Map<HomekitCharacteristicType, GenericItem> characteristicItems = new TreeMap<>();
462         if (taggedItem.isGroup()) {
463             GroupItem groupItem = (GroupItem) taggedItem.getItem();
464             groupItem.getMembers().forEach(item -> getAccessoryTypes(item, metadataRegistry).stream()
465                     .filter(c -> !isRootAccessory(c)).filter(c -> belongsToType(taggedItem.getAccessoryType(), c))
466                     .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
467                     .forEach(characteristic -> characteristicItems.put(characteristic.getValue(), (GenericItem) item)));
468         } else {
469             getAccessoryTypes(taggedItem.getItem(), metadataRegistry).stream().filter(c -> !isRootAccessory(c))
470                     .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
471                     .forEach(characteristic -> characteristicItems.put(characteristic.getValue(),
472                             (GenericItem) taggedItem.getItem()));
473         }
474         logger.trace("Optional characteristics for item {}: {}", taggedItem.getName(), characteristicItems.values());
475         return Collections.unmodifiableMap(characteristicItems);
476     }
477
478     /**
479      * return true is characteristic is a mandatory characteristic for the accessory.
480      *
481      * @param item item
482      * @param characteristic characteristic
483      * @return true if characteristic is mandatory, false if not mandatory
484      */
485     private static boolean isMandatoryCharacteristic(HomekitTaggedItem item, HomekitCharacteristicType characteristic) {
486         return MANDATORY_CHARACTERISTICS.containsKey(item.getAccessoryType())
487                 && getRequiredCharacteristics(item).contains(characteristic);
488     }
489
490     /**
491      * check whether accessory is root accessory, i.e. without characteristic tag.
492      *
493      * @param accessory accessory
494      * @return true if accessory has not characteristic.
495      */
496     private static boolean isRootAccessory(Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory) {
497         return ((accessory.getValue() == null) || (accessory.getValue() == EMPTY));
498     }
499
500     /**
501      * check whether characteristic belongs to the specific accessory type.
502      * characteristic with no accessory type mentioned in metadata are considered as candidates for all types.
503      *
504      * @param accessoryType accessory type
505      * @param characteristic characteristic
506      * @return true if characteristic belongs to the accessory type.
507      */
508     private static boolean belongsToType(HomekitAccessoryType accessoryType,
509             Entry<HomekitAccessoryType, HomekitCharacteristicType> characteristic) {
510         return ((characteristic.getKey() == accessoryType) || (characteristic.getKey() == DUMMY));
511     }
512 }