2 * Copyright (c) 2010-2024 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;
57 * Creates a HomekitAccessory for a given HomekitTaggedItem.
59 * @author Andy Lintner - Initial contribution
60 * @author Eugen Freiter - refactoring for optional characteristics
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
67 /** List of mandatory attributes for each accessory type. **/
68 private static final Map<HomekitAccessoryType, HomekitCharacteristicType[]> MANDATORY_CHARACTERISTICS = new HashMap<>() {
70 put(ACCESSORY_GROUP, new HomekitCharacteristicType[] {});
71 put(LEAK_SENSOR, new HomekitCharacteristicType[] { LEAK_DETECTED_STATE });
72 put(MOTION_SENSOR, new HomekitCharacteristicType[] { MOTION_DETECTED_STATE });
73 put(OCCUPANCY_SENSOR, new HomekitCharacteristicType[] { OCCUPANCY_DETECTED_STATE });
74 put(CONTACT_SENSOR, new HomekitCharacteristicType[] { CONTACT_SENSOR_STATE });
75 put(SMOKE_SENSOR, new HomekitCharacteristicType[] { SMOKE_DETECTED_STATE });
76 put(HUMIDITY_SENSOR, new HomekitCharacteristicType[] { RELATIVE_HUMIDITY });
77 put(AIR_QUALITY_SENSOR, new HomekitCharacteristicType[] { AIR_QUALITY });
78 put(SWITCH, new HomekitCharacteristicType[] { ON_STATE });
79 put(CARBON_DIOXIDE_SENSOR, new HomekitCharacteristicType[] { CARBON_DIOXIDE_DETECTED_STATE });
80 put(CARBON_MONOXIDE_SENSOR, new HomekitCharacteristicType[] { CARBON_MONOXIDE_DETECTED_STATE });
81 put(WINDOW_COVERING, new HomekitCharacteristicType[] { TARGET_POSITION, CURRENT_POSITION, POSITION_STATE });
82 put(LIGHTBULB, new HomekitCharacteristicType[] { ON_STATE });
83 put(BASIC_FAN, new HomekitCharacteristicType[] { ON_STATE });
84 put(FAN, new HomekitCharacteristicType[] { ACTIVE_STATUS });
85 put(LIGHT_SENSOR, new HomekitCharacteristicType[] { LIGHT_LEVEL });
86 put(TEMPERATURE_SENSOR, new HomekitCharacteristicType[] { CURRENT_TEMPERATURE });
87 put(THERMOSTAT, new HomekitCharacteristicType[] { CURRENT_HEATING_COOLING_STATE,
88 TARGET_HEATING_COOLING_STATE, CURRENT_TEMPERATURE, TARGET_TEMPERATURE });
89 put(LOCK, new HomekitCharacteristicType[] { LOCK_CURRENT_STATE, LOCK_TARGET_STATE });
90 put(VALVE, new HomekitCharacteristicType[] { ACTIVE_STATUS, INUSE_STATUS });
92 new HomekitCharacteristicType[] { SECURITY_SYSTEM_CURRENT_STATE, SECURITY_SYSTEM_TARGET_STATE });
93 put(OUTLET, new HomekitCharacteristicType[] { ON_STATE, INUSE_STATUS });
94 put(SPEAKER, new HomekitCharacteristicType[] { MUTE });
95 put(SMART_SPEAKER, new HomekitCharacteristicType[] { CURRENT_MEDIA_STATE, TARGET_MEDIA_STATE });
96 put(GARAGE_DOOR_OPENER,
97 new HomekitCharacteristicType[] { CURRENT_DOOR_STATE, TARGET_DOOR_STATE, OBSTRUCTION_STATUS });
98 put(HEATER_COOLER, new HomekitCharacteristicType[] { ACTIVE_STATUS, CURRENT_HEATER_COOLER_STATE,
99 TARGET_HEATER_COOLER_STATE, CURRENT_TEMPERATURE });
100 put(WINDOW, new HomekitCharacteristicType[] { CURRENT_POSITION, TARGET_POSITION, POSITION_STATE });
101 put(DOOR, new HomekitCharacteristicType[] { CURRENT_POSITION, TARGET_POSITION, POSITION_STATE });
102 put(BATTERY, new HomekitCharacteristicType[] { BATTERY_LEVEL, BATTERY_LOW_STATUS });
103 put(FILTER_MAINTENANCE, new HomekitCharacteristicType[] { FILTER_CHANGE_INDICATION });
104 put(SLAT, new HomekitCharacteristicType[] { CURRENT_SLAT_STATE });
105 put(FAUCET, new HomekitCharacteristicType[] { ACTIVE_STATUS });
106 put(MICROPHONE, new HomekitCharacteristicType[] { MUTE });
107 put(TELEVISION, new HomekitCharacteristicType[] { ACTIVE });
108 put(INPUT_SOURCE, new HomekitCharacteristicType[] {});
109 put(TELEVISION_SPEAKER, new HomekitCharacteristicType[] { MUTE });
110 put(IRRIGATION_SYSTEM, new HomekitCharacteristicType[] { ACTIVE, INUSE_STATUS, PROGRAM_MODE });
114 /** List of service implementation for each accessory type. **/
115 private static final Map<HomekitAccessoryType, Class<? extends AbstractHomekitAccessoryImpl>> SERVICE_IMPL_MAP = new HashMap<>() {
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);
153 put(IRRIGATION_SYSTEM, HomekitIrrigationSystemImpl.class);
157 private static List<HomekitCharacteristicType> getRequiredCharacteristics(HomekitTaggedItem taggedItem) {
158 final List<HomekitCharacteristicType> characteristics = new ArrayList<>();
159 if (MANDATORY_CHARACTERISTICS.containsKey(taggedItem.getAccessoryType())) {
160 characteristics.addAll(Arrays.asList(MANDATORY_CHARACTERISTICS.get(taggedItem.getAccessoryType())));
162 if (taggedItem.getAccessoryType() == BATTERY) {
163 final boolean isChargeable = taggedItem.getConfigurationAsBoolean(HomekitBatteryImpl.BATTERY_TYPE, false);
165 characteristics.add(BATTERY_CHARGING_STATE);
168 return characteristics;
172 * creates HomeKit accessory for an openhab item.
174 * @param taggedItem openhab item tagged as HomeKit item
175 * @param metadataRegistry openhab metadata registry required to get item meta information
176 * @param updater OH HomeKit update class that ensure the status sync between OH item and corresponding HomeKit
178 * @param settings OH settings
179 * @return HomeKit accessory
180 * @throws HomekitException exception in case HomeKit accessory could not be created, e.g. due missing mandatory
183 public static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
184 HomekitAccessoryUpdater updater, HomekitSettings settings) throws HomekitException {
185 Set<HomekitTaggedItem> ancestorServices = new HashSet<>();
186 return create(taggedItem, metadataRegistry, updater, settings, ancestorServices);
189 @SuppressWarnings("null")
190 private static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
191 HomekitAccessoryUpdater updater, HomekitSettings settings, Set<HomekitTaggedItem> ancestorServices)
192 throws HomekitException {
193 final HomekitAccessoryType accessoryType = taggedItem.getAccessoryType();
194 LOGGER.trace("Constructing {} of accessory type {}", taggedItem.getName(), accessoryType.getTag());
195 final List<HomekitTaggedItem> foundCharacteristics = getMandatoryCharacteristicsFromItem(taggedItem,
197 final List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(taggedItem);
198 if (foundCharacteristics.size() < mandatoryCharacteristics.size()) {
199 LOGGER.warn("Accessory of type {} must have following characteristics {}. Found only {}",
200 accessoryType.getTag(), mandatoryCharacteristics, foundCharacteristics);
201 throw new HomekitException("Missing mandatory characteristics");
203 AbstractHomekitAccessoryImpl accessoryImpl;
205 final @Nullable Class<? extends AbstractHomekitAccessoryImpl> accessoryImplClass = SERVICE_IMPL_MAP
207 if (accessoryImplClass != null) {
208 if (ancestorServices.contains(taggedItem)) {
209 LOGGER.warn("Item {} has already been created. Perhaps you have circular Homekit accessory groups?",
210 taggedItem.getName());
211 throw new HomekitException("Circular accessory references");
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.setIsLinkedService(!ancestorServices.isEmpty());
219 accessoryImpl.init();
220 ancestorServices.add(taggedItem);
221 addLinkedServices(taggedItem, accessoryImpl, metadataRegistry, updater, settings, ancestorServices);
222 return accessoryImpl;
224 LOGGER.warn("Unsupported HomeKit type: {}", accessoryType.getTag());
225 throw new HomekitException("Unsupported HomeKit type: " + accessoryType);
227 } catch (NoSuchMethodException | IllegalAccessException | InstantiationException
228 | InvocationTargetException e) {
229 LOGGER.warn("Cannot instantiate accessory implementation for accessory {}", accessoryType.getTag(), e);
230 throw new HomekitException("Cannot instantiate accessory implementation for accessory " + accessoryType);
235 * return HomeKit accessory types for an OH item based on meta data
237 * @param item OH item
238 * @param metadataRegistry meta data registry
239 * @return list of HomeKit accessory types and characteristics.
241 public static List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> getAccessoryTypes(Item item,
242 MetadataRegistry metadataRegistry) {
243 final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessories = new ArrayList<>();
244 final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
245 if (metadata != null) {
246 String[] tags = metadata.getValue().split(",");
247 for (String tag : tags) {
248 final String[] meta = tag.split("\\.");
249 Optional<HomekitAccessoryType> accessoryType = HomekitAccessoryType.valueOfTag(meta[0].trim());
250 if (accessoryType.isPresent()) { // it accessory, check for characteristic
251 HomekitAccessoryType type = accessoryType.get();
252 if (meta.length > 1) {
253 // it has characteristic as well
254 accessories.add(new SimpleEntry<>(type,
255 HomekitCharacteristicType.valueOfTag(meta[1].trim()).orElse(EMPTY)));
256 } else {// it has no characteristic
257 accessories.add(new SimpleEntry<>(type, EMPTY));
259 } else { // it is no accessory, so, maybe it is a characteristic
260 HomekitCharacteristicType.valueOfTag(meta[0].trim())
261 .ifPresent(c -> accessories.add(new SimpleEntry<>(DUMMY, c)));
268 public static @Nullable Map<String, Object> getItemConfiguration(Item item, MetadataRegistry metadataRegistry) {
269 final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
270 return metadata != null ? metadata.getConfiguration() : null;
274 * return list of HomeKit relevant groups linked to an accessory
276 * @param item OH item
277 * @param itemRegistry item registry
278 * @param metadataRegistry metadata registry
279 * @return list of relevant group items
281 public static List<GroupItem> getAccessoryGroups(Item item, ItemRegistry itemRegistry,
282 MetadataRegistry metadataRegistry) {
283 return item.getGroupNames().stream().flatMap(name -> {
284 final @Nullable Item itemFromRegistry = itemRegistry.get(name);
285 if (itemFromRegistry instanceof GroupItem groupItem) {
286 return Stream.of(groupItem);
288 return Stream.empty();
290 }).filter(groupItem -> !getAccessoryTypes(groupItem, metadataRegistry).isEmpty()).collect(Collectors.toList());
294 * collect all mandatory characteristics for a given tagged item, e.g. collect all mandatory HomeKit items from a
297 * @param taggedItem HomeKit tagged item
298 * @param metadataRegistry meta data registry
299 * @return list of mandatory
301 private static List<HomekitTaggedItem> getMandatoryCharacteristicsFromItem(HomekitTaggedItem taggedItem,
302 MetadataRegistry metadataRegistry) {
303 List<HomekitTaggedItem> collectedCharacteristics = new ArrayList<>();
304 if (taggedItem.isGroup()) {
305 for (Item item : ((GroupItem) taggedItem.getItem()).getMembers()) {
306 addMandatoryCharacteristics(taggedItem, collectedCharacteristics, item, metadataRegistry);
309 addMandatoryCharacteristics(taggedItem, collectedCharacteristics, taggedItem.getItem(), metadataRegistry);
311 LOGGER.trace("Mandatory characteristics: {}", collectedCharacteristics);
312 return collectedCharacteristics;
316 * add mandatory HomeKit items for a given main item to a list of characteristics.
317 * Main item is use only to determine, which characteristics are mandatory.
318 * The characteristics are added to item.
319 * e.g. mainItem could be a group tagged as "thermostat" and item could be item linked to the group and marked as
322 * @param mainItem main item
323 * @param characteristics list of characteristics
324 * @param item current item
325 * @param metadataRegistry meta date registry
327 private static void addMandatoryCharacteristics(HomekitTaggedItem mainItem, List<HomekitTaggedItem> characteristics,
328 Item item, MetadataRegistry metadataRegistry) {
329 // get list of mandatory characteristics
330 List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(mainItem);
331 if (mandatoryCharacteristics.isEmpty()) {
332 // no mandatory characteristics linked to accessory type of mainItem. we are done
335 // check whether we are adding characteristic to the main item, and if yes, use existing item proxy.
336 // if we are adding not to the main item (typical for groups), create new proxy item.
337 final HomekitOHItemProxy itemProxy = mainItem.getItem().equals(item) ? mainItem.getProxyItem()
338 : new HomekitOHItemProxy(item);
339 // an item can have several tags, e.g. "ActiveStatus, InUse". we iterate here over all his tags
340 for (Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory : getAccessoryTypes(item,
342 // if the item has only accessory tag, e.g. TemperatureSensor,
343 // then we will link all mandatory characteristic to this item,
344 // e.g. we will link CurrentTemperature in case of TemperatureSensor.
345 // Note that accessories that are members of other accessories do _not_
346 // count - we're already constructing another root accessory.
347 if (isRootAccessory(accessory) && mainItem.getItem().equals(item)) {
348 mandatoryCharacteristics.forEach(c -> characteristics.add(new HomekitTaggedItem(itemProxy,
349 accessory.getKey(), c, mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
350 HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry))));
352 // item has characteristic tag on it, so, adding it as that characteristic.
354 final HomekitCharacteristicType characteristic = accessory.getValue();
356 // check whether it is a mandatory characteristic. optional will be added later by another method.
357 if (belongsToType(mainItem.getAccessoryType(), accessory)
358 && isMandatoryCharacteristic(mainItem, characteristic)) {
359 characteristics.add(new HomekitTaggedItem(itemProxy, accessory.getKey(), characteristic,
360 mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
361 HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry)));
368 * add optional characteristics for given accessory.
370 * @param taggedItem main item
371 * @param accessory accessory
372 * @param metadataRegistry metadata registry
374 private static void addOptionalCharacteristics(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
375 MetadataRegistry metadataRegistry) {
376 Map<HomekitCharacteristicType, GenericItem> characteristics = getOptionalCharacteristics(
377 accessory.getRootAccessory(), metadataRegistry);
378 HashMap<String, HomekitOHItemProxy> proxyItems = new HashMap<>();
379 proxyItems.put(taggedItem.getItem().getUID(), taggedItem.getProxyItem());
380 // an accessory can have multiple optional characteristics. iterate over them.
381 characteristics.forEach((type, item) -> {
383 // check whether a proxyItem already exists, if not create one.
384 final HomekitOHItemProxy proxyItem = Objects
385 .requireNonNull(proxyItems.computeIfAbsent(item.getUID(), k -> new HomekitOHItemProxy(item)));
386 final HomekitTaggedItem optionalItem = new HomekitTaggedItem(proxyItem,
387 accessory.getRootAccessory().getAccessoryType(), type,
388 accessory.getRootAccessory().getRootDeviceGroupItem(),
389 getItemConfiguration(item, metadataRegistry));
390 final Characteristic characteristic = HomekitCharacteristicFactory.createCharacteristic(optionalItem,
391 accessory.getUpdater());
392 accessory.addCharacteristic(optionalItem, characteristic);
393 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | HomekitException e) {
394 LOGGER.warn("Unsupported optional HomeKit characteristic: type {}, characteristic type {}",
395 accessory.getPrimaryService(), type.getTag());
401 * add optional characteristics for given accessory from metadata
403 * @param taggedItem main item
404 * @param accessory accessory
406 private static void addOptionalMetadataCharacteristics(HomekitTaggedItem taggedItem,
407 AbstractHomekitAccessoryImpl accessory)
408 throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, HomekitException {
409 // Check every metadata key looking for a characteristics we can create
410 var config = taggedItem.getConfiguration();
411 if (config == null) {
414 for (var entry : config.entrySet().stream().sorted((lhs, rhs) -> lhs.getKey().compareTo(rhs.getKey()))
415 .collect(Collectors.toList())) {
416 var characteristic = HomekitMetadataCharacteristicFactory.createCharacteristic(entry.getKey(),
418 if (characteristic.isPresent()) {
419 accessory.addCharacteristic(characteristic.get());
425 * creates HomeKit services for an openhab item that are members of this group item.
427 * @param taggedItem openhab item tagged as HomeKit item
428 * @param AbstractHomekitAccessoryImpl the accessory to add services to
429 * @param metadataRegistry openhab metadata registry required to get item meta information
430 * @param updater OH HomeKit update class that ensure the status sync between OH item and corresponding HomeKit
432 * @param settings OH settings
433 * @param ancestorServices set of all accessories/services under the same root accessory, for
434 * for preventing circular references
435 * @throws HomekitException exception in case HomeKit accessory could not be created, e.g. due missing mandatory
438 private static void addLinkedServices(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
439 MetadataRegistry metadataRegistry, HomekitAccessoryUpdater updater, HomekitSettings settings,
440 Set<HomekitTaggedItem> ancestorServices) throws HomekitException {
441 final var item = taggedItem.getItem();
442 if (!(item instanceof GroupItem)) {
446 for (var groupMember : ((GroupItem) item).getMembers().stream()
447 .sorted((lhs, rhs) -> lhs.getName().compareTo(rhs.getName())).collect(Collectors.toList())) {
448 final var characteristicTypes = getAccessoryTypes(groupMember, metadataRegistry);
449 var accessoryTypes = characteristicTypes.stream().filter(HomekitAccessoryFactory::isRootAccessory)
450 .collect(Collectors.toList());
452 LOGGER.trace("accessory types for {} are {}", groupMember.getName(), accessoryTypes);
453 if (accessoryTypes.isEmpty()) {
457 if (accessoryTypes.size() > 1) {
458 LOGGER.warn("Item {} is a HomeKit sub-accessory, but multiple accessory types are not allowed.",
459 groupMember.getName());
463 final @Nullable Map<String, Object> itemConfiguration = getItemConfiguration(groupMember, metadataRegistry);
465 final var accessoryType = accessoryTypes.iterator().next().getKey();
466 LOGGER.trace("Item {} is a HomeKit sub-accessory of type {}.", groupMember.getName(), accessoryType);
467 final var itemProxy = new HomekitOHItemProxy(groupMember);
468 final var subTaggedItem = new HomekitTaggedItem(itemProxy, accessoryType, itemConfiguration);
469 final var subAccessory = create(subTaggedItem, metadataRegistry, updater, settings, ancestorServices);
470 subAccessory.promoteNameCharacteristic();
472 if (subAccessory.isLinkable(accessory)) {
473 accessory.getPrimaryService().addLinkedService(subAccessory.getPrimaryService());
475 accessory.getServices().add(subAccessory.getPrimaryService());
481 * collect optional HomeKit characteristics for a OH item.
483 * @param taggedItem main OH item
484 * @param metadataRegistry OH metadata registry
485 * @return a map with characteristics and corresponding OH items
487 private static Map<HomekitCharacteristicType, GenericItem> getOptionalCharacteristics(HomekitTaggedItem taggedItem,
488 MetadataRegistry metadataRegistry) {
489 Map<HomekitCharacteristicType, GenericItem> characteristicItems = new TreeMap<>();
490 if (taggedItem.isGroup()) {
491 GroupItem groupItem = (GroupItem) taggedItem.getItem();
492 groupItem.getMembers().forEach(item -> getAccessoryTypes(item, metadataRegistry).stream()
493 .filter(c -> !isRootAccessory(c)).filter(c -> belongsToType(taggedItem.getAccessoryType(), c))
494 .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
495 .forEach(characteristic -> characteristicItems.put(characteristic.getValue(), (GenericItem) item)));
497 getAccessoryTypes(taggedItem.getItem(), metadataRegistry).stream().filter(c -> !isRootAccessory(c))
498 .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
499 .forEach(characteristic -> characteristicItems.put(characteristic.getValue(),
500 (GenericItem) taggedItem.getItem()));
502 LOGGER.trace("Optional characteristics for item {}: {}", taggedItem.getName(), characteristicItems.values());
503 return Collections.unmodifiableMap(characteristicItems);
507 * return true is characteristic is a mandatory characteristic for the accessory.
510 * @param characteristic characteristic
511 * @return true if characteristic is mandatory, false if not mandatory
513 private static boolean isMandatoryCharacteristic(HomekitTaggedItem item, HomekitCharacteristicType characteristic) {
514 return MANDATORY_CHARACTERISTICS.containsKey(item.getAccessoryType())
515 && getRequiredCharacteristics(item).contains(characteristic);
519 * check whether accessory is root accessory, i.e. without characteristic tag.
521 * @param accessory accessory
522 * @return true if accessory has not characteristic.
524 private static boolean isRootAccessory(Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory) {
525 return ((accessory.getValue() == null) || (accessory.getValue() == EMPTY));
529 * check whether characteristic belongs to the specific accessory type.
530 * characteristic with no accessory type mentioned in metadata are considered as candidates for all types.
532 * @param accessoryType accessory type
533 * @param characteristic characteristic
534 * @return true if characteristic belongs to the accessory type.
536 private static boolean belongsToType(HomekitAccessoryType accessoryType,
537 Entry<HomekitAccessoryType, HomekitCharacteristicType> characteristic) {
538 return ((characteristic.getKey() == accessoryType) || (characteristic.getKey() == DUMMY));