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