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[] {});
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 });
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 });
115 /** List of service implementation for each accessory type. **/
116 private static final Map<HomekitAccessoryType, Class<? extends AbstractHomekitAccessoryImpl>> SERVICE_IMPL_MAP = new HashMap<>() {
118 put(ACCESSORY_GROUP, HomekitAccessoryGroupImpl.class);
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);
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())));
164 if (taggedItem.getAccessoryType() == BATTERY) {
165 final boolean isChargeable = taggedItem.getConfigurationAsBoolean(HomekitBatteryImpl.BATTERY_TYPE, false);
167 characteristics.add(BATTERY_CHARGING_STATE);
170 return characteristics;
174 * creates HomeKit accessory for an openhab item.
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
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
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);
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<>();
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");
207 AbstractHomekitAccessoryImpl accessoryImpl;
209 final @Nullable Class<? extends AbstractHomekitAccessoryImpl> accessoryImplClass = SERVICE_IMPL_MAP
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");
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;
229 LOGGER.warn("Unsupported HomeKit type: {}", accessoryType.getTag());
230 throw new HomekitException("Unsupported HomeKit type: " + accessoryType);
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);
240 * return HomeKit accessory types for an OH item based on meta data
242 * @param item OH item
243 * @param metadataRegistry meta data registry
244 * @return list of HomeKit accessory types and characteristics.
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));
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)));
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;
279 * return list of HomeKit relevant groups linked to an accessory
281 * @param item OH item
282 * @param itemRegistry item registry
283 * @param metadataRegistry metadata registry
284 * @return list of relevant group items
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);
293 return Stream.empty();
295 }).filter(groupItem -> !getAccessoryTypes(groupItem, metadataRegistry).isEmpty()).collect(Collectors.toList());
299 * collect all mandatory characteristics for a given tagged item, e.g. collect all mandatory HomeKit items from a
302 * @param taggedItem HomeKit tagged item
303 * @param metadataRegistry meta data registry
304 * @return list of mandatory
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);
314 addMandatoryCharacteristics(taggedItem, characteristics, rawCharacteristics, taggedItem.getItem(),
317 LOGGER.trace("Mandatory characteristics: {}, {}", characteristics, rawCharacteristics);
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
327 * @param mainItem main item
328 * @param characteristics list of characteristics
329 * @param item current item
330 * @param metadataRegistry meta date registry
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
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,
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))));
357 // item has characteristic tag on it, so, adding it as that characteristic.
359 final HomekitCharacteristicType characteristic = accessory.getValue();
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)));
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) {
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(),
383 characteristic.ifPresent(rc -> rawCharacteristics.add(rc));
390 * add optional characteristics for given accessory.
392 * @param taggedItem main item
393 * @param accessory accessory
394 * @param metadataRegistry metadata registry
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) -> {
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());
423 * add optional characteristics for given accessory from metadata
425 * @param taggedItem main item
426 * @param accessory accessory
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) {
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(),
440 if (characteristic.isPresent()) {
441 accessory.addCharacteristic(characteristic.get());
447 * creates HomeKit services for an openhab item that are members of this group item.
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
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
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)) {
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());
474 LOGGER.trace("accessory types for {} are {}", groupMember.getName(), accessoryTypes);
475 if (accessoryTypes.isEmpty()) {
479 if (accessoryTypes.size() > 1) {
480 LOGGER.warn("Item {} is a HomeKit sub-accessory, but multiple accessory types are not allowed.",
481 groupMember.getName());
485 final @Nullable Map<String, Object> itemConfiguration = getItemConfiguration(groupMember, metadataRegistry);
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();
494 if (subAccessory.isLinkable(accessory)) {
495 accessory.getPrimaryService().addLinkedService(subAccessory.getPrimaryService());
497 accessory.getServices().add(subAccessory.getPrimaryService());
503 * collect optional HomeKit characteristics for a OH item.
505 * @param taggedItem main OH item
506 * @param metadataRegistry OH metadata registry
507 * @return a map with characteristics and corresponding OH items
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)));
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()));
524 LOGGER.trace("Optional characteristics for item {}: {}", taggedItem.getName(), characteristicItems.values());
525 return Collections.unmodifiableMap(characteristicItems);
529 * return true is characteristic is a mandatory characteristic for the accessory.
532 * @param characteristic characteristic
533 * @return true if characteristic is mandatory, false if not mandatory
535 private static boolean isMandatoryCharacteristic(HomekitTaggedItem item, HomekitCharacteristicType characteristic) {
536 return MANDATORY_CHARACTERISTICS.containsKey(item.getAccessoryType())
537 && getRequiredCharacteristics(item).contains(characteristic);
541 * check whether accessory is root accessory, i.e. without characteristic tag.
543 * @param accessory accessory
544 * @return true if accessory has not characteristic.
546 private static boolean isRootAccessory(Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory) {
547 return ((accessory.getValue() == null) || (accessory.getValue() == EMPTY));
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.
554 * @param accessoryType accessory type
555 * @param characteristic characteristic
556 * @return true if characteristic belongs to the accessory type.
558 private static boolean belongsToType(HomekitAccessoryType accessoryType,
559 Entry<HomekitAccessoryType, HomekitCharacteristicType> characteristic) {
560 return ((characteristic.getKey() == accessoryType) || (characteristic.getKey() == DUMMY));