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