2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.io.homekit.internal.accessories;
15 import static org.openhab.io.homekit.internal.HomekitAccessoryType.*;
16 import static org.openhab.io.homekit.internal.HomekitCharacteristicType.*;
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;
27 import java.util.Map.Entry;
28 import java.util.Objects;
29 import java.util.Optional;
31 import java.util.TreeMap;
32 import java.util.stream.Collectors;
33 import java.util.stream.Stream;
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;
54 import io.github.hapjava.characteristics.Characteristic;
55 import io.github.hapjava.characteristics.impl.common.NameCharacteristic;
58 * Creates a HomekitAccessory for a given HomekitTaggedItem.
60 * @author Andy Lintner - Initial contribution
61 * @author Eugen Freiter - refactoring for optional characteristics
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
68 /** List of mandatory attributes for each accessory type. **/
69 private final static Map<HomekitAccessoryType, HomekitCharacteristicType[]> MANDATORY_CHARACTERISTICS = new HashMap<HomekitAccessoryType, HomekitCharacteristicType[]>() {
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 });
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 put(TELEVISION, new HomekitCharacteristicType[] { ACTIVE });
109 put(INPUT_SOURCE, new HomekitCharacteristicType[] {});
110 put(TELEVISION_SPEAKER, new HomekitCharacteristicType[] { MUTE });
114 /** List of service implementation for each accessory type. **/
115 private final static Map<HomekitAccessoryType, Class<? extends AbstractHomekitAccessoryImpl>> SERVICE_IMPL_MAP = new HashMap<HomekitAccessoryType, Class<? extends AbstractHomekitAccessoryImpl>>() {
117 put(ACCESSORY_GROUP, HomekitAccessoryGroupImpl.class);
118 put(LEAK_SENSOR, HomekitLeakSensorImpl.class);
119 put(MOTION_SENSOR, HomekitMotionSensorImpl.class);
120 put(OCCUPANCY_SENSOR, HomekitOccupancySensorImpl.class);
121 put(CONTACT_SENSOR, HomekitContactSensorImpl.class);
122 put(SMOKE_SENSOR, HomekitSmokeSensorImpl.class);
123 put(HUMIDITY_SENSOR, HomekitHumiditySensorImpl.class);
124 put(AIR_QUALITY_SENSOR, HomekitAirQualitySensorImpl.class);
125 put(SWITCH, HomekitSwitchImpl.class);
126 put(CARBON_DIOXIDE_SENSOR, HomekitCarbonDioxideSensorImpl.class);
127 put(CARBON_MONOXIDE_SENSOR, HomekitCarbonMonoxideSensorImpl.class);
128 put(WINDOW_COVERING, HomekitWindowCoveringImpl.class);
129 put(LIGHTBULB, HomekitLightbulbImpl.class);
130 put(BASIC_FAN, HomekitBasicFanImpl.class);
131 put(FAN, HomekitFanImpl.class);
132 put(LIGHT_SENSOR, HomekitLightSensorImpl.class);
133 put(TEMPERATURE_SENSOR, HomekitTemperatureSensorImpl.class);
134 put(THERMOSTAT, HomekitThermostatImpl.class);
135 put(LOCK, HomekitLockImpl.class);
136 put(VALVE, HomekitValveImpl.class);
137 put(SECURITY_SYSTEM, HomekitSecuritySystemImpl.class);
138 put(OUTLET, HomekitOutletImpl.class);
139 put(SPEAKER, HomekitSpeakerImpl.class);
140 put(SMART_SPEAKER, HomekitSmartSpeakerImpl.class);
141 put(GARAGE_DOOR_OPENER, HomekitGarageDoorOpenerImpl.class);
142 put(DOOR, HomekitDoorImpl.class);
143 put(WINDOW, HomekitWindowImpl.class);
144 put(HEATER_COOLER, HomekitHeaterCoolerImpl.class);
145 put(BATTERY, HomekitBatteryImpl.class);
146 put(FILTER_MAINTENANCE, HomekitFilterMaintenanceImpl.class);
147 put(SLAT, HomekitSlatImpl.class);
148 put(FAUCET, HomekitFaucetImpl.class);
149 put(MICROPHONE, HomekitMicrophoneImpl.class);
150 put(TELEVISION, HomekitTelevisionImpl.class);
151 put(INPUT_SOURCE, HomekitInputSourceImpl.class);
152 put(TELEVISION_SPEAKER, HomekitTelevisionSpeakerImpl.class);
156 private static List<HomekitCharacteristicType> getRequiredCharacteristics(HomekitTaggedItem taggedItem) {
157 final List<HomekitCharacteristicType> characteristics = new ArrayList<>();
158 if (MANDATORY_CHARACTERISTICS.containsKey(taggedItem.getAccessoryType())) {
159 characteristics.addAll(Arrays.asList(MANDATORY_CHARACTERISTICS.get(taggedItem.getAccessoryType())));
161 if (taggedItem.getAccessoryType() == BATTERY) {
162 final boolean isChargeable = taggedItem.getConfigurationAsBoolean(HomekitBatteryImpl.BATTERY_TYPE, false);
164 characteristics.add(BATTERY_CHARGING_STATE);
167 return characteristics;
171 * creates HomeKit accessory for an openhab item.
173 * @param taggedItem openhab item tagged as HomeKit item
174 * @param metadataRegistry openhab metadata registry required to get item meta information
175 * @param updater OH HomeKit update class that ensure the status sync between OH item and corresponding HomeKit
177 * @param settings OH settings
178 * @return HomeKit accessory
179 * @throws HomekitException exception in case HomeKit accessory could not be created, e.g. due missing mandatory
182 public static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
183 HomekitAccessoryUpdater updater, HomekitSettings settings) throws HomekitException {
184 Set<HomekitTaggedItem> ancestorServices = new HashSet<>();
185 return create(taggedItem, metadataRegistry, updater, settings, ancestorServices);
188 @SuppressWarnings("null")
189 private static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
190 HomekitAccessoryUpdater updater, HomekitSettings settings, Set<HomekitTaggedItem> ancestorServices)
191 throws HomekitException {
192 final HomekitAccessoryType accessoryType = taggedItem.getAccessoryType();
193 logger.trace("Constructing {} of accessory type {}", taggedItem.getName(), accessoryType.getTag());
194 final List<HomekitTaggedItem> foundCharacteristics = getMandatoryCharacteristicsFromItem(taggedItem,
196 final List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(taggedItem);
197 if (foundCharacteristics.size() < mandatoryCharacteristics.size()) {
198 logger.warn("Accessory of type {} must have following characteristics {}. Found only {}",
199 accessoryType.getTag(), mandatoryCharacteristics, foundCharacteristics);
200 throw new HomekitException("Missing mandatory characteristics");
202 AbstractHomekitAccessoryImpl accessoryImpl;
204 final @Nullable Class<? extends AbstractHomekitAccessoryImpl> accessoryImplClass = SERVICE_IMPL_MAP
206 if (accessoryImplClass != null) {
207 if (ancestorServices.contains(taggedItem)) {
208 logger.warn("Item {} has already been created. Perhaps you have circular Homekit accessory groups?",
209 taggedItem.getName());
210 throw new HomekitException("Circular accessory references");
212 ancestorServices.add(taggedItem);
213 accessoryImpl = accessoryImplClass.getConstructor(HomekitTaggedItem.class, List.class,
214 HomekitAccessoryUpdater.class, HomekitSettings.class)
215 .newInstance(taggedItem, foundCharacteristics, updater, settings);
216 addOptionalCharacteristics(taggedItem, accessoryImpl, metadataRegistry);
217 addOptionalMetadataCharacteristics(taggedItem, accessoryImpl);
218 accessoryImpl.init();
219 addLinkedServices(taggedItem, accessoryImpl, metadataRegistry, updater, settings, ancestorServices);
220 return accessoryImpl;
222 logger.warn("Unsupported HomeKit type: {}", accessoryType.getTag());
223 throw new HomekitException("Unsupported HomeKit type: " + accessoryType);
225 } catch (NoSuchMethodException | IllegalAccessException | InstantiationException
226 | InvocationTargetException e) {
227 logger.warn("Cannot instantiate accessory implementation for accessory {}", accessoryType.getTag(), e);
228 throw new HomekitException("Cannot instantiate accessory implementation for accessory " + accessoryType);
233 * return HomeKit accessory types for an OH item based on meta data
235 * @param item OH item
236 * @param metadataRegistry meta data registry
237 * @return list of HomeKit accessory types and characteristics.
239 public static List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> getAccessoryTypes(Item item,
240 MetadataRegistry metadataRegistry) {
241 final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessories = new ArrayList<>();
242 final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
243 if (metadata != null) {
244 String[] tags = metadata.getValue().split(",");
245 for (String tag : tags) {
246 final String[] meta = tag.split("\\.");
247 Optional<HomekitAccessoryType> accessoryType = HomekitAccessoryType.valueOfTag(meta[0].trim());
248 if (accessoryType.isPresent()) { // it accessory, check for characteristic
249 HomekitAccessoryType type = accessoryType.get();
250 if (meta.length > 1) {
251 // it has characteristic as well
252 accessories.add(new SimpleEntry<>(type,
253 HomekitCharacteristicType.valueOfTag(meta[1].trim()).orElse(EMPTY)));
254 } else {// it has no characteristic
255 accessories.add(new SimpleEntry<>(type, EMPTY));
257 } else { // it is no accessory, so, maybe it is a characteristic
258 HomekitCharacteristicType.valueOfTag(meta[0].trim())
259 .ifPresent(c -> accessories.add(new SimpleEntry<>(DUMMY, c)));
266 public static @Nullable Map<String, Object> getItemConfiguration(Item item, MetadataRegistry metadataRegistry) {
267 final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
268 return metadata != null ? metadata.getConfiguration() : null;
272 * return list of HomeKit relevant groups linked to an accessory
274 * @param item OH item
275 * @param itemRegistry item registry
276 * @param metadataRegistry metadata registry
277 * @return list of relevant group items
279 public static List<GroupItem> getAccessoryGroups(Item item, ItemRegistry itemRegistry,
280 MetadataRegistry metadataRegistry) {
281 return item.getGroupNames().stream().flatMap(name -> {
282 final @Nullable Item groupItem = itemRegistry.get(name);
283 if (groupItem instanceof GroupItem) {
284 return Stream.of((GroupItem) groupItem);
286 return Stream.empty();
288 }).filter(groupItem -> !getAccessoryTypes(groupItem, metadataRegistry).isEmpty()).collect(Collectors.toList());
292 * collect all mandatory characteristics for a given tagged item, e.g. collect all mandatory HomeKit items from a
295 * @param taggedItem HomeKit tagged item
296 * @param metadataRegistry meta data registry
297 * @return list of mandatory
299 private static List<HomekitTaggedItem> getMandatoryCharacteristicsFromItem(HomekitTaggedItem taggedItem,
300 MetadataRegistry metadataRegistry) {
301 List<HomekitTaggedItem> collectedCharacteristics = new ArrayList<>();
302 if (taggedItem.isGroup()) {
303 for (Item item : ((GroupItem) taggedItem.getItem()).getMembers()) {
304 addMandatoryCharacteristics(taggedItem, collectedCharacteristics, item, metadataRegistry);
307 addMandatoryCharacteristics(taggedItem, collectedCharacteristics, taggedItem.getItem(), metadataRegistry);
309 logger.trace("Mandatory characteristics: {}", collectedCharacteristics);
310 return collectedCharacteristics;
314 * add mandatory HomeKit items for a given main item to a list of characteristics.
315 * Main item is use only to determine, which characteristics are mandatory.
316 * The characteristics are added to item.
317 * e.g. mainItem could be a group tagged as "thermostat" and item could be item linked to the group and marked as
320 * @param mainItem main item
321 * @param characteristics list of characteristics
322 * @param item current item
323 * @param metadataRegistry meta date registry
325 private static void addMandatoryCharacteristics(HomekitTaggedItem mainItem, List<HomekitTaggedItem> characteristics,
326 Item item, MetadataRegistry metadataRegistry) {
327 // get list of mandatory characteristics
328 List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(mainItem);
329 if (mandatoryCharacteristics.isEmpty()) {
330 // no mandatory characteristics linked to accessory type of mainItem. we are done
333 // check whether we are adding characteristic to the main item, and if yes, use existing item proxy.
334 // if we are adding not to the main item (typical for groups), create new proxy item.
335 final HomekitOHItemProxy itemProxy = mainItem.getItem().equals(item) ? mainItem.getProxyItem()
336 : new HomekitOHItemProxy(item);
337 // an item can have several tags, e.g. "ActiveStatus, InUse". we iterate here over all his tags
338 for (Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory : getAccessoryTypes(item,
340 // if the item has only accessory tag, e.g. TemperatureSensor,
341 // then we will link all mandatory characteristic to this item,
342 // e.g. we will link CurrentTemperature in case of TemperatureSensor.
343 // Note that accessories that are members of other accessories do _not_
344 // count - we're already constructing another root accessory.
345 if (isRootAccessory(accessory) && mainItem.getItem().equals(item)) {
346 mandatoryCharacteristics.forEach(c -> characteristics.add(new HomekitTaggedItem(itemProxy,
347 accessory.getKey(), c, mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
348 HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry))));
350 // item has characteristic tag on it, so, adding it as that characteristic.
352 final HomekitCharacteristicType characteristic = accessory.getValue();
354 // check whether it is a mandatory characteristic. optional will be added later by another method.
355 if (belongsToType(mainItem.getAccessoryType(), accessory)
356 && isMandatoryCharacteristic(mainItem, characteristic)) {
357 characteristics.add(new HomekitTaggedItem(itemProxy, accessory.getKey(), characteristic,
358 mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
359 HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry)));
366 * add optional characteristics for given accessory.
368 * @param taggedItem main item
369 * @param accessory accessory
370 * @param metadataRegistry metadata registry
372 private static void addOptionalCharacteristics(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
373 MetadataRegistry metadataRegistry) {
374 Map<HomekitCharacteristicType, GenericItem> characteristics = getOptionalCharacteristics(
375 accessory.getRootAccessory(), metadataRegistry);
376 HashMap<String, HomekitOHItemProxy> proxyItems = new HashMap<>();
377 proxyItems.put(taggedItem.getItem().getUID(), taggedItem.getProxyItem());
378 // an accessory can have multiple optional characteristics. iterate over them.
379 characteristics.forEach((type, item) -> {
381 // check whether a proxyItem already exists, if not create one.
382 final HomekitOHItemProxy proxyItem = Objects
383 .requireNonNull(proxyItems.computeIfAbsent(item.getUID(), k -> new HomekitOHItemProxy(item)));
384 final HomekitTaggedItem optionalItem = new HomekitTaggedItem(proxyItem,
385 accessory.getRootAccessory().getAccessoryType(), type,
386 accessory.getRootAccessory().getRootDeviceGroupItem(),
387 getItemConfiguration(item, metadataRegistry));
388 final Characteristic characteristic = HomekitCharacteristicFactory.createCharacteristic(optionalItem,
389 accessory.getUpdater());
390 accessory.addCharacteristic(optionalItem, characteristic);
391 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | HomekitException e) {
392 logger.warn("Unsupported optional HomeKit characteristic: type {}, characteristic type {}",
393 accessory.getPrimaryService(), type.getTag());
399 * add optional characteristics for given accessory from metadata
401 * @param taggedItem main item
402 * @param accessory accessory
404 private static void addOptionalMetadataCharacteristics(HomekitTaggedItem taggedItem,
405 AbstractHomekitAccessoryImpl accessory)
406 throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, HomekitException {
407 // Check every metadata key looking for a characteristics we can create
408 var config = taggedItem.getConfiguration();
409 if (config == null) {
412 for (var entry : config.entrySet().stream().sorted((lhs, rhs) -> lhs.getKey().compareTo(rhs.getKey()))
413 .collect(Collectors.toList())) {
414 var characteristic = HomekitMetadataCharacteristicFactory.createCharacteristic(entry.getKey(),
416 if (characteristic.isPresent())
417 accessory.addCharacteristic(characteristic.get());
422 * creates HomeKit services for an openhab item that are members of this group item.
424 * @param taggedItem openhab item tagged as HomeKit item
425 * @param AbstractHomekitAccessoryImpl the accessory to add services to
426 * @param metadataRegistry openhab metadata registry required to get item meta information
427 * @param updater OH HomeKit update class that ensure the status sync between OH item and corresponding HomeKit
429 * @param settings OH settings
430 * @param ancestorServices set of all accessories/services under the same root accessory, for
431 * for preventing circular references
432 * @throws HomekitException exception in case HomeKit accessory could not be created, e.g. due missing mandatory
435 private static void addLinkedServices(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
436 MetadataRegistry metadataRegistry, HomekitAccessoryUpdater updater, HomekitSettings settings,
437 Set<HomekitTaggedItem> ancestorServices) throws HomekitException {
438 final var item = taggedItem.getItem();
439 if (!(item instanceof GroupItem))
442 for (var groupMember : ((GroupItem) item).getMembers().stream()
443 .sorted((lhs, rhs) -> lhs.getName().compareTo(rhs.getName())).collect(Collectors.toList())) {
444 final var characteristicTypes = getAccessoryTypes(groupMember, metadataRegistry);
445 var accessoryTypes = characteristicTypes.stream().filter(HomekitAccessoryFactory::isRootAccessory)
446 .collect(Collectors.toList());
448 logger.trace("accessory types for {} are {}", groupMember.getName(), accessoryTypes);
449 if (accessoryTypes.isEmpty())
452 if (accessoryTypes.size() > 1) {
453 logger.warn("Item {} is a HomeKit sub-accessory, but multiple accessory types are not allowed.",
454 groupMember.getName());
458 final @Nullable Map<String, Object> itemConfiguration = getItemConfiguration(groupMember, metadataRegistry);
460 final var accessoryType = accessoryTypes.iterator().next().getKey();
461 logger.trace("Item {} is a HomeKit sub-accessory of type {}.", groupMember.getName(), accessoryType);
462 final var itemProxy = new HomekitOHItemProxy(groupMember);
463 final var subTaggedItem = new HomekitTaggedItem(itemProxy, accessoryType, itemConfiguration);
464 final var subAccessory = create(subTaggedItem, metadataRegistry, updater, settings, ancestorServices);
467 subAccessory.addCharacteristic(new NameCharacteristic(() -> subAccessory.getName()));
468 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
469 // This should never happen; all services should support NameCharacteristic as an optional
471 // If HAP-Java defined a service that doesn't support addOptionalCharacteristic(NameCharacteristic),
472 // Then it's a bug there, and we're just going to ignore the exception here.
475 if (subAccessory.isLinkable(accessory)) {
476 accessory.getPrimaryService().addLinkedService(subAccessory.getPrimaryService());
478 accessory.getServices().add(subAccessory.getPrimaryService());
484 * collect optional HomeKit characteristics for a OH item.
486 * @param taggedItem main OH item
487 * @param metadataRegistry OH metadata registry
488 * @return a map with characteristics and corresponding OH items
490 private static Map<HomekitCharacteristicType, GenericItem> getOptionalCharacteristics(HomekitTaggedItem taggedItem,
491 MetadataRegistry metadataRegistry) {
492 Map<HomekitCharacteristicType, GenericItem> characteristicItems = new TreeMap<>();
493 if (taggedItem.isGroup()) {
494 GroupItem groupItem = (GroupItem) taggedItem.getItem();
495 groupItem.getMembers().forEach(item -> getAccessoryTypes(item, metadataRegistry).stream()
496 .filter(c -> !isRootAccessory(c)).filter(c -> belongsToType(taggedItem.getAccessoryType(), c))
497 .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
498 .forEach(characteristic -> characteristicItems.put(characteristic.getValue(), (GenericItem) item)));
500 getAccessoryTypes(taggedItem.getItem(), metadataRegistry).stream().filter(c -> !isRootAccessory(c))
501 .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
502 .forEach(characteristic -> characteristicItems.put(characteristic.getValue(),
503 (GenericItem) taggedItem.getItem()));
505 logger.trace("Optional characteristics for item {}: {}", taggedItem.getName(), characteristicItems.values());
506 return Collections.unmodifiableMap(characteristicItems);
510 * return true is characteristic is a mandatory characteristic for the accessory.
513 * @param characteristic characteristic
514 * @return true if characteristic is mandatory, false if not mandatory
516 private static boolean isMandatoryCharacteristic(HomekitTaggedItem item, HomekitCharacteristicType characteristic) {
517 return MANDATORY_CHARACTERISTICS.containsKey(item.getAccessoryType())
518 && getRequiredCharacteristics(item).contains(characteristic);
522 * check whether accessory is root accessory, i.e. without characteristic tag.
524 * @param accessory accessory
525 * @return true if accessory has not characteristic.
527 private static boolean isRootAccessory(Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory) {
528 return ((accessory.getValue() == null) || (accessory.getValue() == EMPTY));
532 * check whether characteristic belongs to the specific accessory type.
533 * characteristic with no accessory type mentioned in metadata are considered as candidates for all types.
535 * @param accessoryType accessory type
536 * @param characteristic characteristic
537 * @return true if characteristic belongs to the accessory type.
539 private static boolean belongsToType(HomekitAccessoryType accessoryType,
540 Entry<HomekitAccessoryType, HomekitCharacteristicType> characteristic) {
541 return ((characteristic.getKey() == accessoryType) || (characteristic.getKey() == DUMMY));